Skip to content

fix(drive-abci): sign the asset unlock serialization hash instead of the txid - #5315

Merged
shumkov merged 1 commit into
v5.0-devfrom
shumkov/withdrawal-vote-extension-hash
Oct 8, 2026
Merged

shumkov merged 1 commit into
v5.0-devfrom
shumkov/withdrawal-vote-extension-hash

Conversation

@shumkov

@shumkov shumkov commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

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 quorumSig against SerializeHash of the transaction with only quorumSig zeroed (src/evo/assetlocktx.cpp), which covers requestedHeight and quorumHash. For version 2 unlocks (dashpay/dash#7639) the txid zeroes those fields too, and dashpay/rust-dashcore#1047 tracks teaching Transaction::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_extension now uses sha256d::Hash::hash(&serialize(tx)) instead of tx.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 calls tx_to_extend_vote_extension, so the two cannot drift apart.

In-place changes to shipped generations

  • tx_to_extend_vote_extension is not versioned, so every protocol version (1 through latest) uses it.
  • Why consensus cannot change: every protocol version builds asset unlocks with payload version 1 (document_try_into_asset_unlock_base_transaction_info v0 and v1). For those, Transaction::txid() at the pinned rust-dashcore and consensus::serialize hash 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_unlocks asserts 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 equals 7df5cfea…. 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 new requestedHeight or quorumHash changes the extension.
  • should_keep_signing_the_txid_of_version_1_unlocks: for version 1 unlocks built the way a proposal builds them, the extension equals txid().

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 warnings
  • cargo fmt -p drive-abci -- --check
  • RUST_MIN_STACK=4194304 cargo test -p drive-abci --test strategy_tests -- withdrawal_tests vote_extension_round_tests (13 passed). These drive mimic with real withdrawals, including expiry and re-signing. CI's workspace test filter skips them, so they were run locally.

Breaking Changes

None.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have added "!" to the title and described breaking changes in the corresponding section if my code contains any
  • I have made corresponding changes to the documentation if needed
  • If I added or changed GroveDB structure, I described it in the area's structure.rs, regenerated grovedb-structure.json, and checked the structure viewer link posted on this pull request

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

🤖 Generated with Claude Code

PR Hygiene · 1c50185

  • Bots — coderabbitai ✓ · thepastaclaw ✓
  • Self-review — posted; again after any push
  • Build green
  • Approvals — you own every area touched; none needed

When every merge requirement is met, the PR Hygiene check passes. Reviewer limits do not block merging; other required GitHub checks and protections still apply.

…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>
@github-actions github-actions Bot added this to the v5.0.0 milestone Oct 7, 2026
@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Waiting for bot review — coderabbitai ✓ · thepastaclaw not yet. Wait for the missing reviews, or a writer can post /skip-bots to proceed without them; blocking findings still need addressing.
Full checklist in the description.

@github-actions github-actions Bot added the waiting-bots Waiting for the review bots to report on this head label Oct 7, 2026
@coderabbitai

coderabbitai Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: dashpay/platform/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: ca302fa9-fc9e-49bc-97e4-81089e569cd2
📥 Commits

Reviewing files that changed from the base of the PR and between 7b61d6f and 1c50185.

📒 Files selected for processing (2)
  • packages/rs-drive-abci/src/mimic/mod.rs
  • packages/rs-drive-abci/src/platform_types/withdrawal/unsigned_withdrawal_txs/v0/mod.rs

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

Unsigned 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.

Changes

Withdrawal Vote Extensions

Layer / File(s) Summary
Hash serialized withdrawal transactions
packages/rs-drive-abci/src/platform_types/withdrawal/unsigned_withdrawal_txs/v0/mod.rs
tx_to_extend_vote_extension now hashes the full serialized transaction. Tests check that version 2 signing fields affect the hash and that version 1 extensions remain txid-equivalent.
Use converted extensions in mimic execution
packages/rs-drive-abci/src/mimic/mod.rs
Mimic execution now copies the conversion helper’s returned extension type, bytes, and sign request ID into each VoteExtension.

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix

Suggested reviewers: quantumexplorer

Merge Risk: ⚪ Minimal · up to 1c501

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)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 8 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: signing the serialized asset unlock hash instead of the transaction ID.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@thepastaclaw

thepastaclaw commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

✅ Final review complete — no blockers (commit 1c50185) · triage: low

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: low by gpt-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); agent phase1-reviewer, glm-5.3-flash — rust-quality (completed, effort high); agent phase1-reviewer
  • Phase 1 model: glm-5.3-flash — zai quota: 5h 100% left, weekly 96% left; passed over gemini-3.8-flash-high (antigravity below 15% reserve: weekly 15% left, 5h 100% left)
  • Fresh verifier: gpt-6.1-sol — final-verifier; agent sol-verifier
  • Phase 2 reviewers: gpt-6.1-sol — general (completed, effort medium); agent phase2-reviewer, gpt-6.1-sol — architecture-layering (completed, effort medium); agent phase2-reviewer, gpt-6.1-sol — platform-versioning (completed, effort medium); agent phase2-reviewer, gpt-6.1-sol — rust-quality (completed, effort medium); agent phase2-reviewer, gpt-6.1-sol — security-auditor (completed, effort medium); agent phase2-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.

@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

Bots are done — your move: post /self-reviewed.
Full checklist in the description.

@github-actions github-actions Bot added waiting-self-review Waiting for the author to post /self-reviewed and removed waiting-bots Waiting for the review bots to report on this head labels Oct 7, 2026
@shumkov

shumkov commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator Author

/self-reviewed

@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Policy satisfied — this can merge.
Full checklist in the description.

@github-actions github-actions Bot removed the waiting-self-review Waiting for the author to post /self-reviewed label Oct 8, 2026
@shumkov
shumkov merged commit 683dfc5 into v5.0-dev Oct 8, 2026
31 of 32 checks passed
@shumkov
shumkov deleted the shumkov/withdrawal-vote-extension-hash branch October 8, 2026 10:18
@QuantumExplorer QuantumExplorer mentioned this pull request Oct 13, 2026
8 of 26 tasks
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