Repository navigation
fix(platform)!: keep unknown Core payload layouts decodable - #5403
Conversation
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 25 minutes. View limit details
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 |
|
🕓 Review not started yet because the new head is waiting for the 30-minute push debounce.
Commit 8097784. Normal review starts when eligible; priority review starts as soon as a slot is available. |
|
Waiting for bot review — coderabbitai skipped after its own rate limit · thepastaclaw not yet. Wait for the missing reviews, or a writer can post |
|
/self-reviewed |
f38bad0 to
07be276
Compare
|
Waiting for bot review — coderabbitai skipped after its own rate limit · thepastaclaw not yet. Wait for the missing reviews, or a writer can post |
thepastaclaw
left a comment
There was a problem hiding this comment.
Final validation — Phase 1 + Phase 2
Static verification of the complete diff at 07be276, including the exact upstream dependency comparison, found no actionable in-scope defects. The asset-lock shim preserves legacy decoding semantics, and the raw coinbase fallback preserves balance extraction for Core-framed payloads with appended fields; regression coverage addresses framing, execution equivalence, and persisted transactions. Exact-head CI confirms Rust workspace tests, JS builds, and Kotlin checks passed; dedicated Rust wallet and Docker jobs were skipped, PR Hygiene remains pending, and the PR lists S5b-1 validation as outstanding.
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: 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:
normalbygpt-6.1-sol(effort low) — The production diff is a small, contained asset-lock decoding shim in decode_asset_lock_transaction and a coinbase fallback in credit_pool_balance_from_coinbase, so despite touching consensus-sensitive deserialization it does not meet the large-or-intricate bar for critical. - Phase 1 reviewers:
muse-spark-1.3-contributor— general (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— rust-quality (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 14% left, 5h 100% left),glm-5.3-flash(not used above high effort; tier asks max) - Fresh verifier:
gpt-6.1-sol— final-verifier; agentsol-verifier - Phase 2 reviewers:
gpt-6.1-sol— general (completed, effort high); agentphase2-reviewer,gpt-6.1-sol— architecture-layering (completed, effort high); agentphase2-reviewer,gpt-6.1-sol— platform-versioning (completed, effort high); agentphase2-reviewer,gpt-6.1-sol— rust-quality (completed, effort high); agentphase2-reviewer,gpt-6.1-sol— security-auditor (completed, effort high); agentphase2-reviewer
|
Missing human approval. Automatic reviewer requests are paused and this PR is excluded from reviewers' queues. Merge, close, or draft another active PR by this author to free a review slot. Required human review can be satisfied without a slot; once all merge requirements are met, PR Hygiene passes. |
…hcore #1154) Pin rust-dashcore to integration/v5.0-core-v24 at 7302d0a6, which adds length prefix and kept as RawPayloadType when its typed decoder does not read exactly those bytes. A block carrying a later layout of a known type (a CbTx v5, a ProTx v4) still decodes, instead of misaligning every later transaction as V24 did before #5392. identical to 6c5775cf, at every protocol version, with no gate: - dpp: TryFrom<RawInstantLockProof> decodes an asset lock (version >= 3, type 8) with the legacy framing. The payload length prefix is read and its value ignored, and AssetLockPayload must end the bytes. Neither the InstantSend lock nor the transition's signature covers the prefix, so the old pin accepts, and pays for, a proof whose prefix was changed by whoever relays it. c7 alone would refuse it (10002, unpaid) and split mixed pins. A two-pin decoder probe found 0 differences over 180 033 proof transactions (28 431 without the shim). - drive-abci: credit_pool_balance_from_coinbase reads a RawPayloadType(5) coinbase payload with the same partial CoinbasePayload decode. A later coinbase layout still yields the balance, instead of failing block execution from PV14 and startup. 0 differences over 20 000 coinbases (7 838 balances lost without it). The wallet-storage guard follows the UnknownPayloadType -> RawPayloadType rename. The enum index is still 10, so persisted blobs are unchanged. Red to green (6c5775cf / c7 alone / c7 + this change): ✔ ✖ ✔ dpp test_raw_instant_lock_proof_reads_an_asset_lock_payload_whatever_its_length_prefix ✔ ✖ ✔ dpp test_raw_instant_lock_proof_refuses_an_asset_lock_with_bytes_its_payload_does_not_read ✔ ✖ ✔ drive-abci should_create_the_same_identity_from_a_proof_with_another_payload_length_prefix ✔ ✖ ✔ drive-abci should_read_the_balance_of_a_coinbase_with_fields_after_version_4 ✔ ✖ ✔ drive-abci should_read_the_balance_of_a_coinbase_of_a_later_version Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
07be276 to
8097784
Compare
`Transaction::deserialize_ignoring_asset_lock_payload_length` decodes an asset lock as this crate did before special payloads were read strictly through their length prefix (#1154): the prefix must be a minimal CompactSize, its value is ignored, and the payload must end the bytes. Any other transaction goes to `deserialize`. Dash Platform needs it for instant asset lock proofs. Neither the InstantSend lock (checked against the re-encoded txid) nor the transition signature covers the prefix, and Platform releases built on earlier pins accepted and charged for proofs whose prefix differs from the payload length. Platform v5.0.0-rc.1 restored that with a byte-level workaround on its side (dashpay/platform#5403); this gives it a named, frozen decoder. The function is standalone rather than a mode of `Decodable for Transaction`, and writes the payload layout out instead of calling `AssetLockPayload::consensus_decode`, so a later transaction rule or a new asset lock payload layout cannot change what it accepts. It makes the same finite-reader calls as the transaction decoder. `dash/tests/legacy_asset_lock.rs` pins absolute results (fixed bytes, txids, error variants): 1/3/5/9-byte prefixes, versions 4 and 0x7fff, the witness form (including a witness on one of two inputs) and the zero-input form, a long payload read after a short prefix, a payload without credit outputs, payload versions 0/2/3/255 with appended fields counted by the prefix or not, and the refusals (non-minimal prefix or credit-output count, truncation, trailing bytes, segwit flags 0 and 2, a classic transaction). A coinbase, a ProRegTx, an unknown type and type 8 below version 3 are checked against `deserialize`, and a seeded round-trip property covers mutated inputs. Each of these changes to the function turns tests red: delegating everything to `deserialize` (22 of 46), dropping the segwit rule (12), checking `any` instead of `all` witnesses empty (2), accepting only version 3 (2), comparing the version unsigned (1), refusing empty credit outputs (2), dropping the trailing-bytes check (6). A differential run against Platform's workaround over 16M seeded mutations, on dev and on 7302d0a, found no Ok/Err, decoded-transaction or error mismatch. Adapted for this branch: the `AssetLockPayload` import no longer sits next to the `AssetUnlockPayload` import from #1157, which this branch does not have, and the tests decode hex with `hex`, since `hex-conservative` is not a dependency here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit 28c1a04)
…legacy asset lock decoder An instant asset lock proof's transaction is decoded from sender-chosen bytes, and an asset lock's payload has always been read whatever length its length prefix announces: neither the InstantSend lock nor the transition signature covers the prefix. #5403 kept that working with `decode_asset_lock_transaction`, a byte-level workaround on top of rust-dashcore's strict decoder. rust-dashcore now provides the same decoding as `Transaction::deserialize_ignoring_asset_lock_payload_length` (dashpay/rust-dashcore#1167). The rust-dashcore pin moves to a5ae6375 on integration/v5.0-core-v24, which is 379f0574 plus that commit cherry-picked, so the workaround is deleted and the proof decodes through it. Consensus is unchanged at every protocol version: the new function gives the same Ok/Err, decoded transaction and error string as the workaround for every input. A new differential test checks this against a test-only copy of the workaround over 100,000 seeded mutations of a real asset lock, its witness form and its zero-input form. A differential run in rust-dashcore covered 16M more. The #5403 tests pass unchanged, and now also cover 5- and 9-byte prefixes. The drive-abci IdentityCreate test also replaces the 1-byte prefix with a 3- and a 5-byte one, and checks the same identity, fee and balance. No red/green step applies: the change keeps the output identical. The differential test goes red when the new call is replaced with plain `deserialize`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…legacy asset lock decoder An instant asset lock proof's transaction is decoded from sender-chosen bytes, and an asset lock's payload has always been read whatever length its length prefix announces: neither the InstantSend lock nor the transition signature covers the prefix. #5403 kept that working with `decode_asset_lock_transaction`, a byte-level workaround on top of rust-dashcore's strict decoder. rust-dashcore now provides the same decoding as `Transaction::deserialize_ignoring_asset_lock_payload_length` (dashpay/rust-dashcore#1167). The rust-dashcore pin moves to a5ae6375 on integration/v5.0-core-v24, which is 379f0574 plus that commit cherry-picked, so the workaround is deleted and the proof decodes through it. Consensus is unchanged at every protocol version: the new function gives the same Ok/Err, decoded transaction and error string as the workaround for every input. A new differential test checks this against a test-only copy of the workaround over 100,000 seeded mutations of a real asset lock, its witness form and its zero-input form. A differential run in rust-dashcore covered 16M more. The #5403 tests pass unchanged, and now also cover 5- and 9-byte prefixes. The drive-abci IdentityCreate test also replaces the 1-byte prefix with a 3- and a 5-byte one, and checks the same identity, fee and balance. No red/green step applies: the change keeps the output identical. The differential test goes red when the new call is replaced with plain `deserialize`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…1167) * feat(dash): decode asset locks ignoring their payload length prefix `Transaction::deserialize_ignoring_asset_lock_payload_length` decodes an asset lock as this crate did before special payloads were read strictly through their length prefix (#1154): the prefix must be a minimal CompactSize, its value is ignored, and the payload must end the bytes. Any other transaction goes to `deserialize`. Dash Platform needs it for instant asset lock proofs. Neither the InstantSend lock (checked against the re-encoded txid) nor the transition signature covers the prefix, and Platform releases built on earlier pins accepted and charged for proofs whose prefix differs from the payload length. Platform v5.0.0-rc.1 restored that with a byte-level workaround on its side (dashpay/platform#5403); this gives it a named, frozen decoder. The function is standalone rather than a mode of `Decodable for Transaction`, and writes the payload layout out instead of calling `AssetLockPayload::consensus_decode`, so a later transaction rule or a new asset lock payload layout cannot change what it accepts. It makes the same finite-reader calls as the transaction decoder. `dash/tests/legacy_asset_lock.rs` pins absolute results (fixed bytes, txids, error variants): 1/3/5/9-byte prefixes, versions 4 and 0x7fff, the witness form (including a witness on one of two inputs) and the zero-input form, a long payload read after a short prefix, a payload without credit outputs, payload versions 0/2/3/255 with appended fields counted by the prefix or not, and the refusals (non-minimal prefix or credit-output count, truncation, trailing bytes, segwit flags 0 and 2, a classic transaction). A coinbase, a ProRegTx, an unknown type and type 8 below version 3 are checked against `deserialize`, and a seeded round-trip property covers mutated inputs. Each of these changes to the function turns tests red: delegating everything to `deserialize` (22 of 46), dropping the segwit rule (12), checking `any` instead of `all` witnesses empty (2), accepting only version 3 (2), comparing the version unsigned (1), refusing empty credit outputs (2), dropping the trailing-bytes check (6). A differential run against Platform's workaround over 16M seeded mutations, on dev and on 7302d0a, found no Ok/Err, decoded-transaction or error mismatch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(dash): move the legacy asset lock decoder into its own module `Transaction::deserialize_ignoring_asset_lock_payload_length` becomes `Transaction::deserialize_with_legacy_asset_lock_framing` and moves to `dash/src/blockdata/transaction/legacy_asset_lock.rs`. The transaction module keeps only the `mod` line. The name and the doc comments now say plainly that this is a legacy decoder whose framing must never change. It is neither `#[deprecated]` nor behind a cargo feature: Dash Platform has to call it for as long as it must agree with its older binaries and replay its chain, so there is nothing to migrate to, and a feature would only allow building Platform without it. The function body is byte-for-byte the one it replaces. The 46 tests in `dash/tests/legacy_asset_lock.rs` pass with only the name changed. No red/green step applies: nothing behaves differently. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…legacy asset lock decoder An instant asset lock proof's transaction is decoded from sender-chosen bytes, and an asset lock's payload has always been read whatever length its length prefix announces: neither the InstantSend lock nor the transition signature covers the prefix. #5403 kept that working with `decode_asset_lock_transaction`, a byte-level workaround on top of rust-dashcore's strict decoder. rust-dashcore now provides the same decoding as `Transaction::deserialize_ignoring_asset_lock_payload_length` (dashpay/rust-dashcore#1167). The rust-dashcore pin moves to a5ae6375 on integration/v5.0-core-v24, which is 379f0574 plus that commit cherry-picked, so the workaround is deleted and the proof decodes through it. Consensus is unchanged at every protocol version: the new function gives the same Ok/Err, decoded transaction and error string as the workaround for every input. A new differential test checks this against a test-only copy of the workaround over 100,000 seeded mutations of a real asset lock, its witness form and its zero-input form. A differential run in rust-dashcore covered 16M more. The #5403 tests pass unchanged, and now also cover 5- and 9-byte prefixes. The drive-abci IdentityCreate test also replaces the 1-byte prefix with a 3- and a 5-byte one, and checks the same identity, fee and balance. No red/green step applies: the change keeps the output identical. The differential test goes red when the new call is replaced with plain `deserialize`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Issue being fixed or feature implemented
At rust-dashcore
6c5775cf, which #5392 pinned, a special payload is read field by field after its length prefix, and the prefix itself is ignored. If Core changes the layout of a known type again (a longer CbTx v5, a ProTx v4), every later transaction in that block is misaligned. That is exactly how V24 broke rs-dapi's streams, rs-sdk and the wallet's SPV before #5392.rust-dashcore #1154 fixes this. It reads every payload through its length prefix, and keeps any payload it can't type exactly as
RawPayloadType(type, bytes), so the block still decodes. Every real V24 payload still decodes typed (rust-dashcore #1158's 11 devnet vectors), so v5.0 gains nothing for V24 itself. What it gains is protection against Core's next layout change.In drive-abci, #1154 changes two consensus outcomes. So this PR ships two Platform changes in the same commit as the re-pin, and both keep every paid or successful outcome identical to
6c5775cf.#5392 has merged (
7f7ed875), so this PR targetsv5.0-devdirectly.What was done?
rust-dashcore:
integration/v5.0-core-v24@7302d0a67302d0a6is a signedcherry-pick -xofdev's squash commit2232580f(#1154) on top of6c5775cf. It was pushed as a fast-forward, so6c5775cfstays an ancestor.special_transaction/mod.rs. The resolution takes feat(drive): graceful shutdown #1154's side without the types 10-12 lines from fix(sdk): missing and invalid types #1156, which isn't on this branch. The resolved file differs fromdev's only by fix(sdk): missing and invalid types #1156's lines.36981d54ee20).RawPayloadTypestays at enum index 10, whereUnknownPayloadTypewas.Checks on
7302d0a6:cargo test -p dashcore --all-features: 740 passed, 0 failed (+9 from feat(drive): graceful shutdown #1154).cargo test -p key-wallet -p key-wallet-ffi -p key-wallet-manager --all-features: 1063 passed, 0 failed.cargo clippy --workspace --all-targets --all-features -- -D warnings: clean.workflow_dispatch: green. Ubuntu / spv failed on its first attempt with a sync timeout (test_runtime_add_during_initial_sync: 37 000 of 40 000 blocks in 180 s) and passed on rerun. The same job passed on macOS, Windows and Ubuntu ARM.Platform
Re-pin. The eight rust-dashcore crates move to
7302d0a6. Only the 12 rust-dashcoresourcelines change inCargo.lock.dpp: legacy framing for instant proof transactions.
TryFrom<RawInstantLockProof>decodes an asset lock (transaction version 3 or later asint16, type 8) the way every node always has:AssetLockPayloadis read and must end the bytes.Every other transaction decodes as before.
drive-abci: raw coinbase fallback.
credit_pool_balance_from_coinbasereads aRawPayloadType(5, ..)coinbase payload with the same partialCoinbasePayloaddecode as before. The doc comment now says why that reads the same bytes.The rename
UnknownPayloadType→RawPayloadType, in the wallet-storage guard test. No production code names the variant.Why the shim
At
6c5775cf(and40268cc0), an asset lock's payload length prefix is read and its value ignored. The proof transaction must end exactly whereAssetLockPayloadends.7302d0a6alone, the same proof is 10002, unpaid. In a rolling upgrade, bumped nodes would reject a proposal that carries it, or compute another app hash if it committed. Replaying such a transition would diverge.The shim runs the old decoder's steps on the same bytes:
VarIntdecoder.AssetLockPayloaddecoder, which feat(drive): graceful shutdown #1154 doesn't touch, and requires it to end the bytes.So it produces the same transaction, txid, outputs and identity, and the same inputs fail. Any other header can't produce a typed asset lock on either pin: basic structure refuses it unpaid on both, and only the error code may differ (10002 against 10509).
Two-pin decoder probes, each one program compiled against both rust-dashcore revisions:
git archiveof both revisions: 3 298 cases covering short inputs, versions up to0xffff, nine prefixes (non-minimal, huge, truncated), segwit flags 0, 1, 2 and0xff, and payloads just under and over 4 MB.Debugoutput, txid, re-encoding or error.No protocol-version gate. The shim keeps the old acceptance at every protocol version.
If canonical framing is ever wanted, it gets its own protocol version.
The credit pool fallback
At
6c5775cf,deserialize_partialread a later payload with the version 4 layout and ignored the rest. At7302d0a6alone, that payload isRawPayloadType(5, ..), and the function returnsErr. From PV14, block execution would then fail on every bumped node, andwait_for_core_to_syncwould refuse to start.The fallback reads the raw bytes with the same partial decoder:
getspecialtxesreturns the coinbase alone, framed by Core.0x7fffand0xffff, extended or truncated.record_core_credit_pool_blocks, and it is unchanged.In-place changes to shipped generations
No shipped
vNmodule is edited. Two unversioned or shipped decoders change, so that their outputs stay identical to6c5775cfon every paid or successful outcome:RawInstantLockProofdeserialization: unversioned, at every protocol version, for every funding transition (IdentityCreate, IdentityTopUp, AddressFunding, Shield). It keeps the old acceptance through the shim; the argument and probes are under "Why the shim".credit_pool_balance_from_coinbase: PV14 block execution, and startup inwait_for_core_to_sync. It reads the same balance as before for every coinbase Core produces; see "The credit pool fallback".Every other site that decodes Core bytes only gets more lenient: a later layout of a known type stays raw instead of failing the block.
getrawtransaction): the old and the new result are both "unexpected".Persistence
Compatible, with no migration.
RawPayloadTypekeeps index 10 and the fields(u16, Vec<u8>). A blob written at6c5775cfdecodes at7302d0a6and re-encodes byte-identical, and the wallet-storage guardpersisted_core_transactionspasses.Forward merge into v5.1-dev
v5.1-devpins40268cc0and #5307 pinsd036d844; neither has #1154. Whenv5.0-devmerges forward:6c5775cfwith the shim applied.RawPayloadTypearm incredit_pool_balance_from_coinbase, which won't compile there.The arm comes back in the PR that pins #1154 on v5.1, which needs the shim too.
How Has This Been Tested?
Red to green. Each test passes at
6c5775cf, fails at7302d0a6without the two changes, and passes with them. At7302d0a6without the changes, these five were the only failures in the dppasset_lockand drive-abcirpc::core/credit_pool/asset_lockruns (dpp 177 of 179 passed, drive-abci 193 of 196).6c5775cf7302d0a6alone7302d0a6+ this PRtest_raw_instant_lock_proof_reads_an_asset_lock_payload_whatever_its_length_prefix(new): prefixes23,25,00,fcandfd0001decode to the canonical transaction, txid81a1d138…and identityHY8jYYsT…test_raw_instant_lock_proof_refuses_an_asset_lock_with_bytes_its_payload_does_not_read(new): refuses a non-minimal prefix, a byte after the payload, the same byte counted by the prefix, and an asset-lock payload after a classic transactionshould_create_the_same_identity_from_a_proof_with_another_payload_length_prefix(new): an IdentityCreate whose proof prefix is shifted by ±1 inside the serialized transition executes, with the same fee and balance as the unshifted oneshould_read_the_balance_of_a_coinbase_with_fields_after_version_4(exists, from #5392)should_read_the_balance_of_a_coinbase_of_a_later_version(new): a version 5 payload with a field after version 4'sThe classic-transaction row guards the shim's type-8 check. With that check removed, the test fails on this row.
Still green: #5392's guards. These are the dpp golden asset lock, the below-version-3 refusal, the version 3 and 4 coinbase balances, and wallet-storage
persisted_core_transactions.Suites (local, macOS arm64, at
7302d0a6with this PR):cargo fmt --all --check, andcargo clippy --workspace --all-targets --locked -- -D warnings: clean.RUST_MIN_STACK=4194304): 15 passed, 0 failed.core::: 3 passed, 0 failed.Still to run: S5b-1 on Jameson with images built at
7302d0a6, because client decode paths change. S5c (mixed pins) is skipped: the two-pin probes and the tests above run the same bytes through both pins.Breaking Changes
dpp and dash-sdk re-export
dashcore, so downstream Rust code sees two changes:TransactionPayload::UnknownPayloadTypeis renamedRawPayloadType;RawPayloadTypewhen its typed decoder doesn't consume exactly the bytes its length prefix frames, or doesn't re-encode to them.Wire, state and persisted formats are unchanged.
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 ·
07be276dpp— you own itrs-drive-abci— you own itwallet-storage(packages/rs-platform-wallet-storage/tests/persisted_core_transactions.rs) — lklimekWhen every merge requirement is met, the
PR Hygienecheck passes. Reviewer limits do not block merging; other required GitHub checks and protections still apply.