Skip to content

Emit version 2 Asset Unlock transactions once Core v24 activates (stable txid + InstantSend-locked withdrawals) #4908

Closed

Description

@PastaPastaPasta

Summary

dashpay/dash#7639 (DIP-0027 amendment, spec dashpay/dips#189) is merged into Core develop and ships with the v24 hard fork. Once DEPLOYMENT_V24 is active, Core accepts version 2 Asset Unlock transactions: same serialization as v1, but the txid is computed with the quorum signing fields zeroed, so every re-signed instance of one withdrawal has one stable txid, and Core InstantSend-locks the unlock as soon as it is minable. That is what makes Platform→Core withdrawals respendable immediately.

Core keeps accepting v1 unlocks after activation, so v24 activating on testnet does not break withdrawals as Platform emits them today (rs-dpp/src/withdrawal/document_try_into_asset_unlock_base_transaction_info/v1/mod.rs:71-78 hard-codes version: 1). But none of the v2 behaviour is exercised until Platform emits v2. dash#7639 lists this as a known follow-up ("Platform-side emitter PR: payload version byte + deterministic v24 gate on core_chain_locked_height"); this issue tracks it so it is scheduled before v24 activates on testnet.

What Core expects from a v2 unlock

  • Payload version byte 2; every other payload byte identical to v1 (index, fee, requestedHeight, quorumHash, quorumSig).
  • Before v24 is active Core rejects v2 with bad-assetunlocktx-version-2 (src/evo/assetlocktx.cpp:207), so the version must be chosen deterministically from Core fork state, not from a node-local flag.
  • txid = SHA256d(serialized tx with the trailing 132 bytes zeroed): requestedHeight (4) + quorumHash (32) + quorumSig (96), i.e. ASSET_UNLOCK_QUORUM_INFO_SIZE in src/primitives/transaction.h:47; see ComputeStableUnlockHash in src/primitives/transaction.cpp:76-88. The full-serialization hash is the "instance hash" (CTransaction::GetInstanceHash), committed to by the coinbase (CbTx v4 merkleRootAssetUnlocks), relayed as inv type MSG_ASSET_UNLOCK (34) to peers ≥ 70242.
  • The message the quorum signs is unchanged: SerializeHash(tx with quorumSig zeroed), which still covers requestedHeight and quorumHash (src/evo/assetlocktx.cpp:220-228). That equals today's Transaction::txid() of the unsigned tx, but it will not equal txid() for a v2 unlock once rust-dashcore implements the v2 rule (item 2). See item 3.
  • One claimant per withdrawal index in the Core mempool: a second instance under a different txid is rejected as assetunlock-stale-instance unless its requestedHeight is higher. A re-sign of a v2 instance keeps the txid, so it is an in-place refresh.
  • Core signs the IS lock only when the unlock is minable in the next block, which includes "the mempool's pending withdrawal total fits the credit pool's currentLimit". Platform pooling above Core's limit therefore delays locks; that is the coupling feat(consensus): relative net asset-unlock limit for v24 and getcreditpoolinfo dash#7712 and the closed feat(drive)!: withdrawal limit mirrors Core's credit pool and counts withdrawals in flight #4906 are about.

What needs to change

  1. Version selection: emit version: 2 once v24 is active at the Core height the block commits to. The gate must be deterministic across validators, so derive it from the block header's core_chain_locked_height (e.g. getdeploymentinfo <hash of that height> → deployments.v24.active, or an equivalent already-queried fork-state source), never from each node's local Core tip. dashcore-rpc may need getdeploymentinfo support; there is no get_deployment_info in rust-dashcore today.
  2. rust-dashcore Transaction::txid() must implement the v2 rule (tracked separately on rust-dashcore); otherwise anything in Platform that derives an outpoint or txid from the built transaction (append_signatures_and_broadcast_withdrawal_transactions/v0/mod.rs:85 logs it; SDK/wallet code derives withdrawal output outpoints) disagrees with Core.
  3. Sign the full-serialization hash, not txid() (must land together with the rust-dashcore bump). The vote extension the quorum signs is built from tx.txid() (rs-drive-abci/src/platform_types/withdrawal/unsigned_withdrawal_txs/v0/mod.rs:164, tx_to_extend_vote_extension). After rust-dashcore#1047, txid() of a v2 unlock zeroes requestedHeight/quorumHash, so it no longer equals Core's message hash and every v2 withdrawal signature fails Core verification. Switch the extension to the full-serialization (instance) hash; for v1 the two are identical, so this is a no-op before v2 is emitted. (The order matters: emitting v2 on the current pin would still sign correctly, but wallets would compute wrong txids; bumping the pin without this change breaks signing.)
  4. Re-sign path: an expired v2 instance re-signed with a new requestedHeight/quorum keeps the same txid. Make sure nothing in the withdrawal document lifecycle assumes the re-signed transaction has a new id.
  5. Never rewrite a signed withdrawal's untied bytes. The v2 txid is stable only while outputs, fee, index and payload version stay fixed across re-signs. Today this holds (rebroadcast_expired_withdrawal_documents v2 only resets transactionSignHeight and moves the same untied transaction back to the queue), but nothing enforces it. A future migration that recomputes fees or output scripts for queued/expired withdrawals would silently invalidate every child spend. Make it a stated invariant with a test. Untied transactions built as v1 before the gate flips stay v1 when re-signed; that is valid.
  6. Optional — Mined status and IS lock. update_broadcasted_withdrawal_statuses only acts on Chainlocked; a Mined-but-not-yet-chainlocked unlock past core_expiration_blocks (48) is marked EXPIRED and re-signed (harmless — Core rejects already-mined indexes — but wasted work). Optionally surface getassetunlockstatuses' new instantlock field so SDKs can show withdrawals as locked before COMPLETE (needs the optional field in dashcore-rpc's AssetUnlockStatusResult).
  7. Tests: regtest activates v24 at genesis and devnets after their start time, so a dashmate local network on Core develop can exercise this end to end: getassetunlockstatuses reports instantlock: true for pooled indexes, getrawtransaction shows instanceHash, and a forced expiry + re-sign yields the same txid. dash#7639's feature_asset_locks.py (test_asset_locks_v2_*) shows the expected sequences.

Related

Activity

  1. added this to the v4.2.0 milestone on Sep 22, 2026
  2. modified the milestones: v5.0.0, v5.1.0 on Oct 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions