Repository navigation
feat(dash): decode asset locks with their legacy payload framing - #1167
Conversation
`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>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Merge Risk: ⚪ Minimal · up to
|
|
Waiting for bot review — coderabbitai not yet. Wait for the missing reviews, or a writer can post |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## dev #1167 +/- ##
==========================================
+ Coverage 78.76% 79.01% +0.25%
==========================================
Files 302 303 +1
Lines 80644 81293 +649
==========================================
+ Hits 63518 64236 +718
+ Misses 17126 17057 -69
|
|
Bots are done — your move: post |
…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>
`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>
…new name rust-dashcore renamed `Transaction::deserialize_ignoring_asset_lock_payload_length` to `Transaction::deserialize_with_legacy_asset_lock_framing` and moved it into its own module (dashpay/rust-dashcore#1167), so that it reads as what it is: a legacy decoder Platform keeps calling to agree with its older binaries. The rust-dashcore pin moves to edc4d430 on integration/v5.0-core-v24, which is a5ae6375 plus that commit cherry-picked. Instant asset lock proofs and the differential test call the new name. Consensus is unchanged: the function body is byte-for-byte the one the previous pin had. The differential test against the byte-level decoder, the test_raw_instant_lock_proof_* tests and the drive-abci IdentityCreate test with a 3- and a 5-byte payload length prefix pass. No red/green step applies: nothing behaves differently. 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>
…new name rust-dashcore renamed `Transaction::deserialize_ignoring_asset_lock_payload_length` to `Transaction::deserialize_with_legacy_asset_lock_framing` and moved it into its own module (dashpay/rust-dashcore#1167), so that it reads as what it is: a legacy decoder Platform keeps calling to agree with its older binaries. The rust-dashcore pin moves to edc4d430 on integration/v5.0-core-v24, which is a5ae6375 plus that commit cherry-picked. Instant asset lock proofs and the differential test call the new name. Consensus is unchanged: the function body is byte-for-byte the one the previous pin had. The differential test against the byte-level decoder, the test_raw_instant_lock_proof_* tests and the drive-abci IdentityCreate test with a 3- and a 5-byte payload length prefix pass. No red/green step applies: nothing behaves differently. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds
Transaction::deserialize_with_legacy_asset_lock_framing, a frozen legacy decoder in its own module (dash/src/blockdata/transaction/legacy_asset_lock.rs) that reads an asset lock's payload after its length prefix whatever length the prefix announces. Dash Platform needs it to keep decoding instant asset lock proofs the way every Platform release did.Why Platform needs it
An instant asset lock proof carries a Core asset lock transaction as raw bytes the sender chooses. Before #1154, this crate read the asset lock payload length prefix and ignored its value. Since #1154 it reads every special payload strictly through its prefix, as Core does. Neither the InstantSend lock (checked against
txid()of the re-encoded transaction) nor the Platform transition's signature covers the prefix, so anyone relaying a pending funding transition can change it. Platform releases built on earlier pins accept and charge for such a proof; a strict decoder refuses it. Mixed node binaries would disagree on a paid transition.Platform v5.0.0-rc.1 restored the old reading with a byte-level workaround on its side (dashpay/platform#5403: zero the type bytes of a copy, decode as classic up to the payload, read and ignore the prefix, decode the payload). This PR moves that decoding into rust-dashcore as a named function, so Platform can drop the workaround.
The function
i16, type 8): inputs, the BIP144 form when there are no inputs (as for a classic transaction), outputs, lock time, then a CompactSize that must be minimal but whose value is ignored, then the payload (version byte, credit outputs), which must end the bytes.deserialize.It is standalone rather than a mode of
Decodable for Transaction, and it writes the payload layout out instead of callingAssetLockPayload::consensus_decode. That way 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, so its output, errors included, is identical to the Platform workaround's.It lives in its own private module, an
impl Transactionblock inlegacy_asset_lock.rs, sotransaction/mod.rskeeps only themodline. The name and the doc comments say plainly that it is a legacy decoder and that its framing must never change, and the doc tells callers to preferdeserialize.It is deliberately neither
#[deprecated]nor behind a cargo feature. 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.Tests
dash/tests/legacy_asset_lock.rs(46 tests) pins absolute results: fixed bytes, txids and error variants, built from a testnet asset lock Core relayed (txid81a1d138…5dcc5581).deserialize): a mainnet coinbase, a testnet ProRegTx, an unknown type, type 8 below version 3.deserializeat payload version 1).Each of these changes to the function turns tests red:
deserializeanyinstead ofallwitnesses emptyA differential run against Platform's workaround, over 16M seeded mutations on
devand on7302d0a6, found no Ok/Err, decoded-transaction or error-string mismatch.cargo test -p dashcore --all-features,cargo clippy -p dashcore --all-features --all-targets -- -D warningsandRUSTDOCFLAGS=-D warnings cargo doc -p dashcore --all-features --no-depspass.Platform pin
Platform v5.0 pins
integration/v5.0-core-v24(7302d0a6+ Core v24 fixes). The commits here are cherry-picked there too (a5ae6375,edc4d430), with two adaptations: the import conflict with #1157, andhex::decodein the test helper, sincehex-conservativeis not a dependency at that rev. All 46 tests pass there unchanged.🤖 Generated with Claude Code
PR Hygiene ·
28c1a04/self-revieweddash/src/blockdata/transaction/mod.rs,dash/tests/legacy_asset_lock.rs) — QuantumExplorer or ZocoLini or xdustinfaceWhen every merge requirement is met, the
PR Hygienecheck passes. Reviewer limits do not block merging; other required GitHub checks and protections still apply.Summary by CodeRabbit