Skip to content

canton: make ParseAndVerifyVAA's controller the caller, not operator#34

Merged
bengtlofgren merged 3 commits into
integration/cantonfrom
canton-verifyvaa-permissionless
Jul 8, 2026
Merged

canton: make ParseAndVerifyVAA's controller the caller, not operator#34
bengtlofgren merged 3 commits into
integration/cantonfrom
canton-verifyvaa-permissionless

Conversation

@bengtlofgren

@bengtlofgren bengtlofgren commented Jul 7, 2026

Copy link
Copy Markdown

Summary

ParseAndVerifyVAA (on CoreState) is documented as being "for the token bridge / external verifiers" — but was controller operator, meaning every external protocol needed operator to actively co-sign its own read-only verification call.

This choice is nonconsuming and does no create/archive at all — unlike SubmitGovernanceVAA, which is consuming and creates a successor CoreState genuinely needing guardianGovernance's co-signature, nothing in ParseAndVerifyVAA's body needs operator's authority for any structural reason. It's pure computation: parse, look up the guardian set, verify, return.

Changes

  • canton/core/daml/Wormhole/Core/State.daml — added verifier : Party as a choice argument, made it the sole controller (was operator). Doc comment explains why: any external party can now verify a VAA as itself, with operator never an active party. The one real requirement is visibility, not authorization — a non-stakeholder needs CoreState explicitly disclosed to reference it, a data attachment servable by any party with read access, not a discretionary approval.
  • canton/test/daml/Test/TestCore.damltestParseAndVerifyVAA updated for the new argument; new testParseAndVerifyVAAByExternalVerifier proves an external party with no relationship to CoreState at all (not operator, guardianGovernance, or guardianObserver) can verify a real guardian-signed VAA as itself, given only CoreState disclosed to it.
  • canton/README.md — §4.3's ParseAndVerifyVAA sketch and prose updated to match.

Context

This is the same fix as Wormhole.Ntt.Manager.Transfer's controller user change on the canton-keyed-ntt branch (PR #30) — that's where the underlying mechanism (inherited signatory authority from a consuming exercise satisfies nested controller requirements; the real blocker for a non-stakeholder caller is visibility, not authorization) was first verified empirically. ParseAndVerifyVAA lives in already-merged core code on integration/canton, unrelated to NTT, so it's its own standalone PR rather than being bundled into that one.

Testing

dpm build --all clean; dpm test 13/13 (1 new).


Live integration test added, per review request: node/pkg/watchers/canton/core_verify_vaa_integration_test.go boots a real Canton sandbox and runs Test.TestCore:integrationParseAndVerifyVAAByExternalVerifier, which allocates a fresh external party (no relationship to CoreState — not operator, not guardianGovernance), has CoreState disclosed to it, and verifies a real guardian-signed VAA as that party alone. Ran it live: PASS (23.4s), clean teardown, no stray processes. This is no longer just an in-memory Script-interpreter claim — it's proven against a real participant.

ParseAndVerifyVAA is documented as being "for the token bridge / external
verifiers" -- but was controller operator, meaning every external protocol
needed operator to actively co-sign its own read-only verification call.

This is nonconsuming and does no create/archive at all -- unlike
SubmitGovernanceVAA (which IS consuming and creates a successor CoreState
genuinely needing guardianGovernance's co-signature), nothing in this choice's
body needs operator's authority for any structural reason. It's pure
computation: parse, look up the guardian set, verify, return.

Added `verifier : Party` as a choice argument and made it the sole controller.
Any external party can now verify a VAA as itself, with operator never an
active party. The one real requirement is visibility, not authorization: a
non-stakeholder needs CoreState explicitly disclosed to reference it -- a data
attachment servable by any party with read access, not a discretionary
approval. Same pattern as Wormhole.Ntt.Manager.Transfer's controller-user fix
on the canton-keyed-ntt branch (PR #30), which is where this was first
verified empirically; this is the same fix applied to already-merged core
code, hence its own standalone change against integration/canton rather than
being bundled into that NTT-specific PR.

testParseAndVerifyVAAByExternalVerifier proves it end-to-end: an external
party with no relationship to CoreState (not operator, guardianGovernance, or
guardianObserver) verifies a real guardian-signed VAA as itself, given only
CoreState disclosed to it.

Verified: dpm build --all clean; dpm test 13/13 (1 new).
@bengtlofgren
bengtlofgren requested a review from evan-gray as a code owner July 7, 2026 19:45
Proves ParseAndVerifyVAA is callable by an external "verifier" party that is
NOT a stakeholder of CoreState (CoreState's only stakeholders are operator and
guardianGovernance) against a real Canton sandbox/participant, not just the
Script interpreter.

Test.TestCore:integrationParseAndVerifyVAAByExternalVerifier allocates a fresh
external party, has CoreState explicitly disclosed to it, and verifies a real
guardian-signed VAA as that party alone -- operator is never active in the
submission. node/pkg/watchers/canton/core_verify_vaa_integration_test.go boots
a live sandbox, runs it via `dpm script`, and confirms the returned party is a
real, live-hosted party id.

Also fixed a copy-paste inaccuracy in testParseAndVerifyVAAByExternalVerifier's
docstring from the previous commit: it named guardianObserver as one of
CoreState's stakeholders, but that party doesn't exist on this branch
(integration/canton) -- CoreState's only stakeholders here are operator and
guardianGovernance.

Verified live:
  go test -tags integration -run TestCantonParseAndVerifyVAAExternalVerifierIntegration ./pkg/watchers/canton -v
PASS (23.4s), clean teardown, no stray processes. dpm build --all / dpm test
also clean (14 scripts, 1 new).
submit (actAs tokenBridge <> discloseMany [dCs]) is the general
SubmitOptions-builder form, needed when multiple actAs parties are required
alongside disclosure. Neither call site here needs more than one actAs party,
so this was just habit -- submitWithDisclosures is strictly equivalent (it's
literally defined as `submitWithDisclosures p ds cmds = submit (actAs p <>
discloseMany ds) cmds` in Daml.Script.Internal.Questions.Submit) and reads
more directly: the function name says "this uses disclosure" instead of
requiring the reader to notice `<> discloseMany` tacked onto a builder.

Also added a direct link to Daml's Explicit Contract Disclosure docs at the
point ParseAndVerifyVAA's comment first introduces the mechanism, and named
submitWithDisclosures as the Script-side idiom, so a future reader can look up
the primary source instead of just trusting the comment.

Verified: dpm build --all clean; dpm test passes (3/3 ParseAndVerifyVAA
scripts); live integration test re-run and still passes
(TestCantonParseAndVerifyVAAExternalVerifierIntegration, PASS, 24.3s).
@bengtlofgren
bengtlofgren merged commit 3638f6a into integration/canton Jul 8, 2026
32 of 35 checks passed
@bengtlofgren
bengtlofgren deleted the canton-verifyvaa-permissionless branch July 8, 2026 10:59
bengtlofgren added a commit that referenced this pull request Jul 16, 2026
…h, keyless emitter)

Rebased onto current integration/canton. Adapts the example to core changes
merged since PR #34:

- Repin core data-dependency 0.1.0 -> 0.2.0; add the token-allocation DAR
  (referenced from shared dars/ to preserve package-id identity).
- PublishMessage now charges the message fee (token-standard fee payments):
  thread coreStateCid, payer = user, and feeAllocation through LockAndPublish.
- Emitter lost its contract key (core key removal): pass the disclosed
  emitterCid as a choice argument instead of fetchByKey/queryByKey.
- Tests: replace the removed EmitterRequest/ApproveEmitter flow with the
  permissionless EmitterRegistry.RegisterEmitter path; supply the new
  CoreState feeInstrument/feeRecipient and registry registrationFee.

dpm build --all green; 17/17 example scripts pass.
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