canton: read-only guardianObserver party for guardian read visibility#32
Merged
Merged
Conversation
Introduce a dedicated read-only guardianObserver party as a template observer on exactly Emitter and CoreState (the attestation surface), so guardians get full read visibility of the contracts and events they attest without being in Canton's confirmation path. The party is carried as a template field, never a signatory or controller. - Emitter/CoreState: guardianObserver field + observer clause. Because PublishMessage and SubmitGovernanceVAA are consuming, observers are informees of the exercise and its result, delivering the message surface and governance transitions over the update stream. - EmitterRegistry: field only (not observer); AllocateEmitterId's result is extended to carry the party so ApproveEmitter threads it into each Emitter from a single key resolution. - mkInitialCoreState / mkInitialEmitterRegistry gain a guardianObserver parameter; Test.TestCore:setup allocates the party and returns it (operator kept first for bootstrap.sh's party extraction). - bootstrap.sh logs the guardianObserver party id. - New tests (Test/TestGuardianObserver.daml) cover visibility of the message surface and CoreState, absence of authority, unchanged publish authority, and out-of-scope invisibility.
Update canton/README.md for the read-only guardianObserver: template listings (§4.1 CoreState, §4.2 Emitter/EmitterRegistry) with the informee rule that makes a template observer see consuming exercises; production --cantonReadAsParty guidance (§7.1, §7.4); the deployment topology (§10) hosting the party at Observation permission with the two-layer non-blocking argument, operator replica-hosting kept as a fallback, and the least-privilege scoping rule; resolve Open Question 3 to the residual runbook/namespace-key-custody items; add the new test module to the file map. Also update the flag help (node.go) and WatcherConfig.ReadAsParty comment (config.go) to recommend the guardianObserver party for production guardians. Comments/help text only; no Go logic change.
bengtlofgren
force-pushed
the
canton-guardian-observer
branch
from
July 7, 2026 01:50
b382d59 to
870a717
Compare
informee, informees, runbook
bengtlofgren
added a commit
that referenced
this pull request
Jul 7, 2026
…erver core Rebases the token-bridge example onto canton-guardian-observer (stacked, since feat/canton-public-observer is closed). The example predates both #29 (registry- keyed Emitter/EmitterRegistry, no more EmitterIdentity) and #32 (guardianObserver replacing the generic public party), so this adapts rather than just replays: - mkInitialCoreState now takes guardianGovernance + guardianObserver; setup also creates an EmitterRegistry via mkInitialEmitterRegistry. - ApproveEmitter returns ContractId Emitter directly (no coreStateCid arg, no separate EmitterIdentity contract). - WormholeMessage carries registrar/owner/emitterId directly instead of an identity contract-id; TestTokenBridge and the Go integration test assert on those fields and derive the address via deriveEmitterAddress, matching the pattern in watcher_integration_test.go. - TokenBridge's public field/observer renamed to guardianObserver. - daml.yaml bumped to sdk-version 3.5.1 / --target=2.3 to match core. Verified: dpm build --all and dpm test --all pass (all 3 scripts); go vet -tags integration ./pkg/watchers/canton/... is clean.
This was referenced Jul 7, 2026
connorstake
approved these changes
Jul 8, 2026
bengtlofgren
added a commit
that referenced
this pull request
Jul 8, 2026
…erver core Rebases the token-bridge example onto canton-guardian-observer (stacked, since feat/canton-public-observer is closed). The example predates both #29 (registry- keyed Emitter/EmitterRegistry, no more EmitterIdentity) and #32 (guardianObserver replacing the generic public party), so this adapts rather than just replays: - mkInitialCoreState now takes guardianGovernance + guardianObserver; setup also creates an EmitterRegistry via mkInitialEmitterRegistry. - ApproveEmitter returns ContractId Emitter directly (no coreStateCid arg, no separate EmitterIdentity contract). - WormholeMessage carries registrar/owner/emitterId directly instead of an identity contract-id; TestTokenBridge and the Go integration test assert on those fields and derive the address via deriveEmitterAddress, matching the pattern in watcher_integration_test.go. - TokenBridge's public field/observer renamed to guardianObserver. - daml.yaml bumped to sdk-version 3.5.1 / --target=2.3 to match core. Verified: dpm build --all and dpm test --all pass (all 3 scripts); go vet -tags integration ./pkg/watchers/canton/... is clean.
bengtlofgren
added a commit
that referenced
this pull request
Jul 9, 2026
The rebase onto integration/canton (which absorbed #32's guardianObserver threading) changed Test.TestCore:setup to return (operator, guardianObserver, csId); destructure accordingly in the tests added by this branch. Claude-Session: https://claude.ai/code/session_01QJMuDHkjKHxZ3KvoS9Ryyz
bengtlofgren
added a commit
that referenced
this pull request
Jul 9, 2026
* canton: per-VAA replay guards, consumer-scoped and operator-co-signed * canton: propose-accept example integrator; pin fail-open lookup semantics * canton: document per-VAA replay protection * canton: adapt replay tests to the 3-tuple setup from integration/canton The rebase onto integration/canton (which absorbed #32's guardianObserver threading) changed Test.TestCore:setup to return (operator, guardianObserver, csId); destructure accordingly in the tests added by this branch. Claude-Session: https://claude.ai/code/session_01QJMuDHkjKHxZ3KvoS9Ryyz * canton: factor shared VAA verification into verifyEncoded ParseAndVerifyVAA and VerifyAndConsumeVAA carried byte-identical verification blocks (parse, resolve the named guardian set, verify signatures). Extract it as a helper both choices call, so verification semantics have one source of truth. SubmitGovernanceVAA stays separate: it accepts the CURRENT set only, per EVM Governance.sol. No transaction-stream change: this is a plain function, not a nested choice exercise, so observers see the same events as before. Claude-Session: https://claude.ai/code/session_01QJMuDHkjKHxZ3KvoS9Ryyz * canton: add reserialize(d) to cspell custom words Claude-Session: https://claude.ai/code/session_01QJMuDHkjKHxZ3KvoS9Ryyz --------- Co-authored-by: Bengt Lofgren <bengtlofgren8@gmail.com>
bengtlofgren
added a commit
that referenced
this pull request
Jul 9, 2026
…erver core Rebases the token-bridge example onto canton-guardian-observer (stacked, since feat/canton-public-observer is closed). The example predates both #29 (registry- keyed Emitter/EmitterRegistry, no more EmitterIdentity) and #32 (guardianObserver replacing the generic public party), so this adapts rather than just replays: - mkInitialCoreState now takes guardianGovernance + guardianObserver; setup also creates an EmitterRegistry via mkInitialEmitterRegistry. - ApproveEmitter returns ContractId Emitter directly (no coreStateCid arg, no separate EmitterIdentity contract). - WormholeMessage carries registrar/owner/emitterId directly instead of an identity contract-id; TestTokenBridge and the Go integration test assert on those fields and derive the address via deriveEmitterAddress, matching the pattern in watcher_integration_test.go. - TokenBridge's public field/observer renamed to guardianObserver. - daml.yaml bumped to sdk-version 3.5.1 / --target=2.3 to match core. Verified: dpm build --all and dpm test --all pass (all 3 scripts); go vet -tags integration ./pkg/watchers/canton/... is clean.
bengtlofgren
added a commit
that referenced
this pull request
Jul 16, 2026
…erver core Rebases the token-bridge example onto canton-guardian-observer (stacked, since feat/canton-public-observer is closed). The example predates both #29 (registry- keyed Emitter/EmitterRegistry, no more EmitterIdentity) and #32 (guardianObserver replacing the generic public party), so this adapts rather than just replays: - mkInitialCoreState now takes guardianGovernance + guardianObserver; setup also creates an EmitterRegistry via mkInitialEmitterRegistry. - ApproveEmitter returns ContractId Emitter directly (no coreStateCid arg, no separate EmitterIdentity contract). - WormholeMessage carries registrar/owner/emitterId directly instead of an identity contract-id; TestTokenBridge and the Go integration test assert on those fields and derive the address via deriveEmitterAddress, matching the pattern in watcher_integration_test.go. - TokenBridge's public field/observer renamed to guardianObserver. - daml.yaml bumped to sdk-version 3.5.1 / --target=2.3 to match core. Verified: dpm build --all and dpm test --all pass (all 3 scripts); go vet -tags integration ./pkg/watchers/canton/... is clean.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a dedicated read-only
guardianObserverparty so guardians can observe the core-bridge attestation surface (message publication and guardian-set/governance state) without being placed in Canton's confirmation path.guardianObserveris a Daml templateobserveron exactlyEmitterandCoreState— never a signatory or controller. Because a template observer is a stakeholder, it is an informee of every consuming exercise on those contracts (e.g.PublishMessage,SubmitGovernanceVAA), giving guardians the full message + governance stream to read. It is intentionally distinct from theoperator(authority) andguardianGovernance(signer) parties. Hosting it at Observation permission keeps guardian downtime off the transaction-confirmation path (fail-safe: a blinded guardian simply stops signing).Changes
Core/State.daml:guardianObserver : Partyfield +observer guardianObserveronEmitterandCoreState; field-only (not an observer) onEmitterRegistry, threaded into newEmitters via an extendedAllocateEmitterIdresult (no second key resolution).Core/Setup.daml:mkInitialCoreState/mkInitialEmitterRegistrytake aguardianObserverparameter.Test/TestCore.daml:setupallocates the party and returns it (operator kept first so bootstrap extraction is unchanged); call sites updated.Test/TestGuardianObserver.daml(new): 5 tests — sees published messages, sees CoreState/governance transitions, has no authority, publishing needs only the owner, and does not see out-of-scope contracts.devnet/bootstrap.sh: logs the observer party id.README.md: template listings + informee rule,--cantonReadAsPartyproduction guidance, topology section (Observation permission, non-blocking rationale, least-privilege scoping), resolves Open Question 3 to residual ops items.guardiandwatcher: flag-help / comment only — no Go logic change.Testing
dpm build --allclean;dpm test— 17/17 scripts pass (5 new + 12 existing). Cross-language address vector unchanged.Out of scope / follow-ups
guardianObserver(ledger-API-level proof of the informee rule) — tracked, not included.PartyToParticipantrunbook (Observation permission; party-before-genesis or party replication) and observer namespace-key custody — ops decisions.Independent of the NTT and
cs/canton-nitsPRs (all offintegration/canton); will need a trivial rebase against #31 (onesubmitMulti→actAsline) whenever both land.