Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 50 additions & 11 deletions dash/src/blockdata/transaction/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
/// 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<W: io::Write + ?Sized>(&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.
Expand All @@ -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.
Expand All @@ -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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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());
}
}
19 changes: 18 additions & 1 deletion dash/src/network/message_blockdata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand All @@ -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 {
Expand Down Expand Up @@ -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<u8> = hex!(
Expand Down
Loading