From 5f04c128d0a4e6de604d5b26db686d7b625b2a93 Mon Sep 17 00:00:00 2001 From: pasta Date: Mon, 24 Aug 2026 19:02:44 +0200 Subject: [PATCH 1/4] docs(dip27): add version 2 Asset Unlock transactions with stable withdrawal identifier Version 2 Asset Unlock transactions are serialized identically to version 1 but introduce a withdrawal transaction identifier: the transaction hashed with signHeight, quorumHash, and quorumSig set to zeros. Outputs are referenced by this identifier, so transactions spending an unmined withdrawal remain valid when Platform re-signs it, and such spends become eligible for InstantSend. Also fixes existing errata (asset lock/unlock wording, duplicated word, TOC omission, table styles) and bumps the DIP-2 registry payload version. --- dip-0002/special-transactions.md | 6 +-- dip-0027.md | 45 ++++++++++++++++----- dip-0027/dip-0027-withdrawal-txid-calc.py | 49 +++++++++++++++++++++++ project-words.txt | 1 + 4 files changed, 88 insertions(+), 13 deletions(-) create mode 100644 dip-0027/dip-0027-withdrawal-txid-calc.py diff --git a/dip-0002/special-transactions.md b/dip-0002/special-transactions.md index f1b8a743..ae534c42 100644 --- a/dip-0002/special-transactions.md +++ b/dip-0002/special-transactions.md @@ -4,8 +4,8 @@ The transaction type is described based on proposed DIPs. Here is a table of current proposed types and their associated DIP. Future DIPs may introduce more types. -*Note:* This table refers to the _payload_ version which relates only to the special transaction -payload and is distinct from the _transaction_ version. +*Note:* This table refers to the *payload* version which relates only to the special transaction +payload and is distinct from the *transaction* version. | Type | Transaction Type | DIP Number and Name | Payload Version | State | | ---- | ---------------- | ------------------- | --------------- | ----- | @@ -17,4 +17,4 @@ payload and is distinct from the _transaction_ version. | 6 | Quorum Commitment | [DIP 006: Long Living Masternode Quorums](https://github.com/dashpay/dips/blob/master/dip-0006.md) | 1 | Active | | 7 | Masternode Hard Fork Signal | [DIP 023: Enhanced Hard Fork Mechanism](https://github.com/dashpay/dips/blob/master/dip-0023.md) | 1 | Active | | 8 | Asset Lock | [DIP 027: Dash Core Credit Pool](https://github.com/dashpay/dips/blob/master/dip-0027.md) | 1 | Active | -| 9 | Asset Unlock | [DIP 027: Dash Core Credit Pool](https://github.com/dashpay/dips/blob/master/dip-0027.md) | 1 | Active | +| 9 | Asset Unlock | [DIP 027: Dash Core Credit Pool](https://github.com/dashpay/dips/blob/master/dip-0027.md) | 2 | Active | diff --git a/dip-0027.md b/dip-0027.md index 8a575f11..44735757 100644 --- a/dip-0027.md +++ b/dip-0027.md @@ -23,6 +23,8 @@ * [Proof of Finality](#proof-of-finality) * [Asset Unlocking](#asset-unlocking) * [Asset Unlock Transaction](#asset-unlock-transaction) + * [Signing Asset Unlock Transactions](#signing-asset-unlock-transactions) + * [Withdrawal Transaction Identifier](#withdrawal-transaction-identifier) * [Client verification](#client-verification) * [Withdrawal Completion](#withdrawal-completion) * [Withdrawal Safety](#withdrawal-safety) @@ -76,7 +78,7 @@ The format of the Asset Lock special transaction payload: | - | - | - | - | | version | uint8_t | 1 | Asset Lock transaction version number | | count | uint8_t | 1 | The number of accounts funded via this Asset Lock | -| credit_outputs | vec\ | 28 * count| The hash of the public key(s) used to claim credits on Dash Platform, as well as the amount of Dash in duffs transferred into credits for this public key hash | +| credit_outputs | vec\ | 28 * count | The hash of the public key(s) used to claim credits on Dash Platform, as well as the amount of Dash in duffs transferred into credits for this public key hash | Each Asset Lock transaction must create an unspendable output by using an OP_RETURN as the first script. An Asset Lock transaction may only include a single OP_RETURN output. Additionally, the following criteria apply to the OP_RETURN output: @@ -104,7 +106,7 @@ The format of the Asset Unlock special transaction payload: | **Field** | **Type** | **Size** | **Description** | | - | - | - | - | -| version | uint8_t | 1 | Asset Unlock version number. Currently set to 1 | +| version | uint8_t | 1 | Asset Unlock version number. 1 or 2. Set to 2 once the v24 hard fork activates | | index | uint64 | 8 | The index of the transaction | | fee | uint32 | 4 | The miner's fee in duffs | | signHeight | uint32 | 4 | The height of the Core chain known by Platform at the moment of the Asset Unlock signing (Core ChainLock height) | @@ -113,29 +115,52 @@ The format of the Asset Unlock special transaction payload: #### Signing Asset Unlock Transactions -An active Platform validator quorum quorum must sign Asset Unlock transactions per the process described in [DIP-7](./dip-0007.md). The signing session parameters are: +An active Platform validator quorum must sign Asset Unlock transactions per the process described in [DIP-7](./dip-0007.md). The signing session parameters are: | DIP-7 parameter | Value | -|-|-| +| - | - | | Request ID | `SHA256(SHA256("plwdtx", index))` | -| Message hash | The asset lock transaction, with the `quorumSig` field set to zeros, hashed as described in [DIP-2](./dip-0002.md#serialization-hashing-and-signing) | +| Message hash | The Asset Unlock transaction, with the `quorumSig` field set to zeros, hashed as described in [DIP-2](./dip-0002.md#serialization-hashing-and-signing) | For reference, the following table shows some example index values and their request IDs. See the [provided script](dip-0027/dip-0027-request-id-calc.py) for example code: -| **Index** | **Request ID** | -|-----------|----------------| +| **Index** | **Request ID** | +|-----------|------------------------------------------------------------------| | 101 | fcc76a643c5c668244fdcef09833955d6f4b803fa6c459f7732983c2332389fd | | 123456789 | ebd9d75ad72184bec8e8f25a499eb5386ba564eb7ce70a4a3b9db652599d6d72 | +#### Withdrawal Transaction Identifier + +Version 2 Asset Unlock transactions are serialized identically to version 1 transactions. The version field instead signals a change in how the transaction's outputs are identified. + +When an Asset Unlock transaction is not mined before it expires, Platform re-signs the withdrawal and broadcasts a replacement transaction. The replacement differs from the original only in the `signHeight`, `quorumHash`, and `quorumSig` fields; the index, fee, and outputs are unchanged. Since the transaction hash covers these fields, each replacement has a new hash, and any transaction spending an output of an unmined withdrawal becomes invalid when the withdrawal is re-signed. + +To resolve this, each version 2 Asset Unlock transaction has a withdrawal transaction identifier: the transaction serialized with the `signHeight`, `quorumHash`, and `quorumSig` fields set to zeros, hashed as described in [DIP-2](./dip-0002.md#serialization-hashing-and-signing). Every re-signed instance of the same withdrawal shares one withdrawal transaction identifier because the identifier omits only the fields that change when Platform re-signs. Note that the identifier is distinct from the signing session's message hash, which sets only the `quorumSig` field to zeros and therefore still commits to `signHeight` and `quorumHash`. + +The outputs of a version 2 Asset Unlock transaction are identified by the withdrawal transaction identifier. Transactions spending these outputs must reference them by the withdrawal transaction identifier and output index, and nodes must track these outputs in the UTXO set under the withdrawal transaction identifier. A transaction spending an output of an unmined version 2 Asset Unlock transaction therefore remains valid regardless of which instance of the withdrawal is eventually mined. Only one instance of a withdrawal can ever be mined since the index must be unique. + +All block-level structures — the merkle tree, block serialization, and transaction relay — continue to use the standard transaction hash. + +When a node receives a version 2 Asset Unlock transaction whose withdrawal transaction identifier matches a transaction already in its mempool, it should keep only the instance with the higher `signHeight`, provided its signature is valid. + +For reference, the following table shows example withdrawal transaction identifiers. Both examples use a fee of 70000 duffs and a single output paying 100000000 duffs to the P2PKH script for the public key hash `1111111111111111111111111111111111111111`. See the [provided script](dip-0027/dip-0027-withdrawal-txid-calc.py) for example code: + +| **Index** | **Withdrawal Transaction Identifier** | +|-----------|------------------------------------------------------------------| +| 101 | 3c4db73c8356407a5d7c78df5045bd280f2dc4fd644b06c4bfbdead3d5ae41cf | +| 123456789 | a67e1107ae6e04b813bc8e81348266f5206d1ca93d305dc4323940e18cdbaf34 | + ### Client verification -All clients must verify the quorumSig field before accepting the transaction. Quorum signatures must be created by a recent quorum (one that is currently active or the most recently replaced one) to be considered valid. If the quorum is recent and the transaction is not yet mined, light clients should attempt to verify the quorumSig by retrieving the quorum public key as described in DIP-4 and verifying the signature. If the quorum is not recent and the transaction is not mined, the transaction should be ignored until it is mined to prevent attacks on the light client. +All clients must verify the quorumSig field before accepting the transaction. Quorum signatures must be created by a recent quorum (one that is currently active or the most recently replaced one) to be considered valid. If the quorum is recent and the transaction is not yet mined, light clients should attempt to verify the quorumSig by retrieving the quorum public key as described in DIP-4 and verifying the signature. If the quorum is not recent and the transaction is not mined, the transaction should be ignored until it is mined to prevent attacks on the light client. Light clients must track the outputs of version 2 Asset Unlock transactions by their withdrawal transaction identifier. ### Withdrawal Completion Since Asset Unlock transactions do not have inputs, they are not eligible for InstantSend. Once mined into a block and subsequently ChainLocked, they are final and identical to all other outputs. A withdrawal should be considered complete when the corresponding Asset Unlock transaction is finalized on the Core chain. -Asset Unlock transactions might not be mined for multiple reasons. For example, the quorum who signed a transaction expired, Core fees were too low, or the withdrawal limit was reached. To handle this situation, Asset Unlock transactions have an expiration period based on the quorum lifetime. Transactions are considered invalid if not signed by one of the active quorums or the most recently replaced quorum. In this case, the issuer can retry the withdrawal. To ensure that expired transactions are not included in the Core chain, Asset Lock transactions are refused once the block height exceeds _signHeight_ by 48 or more (i.e., height > _signHeight_ + 48). +The outputs of a version 1 Asset Unlock transaction must not be spent until the transaction is mined. The outputs of a version 2 Asset Unlock transaction may be spent before the transaction is mined, and these spends are eligible for InstantSend. Masternodes must only sign an InstantSend lock for such a spend after observing a validly signed instance of the withdrawal. At that point the withdrawal is irreversible on Platform, and Platform will re-sign the withdrawal until it is mined. Because InstantSend locks reference inputs by the withdrawal transaction identifier, these locks remain valid when the withdrawal is re-signed. Confirmation of such a spend depends on an instance of the withdrawal eventually being mined. + +Asset Unlock transactions might not be mined for multiple reasons. For example, the quorum who signed a transaction expired, Core fees were too low, or the withdrawal limit was reached. To handle this situation, Asset Unlock transactions have an expiration period based on the quorum lifetime. Transactions are considered invalid if not signed by one of the active quorums or the most recently replaced quorum. In this case, the issuer can retry the withdrawal. To ensure that expired transactions are not included in the Core chain, Asset Unlock transactions are refused once the block height exceeds _signHeight_ by 48 or more (i.e., height > _signHeight_ + 48). When a version 2 Asset Unlock transaction expires and is re-signed, the replacement shares the same withdrawal transaction identifier, so transactions spending its outputs remain valid. ## Withdrawal Safety @@ -159,7 +184,7 @@ Prior to activation of the `withdrawals` hard fork in Dash Core v22.0, more rest As described [in the latest version of DIP-4](./dip-0004.md#coinbase-special-transaction), coinbase transaction versions >= 3 include the creditPoolBalance field: | **Field** | **Type** | **Size** | **Description** | -| -| - | - | - | +| - | - | - | - | | creditPoolBalance | int64_t | 8 | The total amount of duffs locked at the block height | The creditPoolBalance field represents the total amount of Dash (in duffs) locked at the block height. The value of this field must be equal to the value of this field in the previous block PLUS the sum of all Dash locked by Asset Lock transactions included in the block MINUS the sum of all Dash unlocked by Asset Unlock transactions in the block. So, for height x, the creditPoolBalance is: diff --git a/dip-0027/dip-0027-withdrawal-txid-calc.py b/dip-0027/dip-0027-withdrawal-txid-calc.py new file mode 100644 index 00000000..4defc21d --- /dev/null +++ b/dip-0027/dip-0027-withdrawal-txid-calc.py @@ -0,0 +1,49 @@ +#!/usr/bin/python3 +# # Example showing how to create the withdrawal transaction identifier for a +# # version 2 Asset Unlock transaction. The identifier is the double-SHA256 of +# # the transaction serialized with the signHeight, quorumHash, and quorumSig +# # fields set to zeros. +import hashlib +import struct + +def sha256(s): + return hashlib.new('sha256', s).digest() + +def compact_size(n): + if n < 253: + return struct.pack("B", n) + if n < 0x10000: + return struct.pack(" Date: Mon, 24 Aug 2026 19:09:12 +0200 Subject: [PATCH 2/4] docs(dip27): review fixes - rename calc script, wording and style consistency --- dip-0027.md | 6 +++--- ...thdrawal-txid-calc.py => dip-0027-withdrawal-id-calc.py} | 0 2 files changed, 3 insertions(+), 3 deletions(-) rename dip-0027/{dip-0027-withdrawal-txid-calc.py => dip-0027-withdrawal-id-calc.py} (100%) diff --git a/dip-0027.md b/dip-0027.md index 44735757..2b48ff06 100644 --- a/dip-0027.md +++ b/dip-0027.md @@ -139,11 +139,11 @@ To resolve this, each version 2 Asset Unlock transaction has a withdrawal transa The outputs of a version 2 Asset Unlock transaction are identified by the withdrawal transaction identifier. Transactions spending these outputs must reference them by the withdrawal transaction identifier and output index, and nodes must track these outputs in the UTXO set under the withdrawal transaction identifier. A transaction spending an output of an unmined version 2 Asset Unlock transaction therefore remains valid regardless of which instance of the withdrawal is eventually mined. Only one instance of a withdrawal can ever be mined since the index must be unique. -All block-level structures — the merkle tree, block serialization, and transaction relay — continue to use the standard transaction hash. +The merkle tree and block serialization remain unchanged, and transaction relay continues to reference the standard transaction hash of each instance. When a node receives a version 2 Asset Unlock transaction whose withdrawal transaction identifier matches a transaction already in its mempool, it should keep only the instance with the higher `signHeight`, provided its signature is valid. -For reference, the following table shows example withdrawal transaction identifiers. Both examples use a fee of 70000 duffs and a single output paying 100000000 duffs to the P2PKH script for the public key hash `1111111111111111111111111111111111111111`. See the [provided script](dip-0027/dip-0027-withdrawal-txid-calc.py) for example code: +For reference, the following table shows example withdrawal transaction identifiers. Both examples use a fee of 70000 duffs and a single output paying 100000000 duffs to the P2PKH script for the public key hash `1111111111111111111111111111111111111111`. See the [provided script](dip-0027/dip-0027-withdrawal-id-calc.py) for example code: | **Index** | **Withdrawal Transaction Identifier** | |-----------|------------------------------------------------------------------| @@ -160,7 +160,7 @@ Since Asset Unlock transactions do not have inputs, they are not eligible for In The outputs of a version 1 Asset Unlock transaction must not be spent until the transaction is mined. The outputs of a version 2 Asset Unlock transaction may be spent before the transaction is mined, and these spends are eligible for InstantSend. Masternodes must only sign an InstantSend lock for such a spend after observing a validly signed instance of the withdrawal. At that point the withdrawal is irreversible on Platform, and Platform will re-sign the withdrawal until it is mined. Because InstantSend locks reference inputs by the withdrawal transaction identifier, these locks remain valid when the withdrawal is re-signed. Confirmation of such a spend depends on an instance of the withdrawal eventually being mined. -Asset Unlock transactions might not be mined for multiple reasons. For example, the quorum who signed a transaction expired, Core fees were too low, or the withdrawal limit was reached. To handle this situation, Asset Unlock transactions have an expiration period based on the quorum lifetime. Transactions are considered invalid if not signed by one of the active quorums or the most recently replaced quorum. In this case, the issuer can retry the withdrawal. To ensure that expired transactions are not included in the Core chain, Asset Unlock transactions are refused once the block height exceeds _signHeight_ by 48 or more (i.e., height > _signHeight_ + 48). When a version 2 Asset Unlock transaction expires and is re-signed, the replacement shares the same withdrawal transaction identifier, so transactions spending its outputs remain valid. +Asset Unlock transactions might not be mined for multiple reasons. For example, the quorum who signed a transaction expired, Core fees were too low, or the withdrawal limit was reached. To handle this situation, Asset Unlock transactions have an expiration period based on the quorum lifetime. Transactions are considered invalid if not signed by one of the active quorums or the most recently replaced quorum. In this case, the issuer can retry the withdrawal. To ensure that expired transactions are not included in the Core chain, Asset Unlock transactions are refused once the block height exceeds `signHeight` by 48 or more (i.e., height > `signHeight` + 48). When a version 2 Asset Unlock transaction expires and is re-signed, the replacement shares the same withdrawal transaction identifier, so transactions spending its outputs remain valid. ## Withdrawal Safety diff --git a/dip-0027/dip-0027-withdrawal-txid-calc.py b/dip-0027/dip-0027-withdrawal-id-calc.py similarity index 100% rename from dip-0027/dip-0027-withdrawal-txid-calc.py rename to dip-0027/dip-0027-withdrawal-id-calc.py From 6ab85782d421cecdfc79d1b939a391ee75a90822 Mon Sep 17 00:00:00 2001 From: pasta Date: Tue, 25 Aug 2026 09:31:04 +0200 Subject: [PATCH 3/4] docs(dip27): revert formatting-only changes unrelated to the amendment Restore original emphasis style and table formatting on untouched lines; keep only wording changes required by the v2 amendment. --- dip-0002/special-transactions.md | 4 ++-- dip-0027.md | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/dip-0002/special-transactions.md b/dip-0002/special-transactions.md index ae534c42..0ca61ff7 100644 --- a/dip-0002/special-transactions.md +++ b/dip-0002/special-transactions.md @@ -4,8 +4,8 @@ The transaction type is described based on proposed DIPs. Here is a table of current proposed types and their associated DIP. Future DIPs may introduce more types. -*Note:* This table refers to the *payload* version which relates only to the special transaction -payload and is distinct from the *transaction* version. +*Note:* This table refers to the _payload_ version which relates only to the special transaction +payload and is distinct from the _transaction_ version. | Type | Transaction Type | DIP Number and Name | Payload Version | State | | ---- | ---------------- | ------------------- | --------------- | ----- | diff --git a/dip-0027.md b/dip-0027.md index 2b48ff06..ce2d4ac7 100644 --- a/dip-0027.md +++ b/dip-0027.md @@ -78,7 +78,7 @@ The format of the Asset Lock special transaction payload: | - | - | - | - | | version | uint8_t | 1 | Asset Lock transaction version number | | count | uint8_t | 1 | The number of accounts funded via this Asset Lock | -| credit_outputs | vec\ | 28 * count | The hash of the public key(s) used to claim credits on Dash Platform, as well as the amount of Dash in duffs transferred into credits for this public key hash | +| credit_outputs | vec\ | 28 * count| The hash of the public key(s) used to claim credits on Dash Platform, as well as the amount of Dash in duffs transferred into credits for this public key hash | Each Asset Lock transaction must create an unspendable output by using an OP_RETURN as the first script. An Asset Lock transaction may only include a single OP_RETURN output. Additionally, the following criteria apply to the OP_RETURN output: @@ -115,17 +115,17 @@ The format of the Asset Unlock special transaction payload: #### Signing Asset Unlock Transactions -An active Platform validator quorum must sign Asset Unlock transactions per the process described in [DIP-7](./dip-0007.md). The signing session parameters are: +An active Platform validator quorum quorum must sign Asset Unlock transactions per the process described in [DIP-7](./dip-0007.md). The signing session parameters are: | DIP-7 parameter | Value | -| - | - | +|-|-| | Request ID | `SHA256(SHA256("plwdtx", index))` | | Message hash | The Asset Unlock transaction, with the `quorumSig` field set to zeros, hashed as described in [DIP-2](./dip-0002.md#serialization-hashing-and-signing) | For reference, the following table shows some example index values and their request IDs. See the [provided script](dip-0027/dip-0027-request-id-calc.py) for example code: -| **Index** | **Request ID** | -|-----------|------------------------------------------------------------------| +| **Index** | **Request ID** | +|-----------|----------------| | 101 | fcc76a643c5c668244fdcef09833955d6f4b803fa6c459f7732983c2332389fd | | 123456789 | ebd9d75ad72184bec8e8f25a499eb5386ba564eb7ce70a4a3b9db652599d6d72 | @@ -160,7 +160,7 @@ Since Asset Unlock transactions do not have inputs, they are not eligible for In The outputs of a version 1 Asset Unlock transaction must not be spent until the transaction is mined. The outputs of a version 2 Asset Unlock transaction may be spent before the transaction is mined, and these spends are eligible for InstantSend. Masternodes must only sign an InstantSend lock for such a spend after observing a validly signed instance of the withdrawal. At that point the withdrawal is irreversible on Platform, and Platform will re-sign the withdrawal until it is mined. Because InstantSend locks reference inputs by the withdrawal transaction identifier, these locks remain valid when the withdrawal is re-signed. Confirmation of such a spend depends on an instance of the withdrawal eventually being mined. -Asset Unlock transactions might not be mined for multiple reasons. For example, the quorum who signed a transaction expired, Core fees were too low, or the withdrawal limit was reached. To handle this situation, Asset Unlock transactions have an expiration period based on the quorum lifetime. Transactions are considered invalid if not signed by one of the active quorums or the most recently replaced quorum. In this case, the issuer can retry the withdrawal. To ensure that expired transactions are not included in the Core chain, Asset Unlock transactions are refused once the block height exceeds `signHeight` by 48 or more (i.e., height > `signHeight` + 48). When a version 2 Asset Unlock transaction expires and is re-signed, the replacement shares the same withdrawal transaction identifier, so transactions spending its outputs remain valid. +Asset Unlock transactions might not be mined for multiple reasons. For example, the quorum who signed a transaction expired, Core fees were too low, or the withdrawal limit was reached. To handle this situation, Asset Unlock transactions have an expiration period based on the quorum lifetime. Transactions are considered invalid if not signed by one of the active quorums or the most recently replaced quorum. In this case, the issuer can retry the withdrawal. To ensure that expired transactions are not included in the Core chain, Asset Unlock transactions are refused once the block height exceeds _signHeight_ by 48 or more (i.e., height > _signHeight_ + 48). When a version 2 Asset Unlock transaction expires and is re-signed, the replacement shares the same withdrawal transaction identifier, so transactions spending its outputs remain valid. ## Withdrawal Safety @@ -184,7 +184,7 @@ Prior to activation of the `withdrawals` hard fork in Dash Core v22.0, more rest As described [in the latest version of DIP-4](./dip-0004.md#coinbase-special-transaction), coinbase transaction versions >= 3 include the creditPoolBalance field: | **Field** | **Type** | **Size** | **Description** | -| - | - | - | - | +| -| - | - | - | | creditPoolBalance | int64_t | 8 | The total amount of duffs locked at the block height | The creditPoolBalance field represents the total amount of Dash (in duffs) locked at the block height. The value of this field must be equal to the value of this field in the previous block PLUS the sum of all Dash locked by Asset Lock transactions included in the block MINUS the sum of all Dash unlocked by Asset Unlock transactions in the block. So, for height x, the creditPoolBalance is: From 81b4f552e1e64b63587679dea145fd23686e7f6b Mon Sep 17 00:00:00 2001 From: pasta Date: Tue, 25 Aug 2026 11:16:44 +0200 Subject: [PATCH 4/4] docs(dip27): rework version 2 Asset Unlocks to stable txids Instead of a separate withdrawal transaction identifier with outputs tracked under it, the txid of a version 2 Asset Unlock transaction is itself computed with the signHeight, quorumHash and quorumSig fields zeroed, so every re-signed instance of one withdrawal is the same transaction and spends reference it like any other. The hash of the full serialization becomes the instance hash, used for relay (new MSG_ASSET_UNLOCK inventory type) and for a new coinbase commitment: CbTx version 4 (DIP-4) commits to the instance hashes the block merkle root no longer covers, with mismatches treated as block mutation. --- dip-0002/special-transactions.md | 2 +- dip-0004.md | 8 +++++ dip-0027.md | 29 ++++++++++--------- ...rawal-id-calc.py => dip-0027-txid-calc.py} | 8 ++--- 4 files changed, 29 insertions(+), 18 deletions(-) rename dip-0027/{dip-0027-withdrawal-id-calc.py => dip-0027-txid-calc.py} (82%) diff --git a/dip-0002/special-transactions.md b/dip-0002/special-transactions.md index 0ca61ff7..068c00cb 100644 --- a/dip-0002/special-transactions.md +++ b/dip-0002/special-transactions.md @@ -13,7 +13,7 @@ payload and is distinct from the _transaction_ version. | 2 | Provider Update Service Transaction (ProUpServTx) | [DIP 003: Deterministic Masternode List](https://github.com/dashpay/dips/blob/master/dip-0003.md) | 1 | Active | | 3 | Provider Update Registrar Transaction (ProUpRegTx) | [DIP 003: Deterministic Masternode List](https://github.com/dashpay/dips/blob/master/dip-0003.md) | 1 | Active | | 4 | Provider Update Revocation Transaction (ProUpRevTx) | [DIP 003: Deterministic Masternode List](https://github.com/dashpay/dips/blob/master/dip-0003.md) | 1 | Active | -| 5 | Coinbase Transaction (CbTx) | [DIP 004: Simplified Verification of Deterministic Masternode Lists](https://github.com/dashpay/dips/blob/master/dip-0004.md) | 3 | Active | +| 5 | Coinbase Transaction (CbTx) | [DIP 004: Simplified Verification of Deterministic Masternode Lists](https://github.com/dashpay/dips/blob/master/dip-0004.md) | 4 | Active | | 6 | Quorum Commitment | [DIP 006: Long Living Masternode Quorums](https://github.com/dashpay/dips/blob/master/dip-0006.md) | 1 | Active | | 7 | Masternode Hard Fork Signal | [DIP 023: Enhanced Hard Fork Mechanism](https://github.com/dashpay/dips/blob/master/dip-0023.md) | 1 | Active | | 8 | Asset Lock | [DIP 027: Dash Core Credit Pool](https://github.com/dashpay/dips/blob/master/dip-0027.md) | 1 | Active | diff --git a/dip-0004.md b/dip-0004.md index 99c7c517..c7c517fc 100644 --- a/dip-0004.md +++ b/dip-0004.md @@ -73,6 +73,14 @@ Starting with version >= 3, the following fields are added: More information on the `bestCLHeightDiff` and `bestCLSignature` fields can be found in [DIP0029 - Randomness Beacon For LLMQ Selection](https://github.com/dashpay/dips/blob/master/dip-0029.md#change-to-the-coinbase-transaction). More information on the `creditPoolBalance` field will be described in a future document. +Starting with version >= 4, the following fields are added: + +| Field | Type | Size | Description | +| ----- | ---- | ---- | ----------- | +| merkleRootAssetUnlocks | uint256 | 32 | Merkle root of the instance hashes of the block's version 2 Asset Unlock transactions | + +The transaction hash of a version 2 Asset Unlock transaction excludes its quorum signing fields (see [DIP0027 - Credit Asset Locks](https://github.com/dashpay/dips/blob/master/dip-0027.md#transaction-identity-for-version-2)), so the block's merkle root does not commit to those bytes. This field restores the commitment: it is the merkle root of the instance hashes (the hashes of the full serialization) of the block's version 2 Asset Unlock transactions in block order, or all zeros when the block contains none. A block whose commitment does not match its transactions must be rejected without marking the block hash invalid, like a block with mutated transaction data, since a relaying peer could have altered the signing fields without affecting the merkle root. + ### Height in CbTx and deprecation of BIP34 The CbTx contains the “height” field. It acts as a guaranteed variance in the CbTx so that each block’s CbTx gets a different hash. This is meant as a replacement for the height value currently found in the coinbase input (BIP34). With the deployment of this DIP, BIP34 becomes obsolete for new blocks and nodes should not enforce the presence of the block height in the coinbase input's “scriptSig” anymore. diff --git a/dip-0027.md b/dip-0027.md index ce2d4ac7..2b589bde 100644 --- a/dip-0027.md +++ b/dip-0027.md @@ -24,7 +24,7 @@ * [Asset Unlocking](#asset-unlocking) * [Asset Unlock Transaction](#asset-unlock-transaction) * [Signing Asset Unlock Transactions](#signing-asset-unlock-transactions) - * [Withdrawal Transaction Identifier](#withdrawal-transaction-identifier) + * [Transaction Identity for Version 2](#transaction-identity-for-version-2) * [Client verification](#client-verification) * [Withdrawal Completion](#withdrawal-completion) * [Withdrawal Safety](#withdrawal-safety) @@ -129,38 +129,41 @@ For reference, the following table shows some example index values and their req | 101 | fcc76a643c5c668244fdcef09833955d6f4b803fa6c459f7732983c2332389fd | | 123456789 | ebd9d75ad72184bec8e8f25a499eb5386ba564eb7ce70a4a3b9db652599d6d72 | -#### Withdrawal Transaction Identifier +#### Transaction Identity for Version 2 -Version 2 Asset Unlock transactions are serialized identically to version 1 transactions. The version field instead signals a change in how the transaction's outputs are identified. +Version 2 Asset Unlock transactions are serialized identically to version 1 transactions. The version field instead changes how the transaction hash is computed. -When an Asset Unlock transaction is not mined before it expires, Platform re-signs the withdrawal and broadcasts a replacement transaction. The replacement differs from the original only in the `signHeight`, `quorumHash`, and `quorumSig` fields; the index, fee, and outputs are unchanged. Since the transaction hash covers these fields, each replacement has a new hash, and any transaction spending an output of an unmined withdrawal becomes invalid when the withdrawal is re-signed. +When an Asset Unlock transaction is not mined before it expires, Platform re-signs the withdrawal and broadcasts a replacement transaction. The replacement differs from the original only in the `signHeight`, `quorumHash`, and `quorumSig` fields; the index, fee, and outputs are unchanged. Under version 1 hashing each replacement has a new transaction hash, so any transaction spending an output of an unmined withdrawal becomes invalid when the withdrawal is re-signed. -To resolve this, each version 2 Asset Unlock transaction has a withdrawal transaction identifier: the transaction serialized with the `signHeight`, `quorumHash`, and `quorumSig` fields set to zeros, hashed as described in [DIP-2](./dip-0002.md#serialization-hashing-and-signing). Every re-signed instance of the same withdrawal shares one withdrawal transaction identifier because the identifier omits only the fields that change when Platform re-signs. Note that the identifier is distinct from the signing session's message hash, which sets only the `quorumSig` field to zeros and therefore still commits to `signHeight` and `quorumHash`. +To resolve this, the transaction hash (txid) of a version 2 Asset Unlock transaction is computed over the transaction serialized with the `signHeight`, `quorumHash`, and `quorumSig` fields set to zeros, hashed as described in [DIP-2](./dip-0002.md#serialization-hashing-and-signing). Every re-signed instance of one withdrawal is therefore the same transaction: all instances share one txid, the transaction's outputs are tracked in the UTXO set and referenced by spending inputs under that txid as for any other transaction, and a transaction spending an output of an unmined version 2 Asset Unlock transaction remains valid regardless of which instance is eventually mined. Only one instance of a withdrawal can ever be mined since the index must be unique. -The outputs of a version 2 Asset Unlock transaction are identified by the withdrawal transaction identifier. Transactions spending these outputs must reference them by the withdrawal transaction identifier and output index, and nodes must track these outputs in the UTXO set under the withdrawal transaction identifier. A transaction spending an output of an unmined version 2 Asset Unlock transaction therefore remains valid regardless of which instance of the withdrawal is eventually mined. Only one instance of a withdrawal can ever be mined since the index must be unique. +Two related hashes remain distinct from the txid: -The merkle tree and block serialization remain unchanged, and transaction relay continues to reference the standard transaction hash of each instance. +* The _instance hash_: the hash of the full serialization (what version 1 hashing produces). It distinguishes the re-signed instances of one withdrawal and is used for relay and for the coinbase commitment described below. +* The signing session's message hash, which sets only the `quorumSig` field to zeros and therefore still commits to `signHeight` and `quorumHash`. -When a node receives a version 2 Asset Unlock transaction whose withdrawal transaction identifier matches a transaction already in its mempool, it should keep only the instance with the higher `signHeight`, provided its signature is valid. +Because the txid excludes the quorum signing fields, a block's merkle root does not commit to them. Coinbase transactions must therefore commit to the instance hashes of the block's version 2 Asset Unlock transactions; see the version 4 coinbase transaction fields in [DIP-4](./dip-0004.md#coinbase-special-transaction). A block whose coinbase commitment does not match must be rejected without marking the block hash invalid, since a relaying peer could have altered the signing fields without affecting the merkle root. -For reference, the following table shows example withdrawal transaction identifiers. Both examples use a fee of 70000 duffs and a single output paying 100000000 duffs to the P2PKH script for the public key hash `1111111111111111111111111111111111111111`. See the [provided script](dip-0027/dip-0027-withdrawal-id-calc.py) for example code: +Relay also identifies these transactions by instance hash: a re-signed instance shares its txid with the instance peers already hold, so a txid-based announcement would never propagate it. Version 2 Asset Unlock transactions are announced with the inventory type `MSG_ASSET_UNLOCK` (34) whose hash is the instance hash, and a `getdata` for it is answered with a `tx` message. When a node receives an instance of a withdrawal already in its mempool, it should keep only the instance with the higher `signHeight`, provided its signature is valid. An expired instance should be retained awaiting a re-signed replacement rather than evicted, so that transactions spending its outputs are not evicted with it. -| **Index** | **Withdrawal Transaction Identifier** | +For reference, the following table shows example version 2 txids. Both examples use a fee of 70000 duffs and a single output paying 100000000 duffs to the P2PKH script for the public key hash `1111111111111111111111111111111111111111`; the txid is independent of the `signHeight`, `quorumHash`, and `quorumSig` values. See the [provided script](dip-0027/dip-0027-txid-calc.py) for example code: + +| **Index** | **Transaction Hash (txid)** | |-----------|------------------------------------------------------------------| | 101 | 3c4db73c8356407a5d7c78df5045bd280f2dc4fd644b06c4bfbdead3d5ae41cf | | 123456789 | a67e1107ae6e04b813bc8e81348266f5206d1ca93d305dc4323940e18cdbaf34 | ### Client verification -All clients must verify the quorumSig field before accepting the transaction. Quorum signatures must be created by a recent quorum (one that is currently active or the most recently replaced one) to be considered valid. If the quorum is recent and the transaction is not yet mined, light clients should attempt to verify the quorumSig by retrieving the quorum public key as described in DIP-4 and verifying the signature. If the quorum is not recent and the transaction is not mined, the transaction should be ignored until it is mined to prevent attacks on the light client. Light clients must track the outputs of version 2 Asset Unlock transactions by their withdrawal transaction identifier. +All clients must verify the quorumSig field before accepting the transaction. Quorum signatures must be created by a recent quorum (one that is currently active or the most recently replaced one) to be considered valid. If the quorum is recent and the transaction is not yet mined, light clients should attempt to verify the quorumSig by retrieving the quorum public key as described in DIP-4 and verifying the signature. If the quorum is not recent and the transaction is not mined, the transaction should be ignored until it is mined to prevent attacks on the light client. Light clients must compute the transaction hash of version 2 Asset Unlock transactions as described above: merkle proofs of their inclusion prove this txid, not the hash of the full serialization. ### Withdrawal Completion Since Asset Unlock transactions do not have inputs, they are not eligible for InstantSend. Once mined into a block and subsequently ChainLocked, they are final and identical to all other outputs. A withdrawal should be considered complete when the corresponding Asset Unlock transaction is finalized on the Core chain. -The outputs of a version 1 Asset Unlock transaction must not be spent until the transaction is mined. The outputs of a version 2 Asset Unlock transaction may be spent before the transaction is mined, and these spends are eligible for InstantSend. Masternodes must only sign an InstantSend lock for such a spend after observing a validly signed instance of the withdrawal. At that point the withdrawal is irreversible on Platform, and Platform will re-sign the withdrawal until it is mined. Because InstantSend locks reference inputs by the withdrawal transaction identifier, these locks remain valid when the withdrawal is re-signed. Confirmation of such a spend depends on an instance of the withdrawal eventually being mined. +The outputs of a version 1 Asset Unlock transaction should not be spent until the transaction is mined, since the spend becomes invalid if the withdrawal is re-signed. The outputs of a version 2 Asset Unlock transaction may be spent before the transaction is mined, and these spends are eligible for InstantSend. Masternodes must only sign an InstantSend lock for such a spend after observing a validly signed instance of the withdrawal. At that point the withdrawal is irreversible on Platform, and Platform will re-sign the withdrawal until it is mined. Because the txid is stable across re-signs, both the spend and its InstantSend lock remain valid when the withdrawal is re-signed. Confirmation of such a spend depends on an instance of the withdrawal eventually being mined. -Asset Unlock transactions might not be mined for multiple reasons. For example, the quorum who signed a transaction expired, Core fees were too low, or the withdrawal limit was reached. To handle this situation, Asset Unlock transactions have an expiration period based on the quorum lifetime. Transactions are considered invalid if not signed by one of the active quorums or the most recently replaced quorum. In this case, the issuer can retry the withdrawal. To ensure that expired transactions are not included in the Core chain, Asset Unlock transactions are refused once the block height exceeds _signHeight_ by 48 or more (i.e., height > _signHeight_ + 48). When a version 2 Asset Unlock transaction expires and is re-signed, the replacement shares the same withdrawal transaction identifier, so transactions spending its outputs remain valid. +Asset Unlock transactions might not be mined for multiple reasons. For example, the quorum who signed a transaction expired, Core fees were too low, or the withdrawal limit was reached. To handle this situation, Asset Unlock transactions have an expiration period based on the quorum lifetime. Transactions are considered invalid if not signed by one of the active quorums or the most recently replaced quorum. In this case, the issuer can retry the withdrawal. To ensure that expired transactions are not included in the Core chain, Asset Unlock transactions are refused once the block height exceeds _signHeight_ by 48 or more (i.e., height > _signHeight_ + 48). When a version 2 Asset Unlock transaction expires and is re-signed, the replacement is the same transaction (same txid), so transactions spending its outputs remain valid. ## Withdrawal Safety diff --git a/dip-0027/dip-0027-withdrawal-id-calc.py b/dip-0027/dip-0027-txid-calc.py similarity index 82% rename from dip-0027/dip-0027-withdrawal-id-calc.py rename to dip-0027/dip-0027-txid-calc.py index 4defc21d..3ccf3352 100644 --- a/dip-0027/dip-0027-withdrawal-id-calc.py +++ b/dip-0027/dip-0027-txid-calc.py @@ -1,8 +1,8 @@ #!/usr/bin/python3 -# # Example showing how to create the withdrawal transaction identifier for a -# # version 2 Asset Unlock transaction. The identifier is the double-SHA256 of -# # the transaction serialized with the signHeight, quorumHash, and quorumSig -# # fields set to zeros. +# # Example showing how to compute the transaction hash (txid) of a version 2 +# # Asset Unlock transaction. The txid is the double-SHA256 of the transaction +# # serialized with the signHeight, quorumHash, and quorumSig fields set to +# # zeros, so every re-signed instance of one withdrawal shares one txid. import hashlib import struct