diff --git a/dash/src/blockdata/transaction/mod.rs b/dash/src/blockdata/transaction/mod.rs index 276f4cf49..659315571 100644 --- a/dash/src/blockdata/transaction/mod.rs +++ b/dash/src/blockdata/transaction/mod.rs @@ -41,6 +41,7 @@ use crate::blockdata::constants::WITNESS_SCALE_FACTOR; #[cfg(feature = "bitcoinconsensus")] use crate::blockdata::script; use crate::blockdata::script::Script; +use crate::blockdata::transaction::special_transaction::asset_unlock::qualified_asset_unlock::AssetUnlockPayload; pub use crate::blockdata::transaction::special_transaction::{TransactionPayload, TransactionType}; use crate::blockdata::transaction::txin::TxIn; use crate::blockdata::transaction::txout::TxOut; @@ -237,13 +238,51 @@ impl Transaction { /// to the output of `wtxid()`, but for segwit transactions, /// this will give the correct txid (not including witnesses) while `wtxid` /// will also hash witnesses. + /// + /// For an asset unlock with payload version 2 or higher the quorum signing info is zeroed + /// before hashing, as Core does, so every re-signed instance of one withdrawal shares one + /// txid. [`Self::instance_hash`] tells those instances apart, and so does `wtxid()`, which + /// hashes them in full. pub fn txid(&self) -> Txid { + if !self.has_stable_txid() { + return self.instance_hash(); + } + let mut bytes = Vec::new(); + self.encode_txid_data(&mut bytes).expect("vecs don't error"); + let quorum_info_start = bytes.len() - AssetUnlockPayload::QUORUM_INFO_SIZE; + bytes[quorum_info_start..].fill(0); + Txid::hash(&bytes) + } + + /// The hash of the full serialization, without witnesses. It equals [`Self::txid`] except + /// for asset unlocks with payload version 2 or higher, which are relayed by this hash. + pub fn instance_hash(&self) -> Txid { let mut enc = Txid::engine(); - self.version.consensus_encode(&mut enc).expect("engines don't error"); - self.tx_type().to_u16().consensus_encode(&mut enc).expect("engines don't error"); - self.input.consensus_encode(&mut enc).expect("engines don't error"); - self.output.consensus_encode(&mut enc).expect("engines don't error"); - self.lock_time.consensus_encode(&mut enc).expect("engines don't error"); + self.encode_txid_data(&mut enc).expect("engines don't error"); + Txid::from_engine(enc) + } + + /// Whether the txid excludes the quorum signing info of an asset unlock payload. + /// + /// Judged from the payload bytes, as Core does, so a payload kept raw counts too. + fn has_stable_txid(&self) -> bool { + let Some(payload) = &self.special_transaction_payload else { + return false; + }; + if (self.version as i16) < 3 || payload.get_type() != TransactionType::AssetUnlock { + return false; + } + let bytes = encode::serialize(payload); + bytes.len() == AssetUnlockPayload::SIZE + && bytes[0] >= AssetUnlockPayload::STABLE_TXID_VERSION + } + + fn encode_txid_data(&self, w: &mut W) -> Result<(), io::Error> { + self.version.consensus_encode(w)?; + self.tx_type().to_u16().consensus_encode(w)?; + self.input.consensus_encode(w)?; + self.output.consensus_encode(w)?; + self.lock_time.consensus_encode(w)?; if let Some(payload) = &self.special_transaction_payload { // Pre-DIP-0002 transactions have no payload section on the wire — keep // the txid bit-identical to the on-chain bytes by skipping the section. @@ -252,13 +291,12 @@ impl Transaction { TransactionPayload::ClassicalWithNonStandardVersionTypeBytesPayloadType(_) ) { let mut buf = Vec::new(); - payload.consensus_encode(&mut buf).expect("engines don't error"); + payload.consensus_encode(&mut buf)?; // this is so we get the size of the payload - buf.consensus_encode(&mut enc).expect("engines don't error"); + buf.consensus_encode(w)?; } } - - Txid::from_engine(enc) + Ok(()) } /// Get the transaction type. If a classical transaction this would be 0. @@ -268,8 +306,9 @@ impl Transaction { } /// Computes SegWit-version of the transaction id (wtxid). For transaction with the witness - /// data this hash includes witness, for pre-witness transaction it is equal to the normal - /// value returned by txid() function. + /// data this hash includes witness, for pre-witness transaction it is equal to `txid()`, + /// except for asset unlocks with payload version 2 or higher, where it equals + /// [`Self::instance_hash`]. pub fn wtxid(&self) -> Wtxid { let mut enc = Wtxid::engine(); self.consensus_encode(&mut enc).expect("engines don't error"); diff --git a/dash/src/blockdata/transaction/special_transaction/asset_unlock/qualified_asset_unlock.rs b/dash/src/blockdata/transaction/special_transaction/asset_unlock/qualified_asset_unlock.rs index 40317bddb..91aee0702 100644 --- a/dash/src/blockdata/transaction/special_transaction/asset_unlock/qualified_asset_unlock.rs +++ b/dash/src/blockdata/transaction/special_transaction/asset_unlock/qualified_asset_unlock.rs @@ -62,6 +62,17 @@ pub struct AssetUnlockPayload { } impl AssetUnlockPayload { + /// First payload version whose txid excludes the quorum signing info, so every re-signed + /// instance of one withdrawal shares one txid. + pub const STABLE_TXID_VERSION: u8 = 2; + + /// Size of the trailing quorum signing info (`request_info` and `quorum_sig`), the only + /// fields Platform changes when it re-signs a withdrawal. + pub const QUORUM_INFO_SIZE: usize = AssetUnlockRequestInfo::SIZE + 96; + + /// Size of the payload: version, index and fee, then the quorum signing info. + pub const SIZE: usize = 1 + 8 + 4 + Self::QUORUM_INFO_SIZE; + /// The size of the payload in bytes. pub fn size(&self) -> usize { self.base.size() + self.request_info.size() + 96 @@ -152,7 +163,8 @@ mod tests { }; use crate::transaction::special_transaction::asset_unlock::request_info::AssetUnlockRequestInfo; use crate::transaction::special_transaction::asset_unlock::unqualified_asset_unlock::AssetUnlockBasePayload; - use crate::{ScriptBuf, Transaction, TxOut, consensus}; + use crate::{PubkeyHash, ScriptBuf, Transaction, TxOut, Txid, consensus}; + use test_case::test_case; #[test] fn size() { @@ -259,4 +271,70 @@ mod tests { assert_eq!(tx.size(), ASSET_UNLOCK_TX_SIZE); } + + /// The DIP-0027 example unlock: one 1 DASH output to a P2PKH of `0x11` bytes. + fn unlock_tx(version: u8, index: u64, request_height: u32, quorum_sig: u8) -> Transaction { + let mut quorum_hash = [0; 32]; + quorum_hash[0] = 1; + Transaction { + version: 3, + lock_time: 0, + input: vec![], + output: vec![TxOut { + value: 100_000_000, + script_pubkey: ScriptBuf::new_p2pkh(&PubkeyHash::from_byte_array([0x11; 20])), + }], + special_transaction_payload: Some(TransactionPayload::AssetUnlockPayloadType( + AssetUnlockPayload { + base: AssetUnlockBasePayload { + version, + index, + fee: 70_000, + }, + request_info: AssetUnlockRequestInfo { + request_height, + quorum_hash: QuorumHash::from_byte_array(quorum_hash), + }, + quorum_sig: BLSSignature::from([quorum_sig; 96]), + }, + )), + } + } + + // Core's `evo_assetunlock` test, from dip-0027/dip-0027-txid-calc.py. + #[test_case(101, "3c4db73c8356407a5d7c78df5045bd280f2dc4fd644b06c4bfbdead3d5ae41cf")] + #[test_case(123456789, "a67e1107ae6e04b813bc8e81348266f5206d1ca93d305dc4323940e18cdbaf34")] + fn version_2_txid_matches_dip_0027(index: u64, txid: &str) { + assert_eq!(unlock_tx(2, index, 500, 0).txid(), Txid::from_str(txid).unwrap()); + } + + // Core tells a stable txid from the payload bytes, so a payload kept raw hashes the same. + #[test_case(1; "version 1")] + #[test_case(2; "version 2")] + fn raw_payload_has_the_typed_txid(version: u8) { + let typed = unlock_tx(version, 101, 500, 0); + let payload = typed.special_transaction_payload.as_ref().unwrap(); + let raw = Transaction { + special_transaction_payload: Some(TransactionPayload::RawPayloadType( + 9, + consensus::serialize(payload), + )), + ..typed.clone() + }; + assert_eq!(raw.txid(), typed.txid()); + assert_eq!(raw.instance_hash(), typed.instance_hash()); + } + + #[test] + fn version_2_txid_excludes_the_quorum_signing_info() { + let tx = unlock_tx(2, 101, 500, 0); + let resigned = unlock_tx(2, 101, 700, 1); + assert_eq!(resigned.txid(), tx.txid()); + assert_ne!(resigned.instance_hash(), tx.instance_hash()); + assert_eq!(tx.instance_hash(), Txid::hash(&consensus::serialize(&tx))); + + let v1 = unlock_tx(1, 101, 500, 0); + assert_eq!(v1.txid(), v1.instance_hash()); + assert_ne!(unlock_tx(1, 101, 700, 1).txid(), v1.txid()); + } } diff --git a/dash/src/network/message_blockdata.rs b/dash/src/network/message_blockdata.rs index e0d090bca..485e62b4d 100644 --- a/dash/src/network/message_blockdata.rs +++ b/dash/src/network/message_blockdata.rs @@ -54,6 +54,11 @@ pub enum Inventory { ChainLock(ChainLockHash), InstantSendLock(InstantSendLockHash), + /// Version 2 asset unlock, announced by its [`Transaction::instance_hash`]. A `getdata` + /// for it is answered with a `tx` message. + /// + /// [`Transaction::instance_hash`]: crate::Transaction::instance_hash + AssetUnlock(Txid), /// Unknown inventory type Unknown { @@ -84,6 +89,7 @@ impl Encodable for Inventory { Inventory::ChainLock(ref b) => encode_inv!(29, b), Inventory::InstantSendLock(ref b) => encode_inv!(31, b), + Inventory::AssetUnlock(ref t) => encode_inv!(34, t), Inventory::Unknown { inv_type: t, @@ -106,6 +112,7 @@ impl Decodable for Inventory { 5 => Inventory::WTx(Decodable::consensus_decode(r)?), 29 => Inventory::ChainLock(Decodable::consensus_decode(r)?), 31 => Inventory::InstantSendLock(Decodable::consensus_decode(r)?), + 34 => Inventory::AssetUnlock(Decodable::consensus_decode(r)?), 0x40000001 => Inventory::WitnessTransaction(Decodable::consensus_decode(r)?), 0x40000002 => Inventory::WitnessBlock(Decodable::consensus_decode(r)?), tp => Inventory::Unknown { @@ -174,10 +181,20 @@ impl_consensus_encoding!(GetHeadersMessage, version, locator_hashes, stop_hash); mod tests { use hashes::Hash; - use super::{GetBlocksMessage, GetHeadersMessage, Vec}; + use super::{GetBlocksMessage, GetHeadersMessage, Inventory, Vec}; + use crate::Txid; use crate::consensus::encode::{deserialize, serialize}; use crate::internal_macros::hex; + #[test] + fn asset_unlock_inventory_round_trips() { + let mut bytes = 34u32.to_le_bytes().to_vec(); + bytes.extend([0xab; 32]); + let inv: Inventory = deserialize(&bytes).unwrap(); + assert_eq!(inv, Inventory::AssetUnlock(Txid::from_byte_array([0xab; 32]))); + assert_eq!(serialize(&inv), bytes); + } + #[test] fn getblocks_message_test() { let from_sat: Vec = hex!(