canton: make ParseAndVerifyVAA's controller the caller, not operator#34
Merged
Merged
Conversation
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).
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).
connorstake
approved these changes
Jul 7, 2026
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.
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
ParseAndVerifyVAA(onCoreState) is documented as being "for the token bridge / external verifiers" — but wascontroller operator, meaning every external protocol neededoperatorto actively co-sign its own read-only verification call.This choice is
nonconsumingand does no create/archive at all — unlikeSubmitGovernanceVAA, which is consuming and creates a successorCoreStategenuinely needingguardianGovernance's co-signature, nothing inParseAndVerifyVAA's body needsoperator'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— addedverifier : Partyas a choice argument, made it the sole controller (wasoperator). Doc comment explains why: any external party can now verify a VAA as itself, withoperatornever an active party. The one real requirement is visibility, not authorization — a non-stakeholder needsCoreStateexplicitly disclosed to reference it, a data attachment servable by any party with read access, not a discretionary approval.canton/test/daml/Test/TestCore.daml—testParseAndVerifyVAAupdated for the new argument; newtestParseAndVerifyVAAByExternalVerifierproves an external party with no relationship toCoreStateat all (notoperator,guardianGovernance, orguardianObserver) can verify a real guardian-signed VAA as itself, given onlyCoreStatedisclosed to it.canton/README.md— §4.3'sParseAndVerifyVAAsketch and prose updated to match.Context
This is the same fix as
Wormhole.Ntt.Manager.Transfer'scontroller userchange on thecanton-keyed-nttbranch (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.ParseAndVerifyVAAlives in already-merged core code onintegration/canton, unrelated to NTT, so it's its own standalone PR rather than being bundled into that one.Testing
dpm build --allclean;dpm test13/13 (1 new).Live integration test added, per review request:
node/pkg/watchers/canton/core_verify_vaa_integration_test.goboots a real Canton sandbox and runsTest.TestCore:integrationParseAndVerifyVAAByExternalVerifier, which allocates a fresh external party (no relationship toCoreState— notoperator, notguardianGovernance), hasCoreStatedisclosed 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.