Repository navigation
fix(drive-abci): sign the asset unlock serialization hash instead of the txid - #5315
Conversation
…the txid Core verifies an asset unlock's quorumSig against the hash of its full serialization with only quorumSig zeroed, which covers requestedHeight and quorumHash. Platform used the txid as the withdrawal vote extension, which is that hash for payload version 1. Version 2 asset unlocks (Core v24, dashpay/dash#7639) compute the txid with requestedHeight, quorumHash and quorumSig zeroed, so once rust-dashcore's txid() follows that rule (dashpay/rust-dashcore#1047), signing it would make every version 2 withdrawal signature fail verification in Core (#4908, item 3). Hash the serialization explicitly. For every version 1 asset unlock, the only payload version any protocol version builds, the bytes are identical to the txid, so no protocol version changes behaviour. The test Tenderdash simulator now reuses tx_to_extend_vote_extension instead of its own copy of the txid-based extension. The tests guard the future rust-dashcore change and cannot go red against the current pin, where txid() and the serialization hash coincide. A temporary mutation emulating the version 2 txid rule failed both version 2 tests while the version 1 equivalence test passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Waiting for bot review — coderabbitai ✓ · thepastaclaw not yet. Wait for the missing reviews, or a writer can post |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
📒 Files selected for processing (2)
Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughUnsigned withdrawal transactions now use a double-SHA-256 hash of their full serialization as the vote extension. Mimic execution uses the converted extension type, bytes, and sign request ID. ChangesWithdrawal Vote Extensions
Priority: ⬇️ Low Estimated code review effort: 2 (Simple) | ~10 minutes Change: Bug fix Suggested reviewers: Merge Risk: ⚪ Minimal · up to No actionable issue is established from the available evidence. Complete the remaining strategy tests as part of normal merge validation. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 2📝 Generate docstrings 💡
🛠️ Fix failing CI checks 💡
🧪 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 1c50185) · triage: low |
thepastaclaw
left a comment
There was a problem hiding this comment.
Final validation — Phase 1 + Phase 2
Static verification at head 1c50185 found no actionable in-scope issues. The serialization hash preserves existing version 1 vote-extension bytes, and production withdrawal construction supplies zeroed quorum signatures; the version 2 test vector is present in the proposed DIP-0027 amendment. Current CI reports successful Rust workspace tests and Kotlin SDK build/tests, with PR Hygiene still pending; no local builds or tests were run.
Review provenance
Source: reviewer 1: glm-5.3-flash (agent: phase1-reviewer, role: general); reviewer 2: glm-5.3-flash (agent: phase1-reviewer, role: rust-quality); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 6: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 7: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)
- Triage:
lowbygpt-6.1-sol(effort low) — The diff makes a small, well-contained signing-message substitution in tx_to_extend_vote_extension, reuses it in the simulator, and adds focused regression tests rather than introducing large or intricate critical-surface changes. - Phase 1 reviewers:
glm-5.3-flash— general (completed, effort high); agentphase1-reviewer,glm-5.3-flash— rust-quality (completed, effort high); agentphase1-reviewer - Phase 1 model:
glm-5.3-flash— zai quota: 5h 100% left, weekly 96% left; passed overgemini-3.8-flash-high(antigravity below 15% reserve: weekly 15% left, 5h 100% left) - Fresh verifier:
gpt-6.1-sol— final-verifier; agentsol-verifier - Phase 2 reviewers:
gpt-6.1-sol— general (completed, effort medium); agentphase2-reviewer,gpt-6.1-sol— architecture-layering (completed, effort medium); agentphase2-reviewer,gpt-6.1-sol— platform-versioning (completed, effort medium); agentphase2-reviewer,gpt-6.1-sol— rust-quality (completed, effort medium); agentphase2-reviewer,gpt-6.1-sol— security-auditor (completed, effort medium); agentphase2-reviewer
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.
- make_extend_vote_request_id panics and unwraps on its input precondition — Out of scope and below the exceptional-follow-up threshold: these lines are unchanged, the asset-unlock payload invariant is satisfied by production construction, and consensus_encode writes to an in-memory Vec. The supplied observation identifies routine adjacent hardening rather than a concrete newly exposed failure.
- Follow-up: Consider creating a separate issue or author/maintainer-requested PR for this.
|
Bots are done — your move: post |
|
/self-reviewed |
|
Policy satisfied — this can merge. |
Basic explanation
What this does: When Platform pays out a withdrawal, the validator quorum signs a Core "asset unlock" transaction. Each validator's vote extension carries the 32-byte message the quorum signs. Platform used the transaction id (txid) for that message. This PR uses the hash of the whole serialized unsigned transaction instead, which is what Core actually checks the signature against.
Value: Core v24 adds version 2 asset unlocks, whose txid leaves out the signing fields so it stays the same across re-signs and the unlock can be InstantSend-locked. Once rust-dashcore computes txids that way, signing the txid would make every version 2 withdrawal signature fail in Core. This change removes that trap, so the rust-dashcore bump and the switch to version 2 can land in either order.
Risks: Low. For version 1 asset unlocks, the only kind any protocol version builds, the new message is byte for byte the old one, so votes, signatures and consensus are unchanged. A test pins that equality.
Issue being fixed or feature implemented
Part of #4908 (item 3). Core verifies
quorumSigagainstSerializeHashof the transaction with onlyquorumSigzeroed (src/evo/assetlocktx.cpp), which coversrequestedHeightandquorumHash. For version 2 unlocks (dashpay/dash#7639) the txid zeroes those fields too, and dashpay/rust-dashcore#1047 tracks teachingTransaction::txid()that rule. After that,tx.txid()is no longer the signed message for version 2.What was done?
platform_types/withdrawal/unsigned_withdrawal_txs/v0/mod.rs:tx_to_extend_vote_extensionnow usessha256d::Hash::hash(&serialize(tx))instead oftx.txid(). A comment at the edit says why the txid is not the message for version 2, and why the change is inert for version 1.mimic/mod.rs: the test Tenderdash simulator had its own copy of the txid-based extension. It now callstx_to_extend_vote_extension, so the two cannot drift apart.In-place changes to shipped generations
tx_to_extend_vote_extensionis not versioned, so every protocol version (1 through latest) uses it.document_try_into_asset_unlock_base_transaction_infov0 and v1). For those,Transaction::txid()at the pinned rust-dashcore andconsensus::serializehash the same fields in the same order: version, type, inputs, outputs, lock time, length-prefixed payload. SegWit encoding is forced off for asset unlocks, and the two hashes have the same byte order.should_keep_signing_the_txid_of_version_1_unlocksasserts the equality.How Has This Been Tested?
New tests in
unsigned_withdrawal_txs/v0/mod.rs:should_sign_the_full_serialization_of_a_version_2_unlock_not_its_txid: with the signing fields zeroed, the extension equals the DIP-0027 published version 2 txid (3c4db73c…), which pins serialization and byte order to Core's. For the real worked example (height 500, quorum hash one), the extension is not that txid and equals7df5cfea…. That value was cross-checked with an independent Python serializer that reproduces both DIP-0027 published txids.should_sign_a_different_message_when_a_version_2_unlock_is_re_signed: a newrequestedHeightorquorumHashchanges the extension.should_keep_signing_the_txid_of_version_1_unlocks: for version 1 unlocks built the way a proposal builds them, the extension equalstxid().The new tests cannot go red against the current code: at the current rust-dashcore pin,
txid()and the serialization hash coincide. They guard the coming rust-dashcore change instead. To check they are not empty, I temporarily mutated the extension to follow the version 2 txid rule. Both version 2 tests failed (the mutated extension became exactly the DIP-0027 txid) and the version 1 test passed.Run locally:
cargo test -p drive-abci --lib -- unsigned_withdrawal_txs extend_vote verify_vote_extension(23 passed)cargo clippy -p drive-abci --lib --tests -- -D warningscargo fmt -p drive-abci -- --checkRUST_MIN_STACK=4194304 cargo test -p drive-abci --test strategy_tests -- withdrawal_tests vote_extension_round_tests(13 passed). These drivemimicwith real withdrawals, including expiry and re-signing. CI's workspace test filter skips them, so they were run locally.Breaking Changes
None.
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 ·
1c50185When every merge requirement is met, the
PR Hygienecheck passes. Reviewer limits do not block merging; other required GitHub checks and protections still apply.