Repository navigation
feat(sdk): reject response metadata from an unexpected chain id - #4963
PastaPastaPasta wants to merge 3 commits into
Conversation
Add SdkBuilder::with_expected_chain_id. When set, verify_response_metadata compares the quorum-signed metadata.chain_id first and fails with StaleNodeError::ChainIdMismatch before the time and height checks and the protocol-version ratchet, so a validly signed proof from another Platform chain (e.g. before a testnet reset, signed by the same quorums) cannot advance the height high-water mark or the protocol version. Unset keeps today's behaviour; an empty chain id is a configuration error. A mismatch does not mean the node is unhealthy, and the configured chain id may be the outdated one, so the error is not retryable: sync::retry fails over to another node with the short flat exclusion it already uses for DPNS rejections instead of the exponential health-ban ladder. A misconfigured Sdk therefore keeps returning ChainIdMismatch rather than banning every node and degrading to NoAvailableAddresses. Mocked expect_fetch responses report the expected chain id so mock Sdks built with it keep working. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository: dashpay/platform/.coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (4)
🚧 Files skipped from review as they are similar to previous changes (3)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe SDK can be configured with an expected chain ID and rejects response metadata with a different ID. Chain-ID mismatches enter the failover path with a short flat exclusion instead of the exponential health-ban ladder. Mock proof metadata and tests cover the configured ID. ChangesChain ID validation and failover
Priority: ➖ Normal Estimated code review effort: 3 (Moderate) | ~20 minutes Change: Feature Sequence Diagram(s)sequenceDiagram
participant Sdk
participant SyncRetry
participant NodeA
participant NodeB
Sdk->>NodeA: request with expected chain ID
NodeA-->>Sdk: response metadata with mismatched chain ID
Sdk-->>SyncRetry: ChainIdMismatch
SyncRetry->>NodeA: exclude for up to two seconds
SyncRetry->>NodeB: retry request
Suggested reviewers: Merge Risk: ⚪ Minimal · up to Configured clients reject responses from a different chain, and mismatches can fail over briefly. No actionable merge risk is evident in the reviewed change. 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
✅ Final review complete — no blockers (commit 1f0cb59) · triage: normal |
thepastaclaw
left a comment
There was a problem hiding this comment.
Final validation — Phase 1 + Phase 2
Verified the supplied findings against head a0e44d6. The chain-ID check precedes SDK freshness and protocol-version updates, and mismatch failover uses the intended short exclusion; no blocking defect was confirmed. One consolidated documentation suggestion remains; independent validation was source inspection and a passing diff whitespace check, not a rerun of the reported tests.
🟡 1 suggestion(s)
Review provenance
Source: reviewer 1: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 2: muse-spark-1.3-contributor (agent: phase1-reviewer, role: architecture-layering); reviewer 3: muse-spark-1.3-contributor (agent: phase1-reviewer, role: ffi-engineer); reviewer 4: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); reviewer 5: muse-spark-1.3-contributor (agent: phase1-reviewer, role: security-auditor); reviewer 6: gpt-6-astra (agent: phase2-reviewer, role: general); reviewer 7: gpt-6-astra (agent: phase2-reviewer, role: architecture-layering); reviewer 8: gpt-6-astra (agent: phase2-reviewer, role: ffi-engineer); reviewer 9: gpt-6-astra (agent: phase2-reviewer, role: rust-quality); reviewer 10: gpt-6-astra (agent: phase2-reviewer, role: security-auditor); final verifier: gpt-6-astra (agent: astra-verifier, role: final-verifier)
- Triage:
normalbygpt-6-astra(effort low) — The diff adds opt-in metadata validation with state-update ordering, retry/failover semantics, and a public error variant requiring ordinary logic review, but does not make large or intricate changes to consensus, cryptography, signature verification, or another qualifying critical surface. - Phase 1 reviewers:
muse-spark-1.3-contributor— general (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— architecture-layering (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— ffi-engineer (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— rust-quality (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— security-auditor (completed, effort xhigh); agentphase1-reviewer - Phase 1 model:
muse-spark-1.3-contributor— not quota-gated; passed overgemini-3.8-flash-high(antigravity below 15% reserve: weekly 1% left, 5h 100% left),glm-5.3-flash(not used above high effort; tier asks max) - Fresh verifier:
gpt-6-astra— final-verifier; agentastra-verifier - Phase 2 reviewers:
gpt-6-astra— general (completed, effort high); agentphase2-reviewer,gpt-6-astra— architecture-layering (completed, effort high); agentphase2-reviewer,gpt-6-astra— ffi-engineer (completed, effort high); agentphase2-reviewer,gpt-6-astra— rust-quality (completed, effort high); agentphase2-reviewer,gpt-6-astra— security-auditor (completed, effort high); agentphase2-reviewer
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `packages/rs-sdk/src/sdk.rs`:
- [SUGGESTION] packages/rs-sdk/src/sdk.rs:1198-1200: Describe mismatch failover as a short exclusion, not absence of banning
The builder documentation promises failover without banning the responding server, but `sync::retry_with_additional_error` calls `address_list.ban_for(Duration::from_secs(2), ...)` when an alternative is available and banning is enabled. That updates `banned_until` and floors `ban_count` at 1, so the exclusion is observable through ban information; it avoids the exponential health-ban path rather than bypassing ban state entirely. Describe that distinction here and in `StaleNodeError::ChainIdMismatch` documentation at error.rs:408–410. Also update broadcast.rs:536–538 to describe failover rather than claiming every `StaleNode` error is retryable: this new variant has `can_retry() == false` and reaches failover through the additional-error predicate. The implementation matches the PR's stated short-exclusion design; this is a documentation correction.
Out-of-scope follow-up suggestions (1)
These are valid observations, but they are outside this PR's scope and should be handled in separate issues or author/maintainer-requested PRs rather than blocking this review.
- Expose expected chain id to wasm and FFI builders — The absent binding option is real, but public WASM and native configuration extensions are outside the stated Rust SDK implementation scope. No existing binding behavior regresses, so this routine API follow-up is omitted.
- Follow-up: Consider creating a separate issue or author/maintainer-requested PR for this.
|
Your move: thepastaclaw left review threads unresolved; resolve them. |
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
thepastaclaw
left a comment
There was a problem hiding this comment.
Re-review — Final validation — Phase 1 + Phase 2
Verified the complete PR diff at 50156bc. The chain-ID guard precedes SDK state updates, and mismatch failover uses the existing bounded short-exclusion path; the prior public-documentation finding is fixed, with one non-blocking wording nit remaining in the tests. Source inspection and git diff --check completed successfully; tests were not independently rerun, and the worktree remains unchanged.
💬 1 nitpick(s)
Review provenance
Source: reviewer 1: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 2: muse-spark-1.3-contributor (agent: phase1-reviewer, role: architecture-layering); reviewer 3: muse-spark-1.3-contributor (agent: phase1-reviewer, role: ffi-engineer); reviewer 4: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); reviewer 5: muse-spark-1.3-contributor (agent: phase1-reviewer, role: security-auditor); reviewer 6: gpt-6-astra (agent: phase2-reviewer, role: general); reviewer 7: gpt-6-astra (agent: phase2-reviewer, role: architecture-layering); reviewer 8: gpt-6-astra (agent: phase2-reviewer, role: ffi-engineer); reviewer 9: gpt-6-astra (agent: phase2-reviewer, role: rust-quality); reviewer 10: gpt-6-astra (agent: phase2-reviewer, role: security-auditor); reviewer 11: gpt-6-astra (agent: phase2-reviewer, role: general); reviewer 12: gpt-6-astra (agent: phase2-reviewer, role: architecture-layering); reviewer 13: gpt-6-astra (agent: phase2-reviewer, role: ffi-engineer); reviewer 14: gpt-6-astra (agent: phase2-reviewer, role: rust-quality); reviewer 15: gpt-6-astra (agent: phase2-reviewer, role: security-auditor); final verifier: gpt-6-astra (agent: astra-verifier, role: final-verifier)
- Triage:
normalbygpt-6-astra(effort low) — The change coordinates opt-in chain-ID validation, SDK state-update ordering, retry exclusions, and mock behavior across several files, but does not introduce large or intricate changes to signature verification or another critical surface. - Phase 1 reviewers:
muse-spark-1.3-contributor— general (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— architecture-layering (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— ffi-engineer (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— rust-quality (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— security-auditor (completed, effort xhigh); agentphase1-reviewer - Phase 1 model:
muse-spark-1.3-contributor— not quota-gated; passed overgemini-3.8-flash-high(antigravity below 15% reserve: weekly 1% left, 5h 100% left),glm-5.3-flash(not used above high effort; tier asks max) - Fresh final gate: an independent Phase-2 review ran after iterative findings were reconciled
- Fresh verifier:
gpt-6-astra— final-verifier; agentastra-verifier - Phase 2 reviewers:
gpt-6-astra— general (completed, effort high); agentphase2-reviewer,gpt-6-astra— architecture-layering (completed, effort high); agentphase2-reviewer,gpt-6-astra— ffi-engineer (completed, effort high); agentphase2-reviewer,gpt-6-astra— rust-quality (completed, effort high); agentphase2-reviewer,gpt-6-astra— security-auditor (completed, effort high); agentphase2-reviewer,gpt-6-astra— general (completed, effort high); agentphase2-reviewer,gpt-6-astra— architecture-layering (completed, effort high); agentphase2-reviewer,gpt-6-astra— ffi-engineer (completed, effort high); agentphase2-reviewer,gpt-6-astra— rust-quality (completed, effort high); agentphase2-reviewer,gpt-6-astra— security-auditor (completed, effort high); agentphase2-reviewer
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `packages/rs-sdk/src/sync.rs`:
- [NITPICK] packages/rs-sdk/src/sync.rs:350-353: Describe the tests as avoiding health bans, not all banning
The test comment and name say that neither node is banned, but mismatch failover calls ban_for with a two-second duration when an alternative is available and banning is enabled. That updates banned_until and floors ban_count at 1; the test correctly permits this short exclusion. Rename the test and describe the absence of exponential health banning rather than all ban state. The same distinction applies to the assertion message at sdk.rs:1731: !error.can_retry() prevents the health-ban path, not the separate short exclusion. These are test-wording corrections only; the assertions and production behavior match the intended design.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
thepastaclaw
left a comment
There was a problem hiding this comment.
Re-review — Final validation — Phase 1 + Phase 2
Independently reviewed the complete diff at 1f0cb59 and found no actionable in-scope defects: the configured chain-ID check precedes freshness and protocol-version mutations, and mismatch failover uses the bounded short-exclusion path. Both prior documentation findings are fixed. Source inspection and git diff --check passed; tests were not rerun during this verification.
🔴 0 blocking | 🟡 0 suggestion(s) | 💬 0 nitpick(s)
Review provenance
Source: reviewer 1: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 2: muse-spark-1.3-contributor (agent: phase1-reviewer, role: architecture-layering); reviewer 3: muse-spark-1.3-contributor (agent: phase1-reviewer, role: ffi-engineer); reviewer 4: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); reviewer 5: muse-spark-1.3-contributor (agent: phase1-reviewer, role: security-auditor); reviewer 6: gpt-6-astra (agent: phase2-reviewer, role: general); reviewer 7: gpt-6-astra (agent: phase2-reviewer, role: architecture-layering); reviewer 8: gpt-6-astra (agent: phase2-reviewer, role: ffi-engineer); reviewer 9: gpt-6-astra (agent: phase2-reviewer, role: rust-quality); reviewer 10: gpt-6-astra (agent: phase2-reviewer, role: security-auditor); reviewer 11: gpt-6-astra (agent: phase2-reviewer, role: general); reviewer 12: gpt-6-astra (agent: phase2-reviewer, role: architecture-layering); reviewer 13: gpt-6-astra (agent: phase2-reviewer, role: ffi-engineer); reviewer 14: gpt-6-astra (agent: phase2-reviewer, role: rust-quality); reviewer 15: gpt-6-astra (agent: phase2-reviewer, role: security-auditor); final verifier: gpt-6-astra (agent: astra-verifier, role: final-verifier)
- Triage:
normalbygpt-6-astra(effort low) — The diff adds opt-in chain-ID validation, SDK state-update guards, and retry/failover handling across several paths, requiring ordinary logic review but not changing cryptographic verification or another critical surface with sufficient size or intricacy to qualify as critical. - Phase 1 reviewers:
muse-spark-1.3-contributor— general (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— architecture-layering (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— ffi-engineer (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— rust-quality (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— security-auditor (completed, effort xhigh); agentphase1-reviewer - Phase 1 model:
muse-spark-1.3-contributor— not quota-gated; passed overgemini-3.8-flash-high(antigravity below 15% reserve: weekly 1% left, 5h 100% left),glm-5.3-flash(not used above high effort; tier asks max) - Fresh final gate: an independent Phase-2 review ran after iterative findings were reconciled
- Fresh verifier:
gpt-6-astra— final-verifier; agentastra-verifier - Phase 2 reviewers:
gpt-6-astra— general (completed, effort high); agentphase2-reviewer,gpt-6-astra— architecture-layering (completed, effort high); agentphase2-reviewer,gpt-6-astra— ffi-engineer (completed, effort high); agentphase2-reviewer,gpt-6-astra— rust-quality (completed, effort high); agentphase2-reviewer,gpt-6-astra— security-auditor (completed, effort high); agentphase2-reviewer,gpt-6-astra— general (completed, effort high); agentphase2-reviewer,gpt-6-astra— architecture-layering (completed, effort high); agentphase2-reviewer,gpt-6-astra— ffi-engineer (completed, effort high); agentphase2-reviewer,gpt-6-astra— rust-quality (completed, effort high); agentphase2-reviewer,gpt-6-astra— security-auditor (completed, effort high); agentphase2-reviewer
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify the current code and confirm that no unresolved issues remain.
No unresolved findings remain from the prior review on this head.
Out-of-scope follow-up suggestions (1)
These are valid observations, but they are outside this PR's scope and should be handled in separate issues or author/maintainer-requested PRs rather than blocking this review.
- Expose expected-chain configuration through language bindings — Duplicate adjacent-feature observation: adding configuration to WASM, JavaScript, and native bindings is explicitly outside this PR's scope. The absence of these additions does not regress existing callers or prevent Rust consumers from using the implemented metadata gate.
- Follow-up: Consider creating a separate issue or author/maintainer-requested PR for this.
|
Bots are done — your move: post |
|
/self-reviewed 🤖 Posted autonomously by Claude on behalf of pasta. |
|
Closing this. On review, it does not defend against any attack, and the one operational case it covers is not worth a breaking change to Why it adds no security:
What's left is a case that only happens on testnet or devnet. After a Platform reset where Core keeps running, a node still serving the old chain can return a validly signed proof. If the context provider still has the old quorum and the time check is off, that proof advances the in-memory height high-water mark, and then responses from the new chain look stale until the process restarts. Setting The branch 🤖 Posted autonomously by Claude on behalf of pasta. |
The shell compared each verified response's signed chain_id with a configured Config.tenderdash_chain_id. That check adds nothing: chain_id is part of the message the Platform quorum signs, so a relabelled id fails verification, and the signing quorum must be one the embedder pushed from its own Core chain, so a response from another network does not verify at all. The stale-node case after a testnet reset is covered by the SDK's signed-time window and the 288-block ChainLock lag floor. This matches the reasoning that closed #4963. Removed from the bridge: Config.tenderdash_chain_id, Meta.chain_id and StatusKind::ChainIdMismatch; UnsupportedProtocolVersion and Internal are renumbered to 6 and 7. Client::tenderdash_chain_id() and the empty-id refusal in Client::new are gone. accept() now runs the height watermark, then records the verified protocol version, then raises the unsupported-version signal. The replay suite replaces the two foreign-chain-id cases with one that relabels the chain id after signing (Rejected, no shell state moved) and one that accepts a proof any pushed Platform quorum signed for another chain id. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The shell compared each verified response's signed chain_id with a configured Config.tenderdash_chain_id. That check adds nothing: chain_id is part of the message the Platform quorum signs, so a relabelled id fails verification, and the signing quorum must be one the embedder pushed from its own Core chain, so a response from another network does not verify at all. The stale-node case after a testnet reset is covered by the SDK's signed-time window and the 288-block ChainLock lag floor. This matches the reasoning that closed #4963. Removed from the bridge: Config.tenderdash_chain_id, Meta.chain_id and StatusKind::ChainIdMismatch; UnsupportedProtocolVersion and Internal are renumbered to 6 and 7. Client::tenderdash_chain_id() and the empty-id refusal in Client::new are gone. accept() now runs the height watermark, then records the verified protocol version, then raises the unsupported-version signal. The replay suite replaces the two foreign-chain-id cases with one that relabels the chain id after signing (Rejected, no shell state moved) and one that accepts a proof any pushed Platform quorum signed for another chain id. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Issue being fixed or feature implemented
rs-sdk never looks at
metadata.chain_id. The chain id is covered by the quorum signature, but the quorums that sign it can be the same on two Platform chains, for example before and after a testnet reset. So a node still serving the old chain can return a validly signed proof that the SDK accepts. That response then advances the SDK's height high-water mark and can ratchet the protocol version, and every later response from the real chain looks stale.The context provider never sees the chain id, so an embedder cannot do this check before the SDK's own state changes. It has to live in rs-sdk.
This came out of the Dash Core work in dashpay/dash#7512, where dash-qt embeds dash-sdk through the
dash-platform-cxxcrate and currently compares the chain id itself after the fact. The check is just as useful to every other SDK consumer (mobile, wasm, CLI tools) that knows which chain it means to talk to.What was done?
SdkBuilder::with_expected_chain_id(chain_id). If it is not called, nothing changes. An empty string is rejected bybuild()withError::Config.verify_response_metadatacomparesmetadata.chain_idfirst, before the time and height checks and before the protocol-version ratchet. A mismatch returns the newStaleNodeError::ChainIdMismatch { expected, received }, and no SDK state moves. Both callers (the proof path and the broadcast wait path) reach it only after the signature has been verified.can_retry()is false for this variant, andsync::retryfails over to another node using the short flat exclusion (2 s) it already uses for DPNS rejections, instead of the exponential health-ban ladder. Without that, a misconfigured SDK would ban every node and then return a bareNoAvailableAddresses; with it, every call keeps returningChainIdMismatch. The debug log on that path now says "failover stopped" instead of "DPNS failover stopped", since it is no longer DPNS-only.expect_fetchresponses report the expected chain id, so mock SDKs built withwith_expected_chain_idkeep working.How Has This Been Tested?
New tests:
sdk.rschain_id_mismatch_is_rejected_before_any_state_advances: the error carries both ids,can_retry()is false, and the protocol version and height high-water mark are unchanged.sdk.rsmatching_chain_id_is_accepted,chain_id_is_not_checked_when_not_configured.sdk.rschain_id_is_checked_before_time: time tolerance set, both chain id and time wrong, result isChainIdMismatch.sdk.rsempty_expected_chain_id_is_rejected.sync.rstest_retry_chain_id_mismatch_does_not_ban: two addresses both answer with a mismatch andretryis called twice. Both calls returnChainIdMismatchdirectly (the first reaches 2 nodes, the second 1 while the first is still excluded), and no address is banned pastnow + 2s.tests/fetch/mock_fetch.rstest_mock_fetch_identity_with_expected_chain_id.Runs:
cargo test -p dash-sdk: lib 218 passed; fetch suite 111 passed / 39 ignored; all other suites pass, 0 failures.cargo clippy -p dash-sdk --all-targets --features mocks -- -D warnings,cargo fmt --all -- --checkcargo check -p dash-sdk --no-default-features,cargo check -p rs-sdk-ffi,cargo check -p wasm-sdk --target wasm32-unknown-unknown,cargo doc -p dash-sdk --no-depsBreaking Changes
No behaviour changes unless
with_expected_chain_idis called.StaleNodeErroris not#[non_exhaustive], so the newChainIdMismatchvariant breaks downstream code that matches onStaleNodeErrorexhaustively without a wildcard arm. Nothing in this repository does (wasm-sdk matchesError::StaleNode(e)as a whole).StaleNodeError::Epochwas added the same way in #4231 without a!, so I have not marked the title; happy to add!if reviewers prefer.Unlike the other
StaleNodeErrorvariants,ChainIdMismatchis not retryable, so wasm-sdk reports it withretriable = false.Checklist:
structure.rs, regeneratedgrovedb-structure.json, and checked the structure viewer link posted on this pull requestFor repository code-owners and collaborators only
🤖 Generated with Claude Code
PR Hygiene ·
1f0cb59/self-reviewedrust-sdk(packages/rs-sdk/src/error.rs,packages/rs-sdk/src/mock/sdk.rs,packages/rs-sdk/src/platform/transition/broadcast.rsand 3 more) — lklimek or shumkovWhen every box is checked the
PR Hygienecheck passes and this can merge.Summary by CodeRabbit