Skip to content

fix(platform)!: keep unknown Core payload layouts decodable - #5403

Merged
shumkov merged 1 commit into
v5.0-devfrom
core24-v50-raw-payload
Oct 13, 2026
Merged

shumkov merged 1 commit into
v5.0-devfrom
core24-v50-raw-payload

Conversation

@shumkov

@shumkov shumkov commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

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 targets v5.0-dev directly.

What was done?

rust-dashcore: integration/v5.0-core-v24 @ 7302d0a6

7302d0a6 is a signed cherry-pick -x of dev's squash commit 2232580f (#1154) on top of 6c5775cf. It was pushed as a fast-forward, so 6c5775cf stays an ancestor.

Checks on 7302d0a6:

  • cargo test -p dashcore --all-features: 740 passed, 0 failed (+9 from feat(drive): graceful shutdown #1154).
  • feat: replace Drive ABCI validation with DPP #1158's 11 V24 devnet vectors: 11/11. Every transaction in each block decodes typed.
  • 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.
  • CI by 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-dashcore source lines change in Cargo.lock.

  • dpp: legacy framing for instant proof transactions. TryFrom<RawInstantLockProof> decodes an asset lock (transaction version 3 or later as int16, type 8) the way every node always has:

    • the length prefix is read as a minimal varint and its value is ignored;
    • AssetLockPayload is read and must end the bytes.

    Every other transaction decodes as before.

  • drive-abci: raw coinbase fallback. credit_pool_balance_from_coinbase reads a RawPayloadType(5, ..) coinbase payload with the same partial CoinbasePayload decode 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 (and 40268cc0), an asset lock's payload length prefix is read and its value ignored. The proof transaction must end exactly where AssetLockPayload ends.

  • Nothing covers the prefix as sent. The InstantSend lock signs the txid. The txid and the transition's signable bytes are both computed over re-encoded bytes, which carry the correct prefix.
  • So the old pin accepts any minimal prefix, and pays for it. A proof with a wrong prefix is a paid success that funds the same identity. Anyone who sees a pending IdentityCreate or TopUp can re-wrap it this way, and the owner's signature stays valid.
  • At 7302d0a6 alone, 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:

  1. It zeroes the type field and decodes everything up to the payload as a classic transaction. The segwit rule is the same for type 0 and type 8, and a classic transaction has no payload section.
  2. It reads the prefix with the same minimal-VarInt decoder.
  3. It reads the payload with the same AssetLockPayload decoder, 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:

  • 180 033 proof transactions, mutated from Core's asset lock, a 0-input segwit layout and a two-output lock. The old pin accepts 54 080 of them. With the shim, 0 differ, in txid or in bytes. Without the shim, 28 431 differ.
  • A review probe built from git archive of both revisions: 3 298 cases covering short inputs, versions up to 0xffff, nine prefixes (non-minimal, huge, truncated), segwit flags 0, 1, 2 and 0xff, and payloads just under and over 4 MB.
    • The 222 cases on the shim's path show 0 differences, in Debug output, txid, re-encoding or error.
    • The 132 cases that differ are all version ≥ 3 with a type other than 8, which are unpaid on both pins.

No protocol-version gate. The shim keeps the old acceptance at every protocol version.

  • A gate would still need this decoder for older versions (replay and mixed pins).
  • The decode runs inside serde, with no platform version.
  • All a gate would buy is canonical proof bytes. A re-wrapped transition has the same signable bytes and replay tag, so only one of the two executes.

If canonical framing is ever wanted, it gets its own protocol version.

The credit pool fallback

At 6c5775cf, deserialize_partial read a later payload with the version 4 layout and ignored the rest. At 7302d0a6 alone, that payload is RawPayloadType(5, ..), and the function returns Err. From PV14, block execution would then fail on every bumped node, and wait_for_core_to_sync would refuse to start.

The fallback reads the raw bytes with the same partial decoder:

  • The bytes are the same. The payload is the coinbase's last field, and getspecialtxes returns the coinbase alone, framed by Core.
  • Probe. 20 000 Core-framed coinbases built from block 15146's, with payload versions 0-15, 0x7fff and 0xffff, extended or truncated.
    • The fallback reads the old pin's 13 575 balances exactly, and errors where the old pin errors.
    • Without the fallback, 7 838 of those balances fail.
    • An extended run of 60 000 more cases showed 0 differences.
  • The only remaining difference is a prefix that disagrees with the payload length (old Ok, new Err), which Core's own serialization can't produce.
  • The balance is in the app hash through record_core_credit_pool_blocks, and it is unchanged.

In-place changes to shipped generations

No shipped vN module is edited. Two unversioned or shipped decoders change, so that their outputs stay identical to 6c5775cf on every paid or successful outcome:

  • RawInstantLockProof deserialization: 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 in wait_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.

  • Chain proofs (getrawtransaction): the old and the new result are both "unexpected".
  • rs-dapi: its broadcast pre-decode now refuses a wrong prefix, as Core does.
  • rs-sdk, and the proof builders in rs-sdk-ffi and wasm-dpp: they re-encode canonically.
  • The wallet (SPV, masternode records, asset-lock recovery): a raw payload counts as "not a ProTx" or "not an asset lock".

Persistence

Compatible, with no migration.

  • RawPayloadType keeps index 10 and the fields (u16, Vec<u8>). A blob written at 6c5775cf decodes at 7302d0a6 and re-encodes byte-identical, and the wallet-storage guard persisted_core_transactions passes.
  • Stored proofs are re-encoded canonically when written, and decode through the shim exactly as before.
  • No Platform store writes a dashcore transaction by variant name.

Forward merge into v5.1-dev

v5.1-dev pins 40268cc0 and #5307 pins d036d844; neither has #1154. When v5.0-dev merges forward:

  • keep v5.1's pin;
  • keep the shim. It compiles on those pins and matches their decoder; the dpp tests below also pass at 6c5775cf with the shim applied.
  • drop the RawPayloadType arm in credit_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 at 7302d0a6 without the two changes, and passes with them. At 7302d0a6 without the changes, these five were the only failures in the dpp asset_lock and drive-abci rpc::core / credit_pool / asset_lock runs (dpp 177 of 179 passed, drive-abci 193 of 196).

Test 6c5775cf 7302d0a6 alone 7302d0a6 + this PR
dpp test_raw_instant_lock_proof_reads_an_asset_lock_payload_whatever_its_length_prefix (new): prefixes 23, 25, 00, fc and fd0001 decode to the canonical transaction, txid 81a1d138… and identity HY8jYYsT… pass fail pass
dpp 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 transaction pass fail pass
drive-abci should_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 one pass fail pass
drive-abci should_read_the_balance_of_a_coinbase_with_fields_after_version_4 (exists, from #5392) pass fail pass
drive-abci should_read_the_balance_of_a_coinbase_of_a_later_version (new): a version 5 payload with a field after version 4's pass fail pass

The 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 7302d0a6 with this PR):

  • cargo fmt --all --check, and cargo clippy --workspace --all-targets --locked -- -D warnings: clean.
  • dpp lib: 5646 passed, 0 failed, 6 ignored.
  • drive-abci lib: 4229 passed, 0 failed, 17 ignored.
  • drive-abci strategy tests, withdrawal, vote-extension round and credit-pool backfill (RUST_MIN_STACK=4194304): 15 passed, 0 failed.
  • rs-dapi lib: 334 passed, 0 failed.
  • dash-sdk core::: 3 passed, 0 failed.
  • platform-wallet lib: 1187 passed, 0 failed.
  • platform-wallet-storage: 978 passed, 0 failed, 3 ignored.

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::UnknownPayloadType is renamed RawPayloadType;
  • a known type can now come back as RawPayloadType when 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:

  • 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 · 07be276

  • Bots — coderabbitai skipped after its own rate limit · thepastaclaw ✓
  • Self-review — posted; again after any push
  • Reviewer requests paused — your 5 review slots are occupied; this PR is excluded from reviewers' queues. Required approvals still count without a slot.
  • Build green
  • Approvals
    • files with no dedicated owner — you own it
    • dpp — you own it
    • rs-drive-abci — you own it
    • wallet-storage (packages/rs-platform-wallet-storage/tests/persisted_core_transactions.rs) — lklimek

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.

@github-actions github-actions Bot added this to the v5.0.0 milestone Oct 13, 2026
@coderabbitai

coderabbitai Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You'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.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository: dashpay/platform/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 84196224-ca74-407b-91cd-580dcf90bb15
📥 Commits

Reviewing files that changed from the base of the PR and between 9ddd432 and 8097784.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (5)
  • Cargo.toml
  • packages/rs-dpp/src/identity/state_transition/asset_lock_proof/instant/instant_asset_lock_proof.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/identity_create/mod.rs
  • packages/rs-drive-abci/src/rpc/core.rs
  • packages/rs-platform-wallet-storage/tests/integration/persisted_core_transactions.rs
  • Autofix · 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 13, 2026 •

Copy link
Copy Markdown
Collaborator

🕓 Review not started yet because the new head is waiting for the 30-minute push debounce.

  • Request normal review — click when the PR is ready for review.
  • Request priority review — click to move this review to the front of the queue.

Commit 8097784. Normal review starts when eligible; priority review starts as soon as a slot is available.

@shumkov
shumkov marked this pull request as ready for review October 13, 2026 11:37
@github-actions

Copy link
Copy Markdown
Contributor

Waiting for bot review — coderabbitai skipped after its own rate limit · 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 waiting-bots Waiting for the review bots to report on this head bot-review-skipped A required review bot did not report; it was skipped by the window or by a person. labels Oct 13, 2026
@shumkov shumkov changed the title fix(platform)!: keep unknown Core payload layouts decodable (rust-dashcore #1154) fix(platform)!: keep unknown Core payload layouts decodable Oct 13, 2026
@shumkov

shumkov commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

/self-reviewed

@shumkov
shumkov force-pushed the core24-v50-raw-payload branch from f38bad0 to 07be276 Compare October 13, 2026 11:43
@github-actions

Copy link
Copy Markdown
Contributor

Waiting for bot review — coderabbitai skipped after its own rate limit · 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.

@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 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: normal by gpt-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); agent phase1-reviewer, muse-spark-1.3-contributor — rust-quality (completed, effort xhigh); agent phase1-reviewer
  • Phase 1 model: muse-spark-1.3-contributor — not quota-gated; passed over gemini-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; agent sol-verifier
  • Phase 2 reviewers: gpt-6.1-sol — general (completed, effort high); agent phase2-reviewer, gpt-6.1-sol — architecture-layering (completed, effort high); agent phase2-reviewer, gpt-6.1-sol — platform-versioning (completed, effort high); agent phase2-reviewer, gpt-6.1-sol — rust-quality (completed, effort high); agent phase2-reviewer, gpt-6.1-sol — security-auditor (completed, effort high); agent phase2-reviewer

@github-actions

Copy link
Copy Markdown
Contributor

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.
Full checklist in the description.

@github-actions github-actions Bot added too-many-open-prs Beyond the author's 5 open PRs; waits for one to merge before a human is asked. and removed waiting-bots Waiting for the review bots to report on this head labels Oct 13, 2026
…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>
@shumkov
shumkov force-pushed the core24-v50-raw-payload branch from 07be276 to 8097784 Compare October 13, 2026 14:34
@shumkov
shumkov merged commit 7da81ae into v5.0-dev Oct 13, 2026
14 of 15 checks passed
@shumkov
shumkov deleted the core24-v50-raw-payload branch October 13, 2026 15:08
shumkov added a commit to dashpay/rust-dashcore that referenced this pull request Oct 14, 2026
`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)
shumkov added a commit that referenced this pull request Oct 14, 2026
…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>
shumkov added a commit that referenced this pull request Oct 14, 2026
…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>
shumkov added a commit to dashpay/rust-dashcore that referenced this pull request Oct 14, 2026
…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>
shumkov added a commit that referenced this pull request Oct 14, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bot-review-skipped A required review bot did not report; it was skipped by the window or by a person. too-many-open-prs Beyond the author's 5 open PRs; waits for one to merge before a human is asked.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants