Skip to content

canton: read-only guardianObserver party for guardian read visibility#32

Merged
bengtlofgren merged 5 commits into
integration/cantonfrom
canton-guardian-observer
Jul 8, 2026
Merged

canton: read-only guardianObserver party for guardian read visibility#32
bengtlofgren merged 5 commits into
integration/cantonfrom
canton-guardian-observer

Conversation

@bengtlofgren

Copy link
Copy Markdown

Summary

Adds a dedicated read-only guardianObserver party so guardians can observe the core-bridge attestation surface (message publication and guardian-set/governance state) without being placed in Canton's confirmation path.

guardianObserver is a Daml template observer on exactly Emitter and CoreState — 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 the operator (authority) and guardianGovernance (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 : Party field + observer guardianObserver on Emitter and CoreState; field-only (not an observer) on EmitterRegistry, threaded into new Emitters via an extended AllocateEmitterId result (no second key resolution).
  • Core/Setup.daml: mkInitialCoreState / mkInitialEmitterRegistry take a guardianObserver parameter.
  • Test/TestCore.daml: setup allocates 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, --cantonReadAsParty production guidance, topology section (Observation permission, non-blocking rationale, least-privilege scoping), resolves Open Question 3 to residual ops items.
  • guardiand watcher: flag-help / comment only — no Go logic change.

Testing

dpm build --all clean; dpm test — 17/17 scripts pass (5 new + 12 existing). Cross-language address vector unchanged.

Out of scope / follow-ups

  • Go integration test reading as guardianObserver (ledger-API-level proof of the informee rule) — tracked, not included.
  • Production PartyToParticipant runbook (Observation permission; party-before-genesis or party replication) and observer namespace-key custody — ops decisions.

Independent of the NTT and cs/canton-nits PRs (all off integration/canton); will need a trivial rebase against #31 (one submitMultiactAs line) whenever both land.

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
bengtlofgren requested a review from kcsongor as a code owner July 7, 2026 02:03
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.
@bengtlofgren
bengtlofgren merged commit 771601a into integration/canton Jul 8, 2026
33 of 36 checks passed
@bengtlofgren
bengtlofgren deleted the canton-guardian-observer branch July 8, 2026 10:57
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants