Problem
For transition families whose proof cannot be bound to execution, waitForStateTransitionResult returns a verified snapshot of the affected keys, tagged AffectedState. The snapshot authenticates state at a committed height but cannot distinguish "transition executed" from "transition dropped and the state never changed". A node that withholds the real outcome can therefore serve a valid quorum-signed proof of unchanged state, and a client that accepts the snapshot as confirmation reports success for a transition that never executed. This was raised as a blocking finding on #4214; the SDK-wide design predates that PR (the Rust SDK routes the same families through the *_affected_state wait APIs).
The affected families: data-contract updates, identity top-ups/transfers/withdrawals, address-funds operations, shields, shield-from-asset-lock, identity creation from the shielded pool, and no-history token operations.
Why the proofs are not binding today
The gap is on the proving side, not just verification. For example, for IdentityCreditTransfer the server builds the proof from balance queries only (packages/rs-drive/src/prove/prove_state_transition/v0/mod.rs, StateTransition::IdentityCreditTransfer branch) — the sender's identity nonce key is not in the proof, and the verify side documents this ("whose signature and nonce are not in the proof", packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs).
Proposed fix: execution binders in the snapshot proof
Prove a transition-specific monotonic key alongside the affected state, so the client can reject a pre-execution snapshot outright. The identity-nonce merged-bitmask encoding makes this a specific-nonce check (was nonce N consumed), not merely a monotonic one, conclusive within the bitmask window — which is sufficient for a wait that runs immediately after broadcast.
Per-family, the cost is a three-tier spread:
Tier 1 — client-only enforcement (the proof already contains the binder):
- Address-funds operations already prove
(AddressNonce, balance) per address; the verify branch decodes the nonce and does not compare it against the transition's expected nonces.
- No-history token operations already include
identity_contract_nonce in their execution path queries (packages/rs-drive/src/verify/state_transition/state_transition_execution_path_queries/token_transition.rs).
Tier 2 — both sides, mechanical:
- Identity credit transfer and identity credit withdrawal: merge the identity-nonce key into the server path query and check the consumed-nonce bitmask in verify.
- Data-contract update: bind via the identity contract nonce (the version-matched contract body is already close to binding, but nonce binding removes the remaining ambiguity).
Tier 3 — needs design:
- Identity top-up carries no nonce; its natural binder is the asset-lock outpoint marked spent in state.
- Shielded families (shield, shield-from-asset-lock, identity-create-from-shielded-pool) bind via nullifier presence in the pool state.
Both need new proof queries and a decision on tree layout/retention.
Cross-cutting plumbing: StateTransitionProofOutcome/StateTransitionProofResult in rs-dpp carry the binding verdict (with the json-safe serialization mirror), wasm-dpp2's convert_proof_result follows, and the rs-sdk affected-state waits gain a "bound snapshot" tier between today's snapshot and a full execution proof.
Deployment
Proof generation is not consensus (no app-hash impact), but old nodes will serve binder-less proofs during rollout, so clients must require the binder subtree only when they can expect it — gate enforcement on the protocol version that ships the proving change.
Suggested split
- Tier 1 + result-type plumbing (immediately shrinks the unbound surface, no server change).
- Tier 2 server+client for credit transfer / withdrawal / contract update.
- Tier 3 binders, each behind its own design note.
Until this lands, consumers of affected-state results (e.g. the E2E platform proof verifier from #4214) should assert expected deltas (balances, nonces) in the tests themselves rather than treating snapshot verification as execution confirmation.
Problem
For transition families whose proof cannot be bound to execution,
waitForStateTransitionResultreturns a verified snapshot of the affected keys, taggedAffectedState. The snapshot authenticates state at a committed height but cannot distinguish "transition executed" from "transition dropped and the state never changed". A node that withholds the real outcome can therefore serve a valid quorum-signed proof of unchanged state, and a client that accepts the snapshot as confirmation reports success for a transition that never executed. This was raised as a blocking finding on #4214; the SDK-wide design predates that PR (the Rust SDK routes the same families through the*_affected_statewait APIs).The affected families: data-contract updates, identity top-ups/transfers/withdrawals, address-funds operations, shields, shield-from-asset-lock, identity creation from the shielded pool, and no-history token operations.
Why the proofs are not binding today
The gap is on the proving side, not just verification. For example, for
IdentityCreditTransferthe server builds the proof from balance queries only (packages/rs-drive/src/prove/prove_state_transition/v0/mod.rs,StateTransition::IdentityCreditTransferbranch) — the sender's identity nonce key is not in the proof, and the verify side documents this ("whose signature and nonce are not in the proof",packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs).Proposed fix: execution binders in the snapshot proof
Prove a transition-specific monotonic key alongside the affected state, so the client can reject a pre-execution snapshot outright. The identity-nonce merged-bitmask encoding makes this a specific-nonce check (was nonce N consumed), not merely a monotonic one, conclusive within the bitmask window — which is sufficient for a wait that runs immediately after broadcast.
Per-family, the cost is a three-tier spread:
Tier 1 — client-only enforcement (the proof already contains the binder):
(AddressNonce, balance)per address; the verify branch decodes the nonce and does not compare it against the transition's expected nonces.identity_contract_noncein their execution path queries (packages/rs-drive/src/verify/state_transition/state_transition_execution_path_queries/token_transition.rs).Tier 2 — both sides, mechanical:
Tier 3 — needs design:
Both need new proof queries and a decision on tree layout/retention.
Cross-cutting plumbing:
StateTransitionProofOutcome/StateTransitionProofResultin rs-dpp carry the binding verdict (with the json-safe serialization mirror), wasm-dpp2'sconvert_proof_resultfollows, and the rs-sdk affected-state waits gain a "bound snapshot" tier between today's snapshot and a full execution proof.Deployment
Proof generation is not consensus (no app-hash impact), but old nodes will serve binder-less proofs during rollout, so clients must require the binder subtree only when they can expect it — gate enforcement on the protocol version that ships the proving change.
Suggested split
Until this lands, consumers of affected-state results (e.g. the E2E platform proof verifier from #4214) should assert expected deltas (balances, nonces) in the tests themselves rather than treating snapshot verification as execution confirmation.