diff --git a/mm2src/derives/enum_derives/src/lib.rs b/mm2src/derives/enum_derives/src/lib.rs index 18d55e912a..854030a732 100644 --- a/mm2src/derives/enum_derives/src/lib.rs +++ b/mm2src/derives/enum_derives/src/lib.rs @@ -144,7 +144,6 @@ pub fn derive(input: TokenStream) -> TokenStream { /// Polygon, /// } /// -///#[test] ///fn test_enum_variant_list() { /// let all_chains = Chain::variant_list(); /// assert_eq!(all_chains, vec![ diff --git a/mm2src/mm2_bitcoin/script/src/sign.rs b/mm2src/mm2_bitcoin/script/src/sign.rs index b4a3399a7e..c5eb79d3be 100644 --- a/mm2src/mm2_bitcoin/script/src/sign.rs +++ b/mm2src/mm2_bitcoin/script/src/sign.rs @@ -10,6 +10,7 @@ use hash::{H256, H512}; use keys::KeyPair; use ser::Stream; use serde::Deserialize; +use std::collections::BTreeSet; use std::convert::TryInto; use {Builder, Script}; @@ -29,6 +30,8 @@ pub enum SignatureVersion { WitnessV0, #[serde(rename = "fork_id")] ForkId, + #[serde(rename = "fork_id_rxd")] + ForkIdRxd, } #[derive(Debug, PartialEq, Clone, Copy)] @@ -79,7 +82,7 @@ impl Sighash { pub fn is_defined(version: SignatureVersion, u: u32) -> bool { // reset anyone_can_pay && fork_id (if applicable) bits let u = match version { - SignatureVersion::ForkId => u & !(0x40 | 0x80), + SignatureVersion::ForkId | SignatureVersion::ForkIdRxd => u & !(0x40 | 0x80), _ => u & !(0x80), }; @@ -90,7 +93,7 @@ impl Sighash { /// Creates Sighash from any u, even if is_defined() == false pub fn from_u32(version: SignatureVersion, u: u32) -> Self { let anyone_can_pay = (u & 0x80) == 0x80; - let fork_id = version == SignatureVersion::ForkId && (u & 0x40) == 0x40; + let fork_id = matches!(version, SignatureVersion::ForkId | SignatureVersion::ForkIdRxd) && (u & 0x40) == 0x40; let base = match u & 0x1f { 2 => SighashBase::None, 3 => SighashBase::Single, @@ -246,7 +249,10 @@ impl TransactionInputSigner { SignatureVersion::ForkId if sighash.fork_id => { self.signature_hash_fork_id(input_index, input_amount, script_pubkey, sighashtype, sighash) }, - SignatureVersion::Base | SignatureVersion::ForkId => { + SignatureVersion::ForkIdRxd if sighash.fork_id => { + self.signature_hash_fork_id_rxd(input_index, input_amount, script_pubkey, sighashtype, sighash) + }, + SignatureVersion::Base | SignatureVersion::ForkId | SignatureVersion::ForkIdRxd => { self.signature_hash_original(input_index, script_pubkey, sighashtype, sighash) }, SignatureVersion::WitnessV0 => { @@ -435,6 +441,43 @@ impl TransactionInputSigner { self.signature_hash_witness0(input_index, input_amount, script_pubkey, sighashtype, sighash) } + fn signature_hash_fork_id_rxd( + &self, + input_index: usize, + input_amount: u64, + script_pubkey: &Script, + sighashtype: u32, + sighash: Sighash, + ) -> H256 { + if input_index >= self.inputs.len() { + return 1u8.into(); + } + + if sighash.base == SighashBase::Single && input_index >= self.outputs.len() { + return 1u8.into(); + } + + let hash_prevouts = compute_hash_prevouts(sighash, &self.inputs); + let hash_sequence = compute_hash_sequence(sighash, &self.inputs); + let hash_output_hashes = compute_hash_output_hashes_rxd(sighash, input_index, &self.outputs); + let hash_outputs = compute_hash_outputs(sighash, input_index, &self.outputs); + + let mut stream = Stream::default(); + stream.append(&self.version); + stream.append(&hash_prevouts); + stream.append(&hash_sequence); + stream.append(&self.inputs[input_index].previous_output); + stream.append_list(script_pubkey); + stream.append(&input_amount); + stream.append(&self.inputs[input_index].sequence); + stream.append(&hash_output_hashes); + stream.append(&hash_outputs); + stream.append(&self.lock_time); + stream.append(&sighashtype); + let out = stream.out(); + dhash256(&out) + } + /// https://github.com/zcash/zips/blob/master/zip-0243.rst#notes /// This method doesn't cover all possible Sighash combinations so it doesn't fully match the /// specification, however I don't need other cases yet as BarterDEX marketmaker always uses @@ -615,6 +658,126 @@ fn compute_hash_outputs(sighash: Sighash, input_index: usize, outputs: &[Transac } } +fn compute_hash_output_hashes_rxd(sighash: Sighash, input_index: usize, outputs: &[TransactionOutput]) -> H256 { + match sighash.base { + SighashBase::All => { + let mut stream = Stream::default(); + for output in outputs { + append_output_data_summary_rxd(&mut stream, output); + } + dhash256(&stream.out()) + }, + SighashBase::Single if input_index < outputs.len() => { + let mut stream = Stream::default(); + append_output_data_summary_rxd(&mut stream, &outputs[input_index]); + dhash256(&stream.out()) + }, + SighashBase::Single | SighashBase::None => 0u8.into(), + } +} + +fn append_output_data_summary_rxd(stream: &mut Stream, output: &TransactionOutput) { + let script_pubkey_hash = dhash256(output.script_pubkey.as_ref()); + let sorted_push_refs = sorted_push_refs_rxd(output.script_pubkey.as_ref()); + let total_refs: u32 = sorted_push_refs.len() as u32; + let refs_hash = if sorted_push_refs.is_empty() { + 0u8.into() + } else { + let refs_concat: Vec = sorted_push_refs.into_iter().flatten().collect(); + dhash256(&refs_concat) + }; + + stream.append(&output.value); + stream.append(&script_pubkey_hash); + stream.append(&total_refs); + stream.append(&refs_hash); +} + +fn sorted_push_refs_rxd(script_pubkey: &[u8]) -> Vec<[u8; 36]> { + const OP_PUSHDATA1: u8 = 0x4c; + const OP_PUSHDATA2: u8 = 0x4d; + const OP_PUSHDATA4: u8 = 0x4e; + const OP_PUSHINPUTREF: u8 = 0xd0; + const OP_REQUIREINPUTREF: u8 = 0xd1; + const OP_DISALLOWPUSHINPUTREF: u8 = 0xd2; + const OP_DISALLOWPUSHINPUTREFSIBLING: u8 = 0xd3; + const OP_PUSHINPUTREFSINGLETON: u8 = 0xd8; + + let mut refs = BTreeSet::new(); + let mut pos = 0usize; + + while pos < script_pubkey.len() { + let opcode = script_pubkey[pos]; + pos += 1; + + match opcode { + 0x01..=0x4b => { + let push_len = opcode as usize; + match pos.checked_add(push_len) { + Some(end) if end <= script_pubkey.len() => pos = end, + _ => break, + } + }, + OP_PUSHDATA1 => { + if pos >= script_pubkey.len() { + break; + } + let push_len = script_pubkey[pos] as usize; + pos += 1; + match pos.checked_add(push_len) { + Some(end) if end <= script_pubkey.len() => pos = end, + _ => break, + } + }, + OP_PUSHDATA2 => { + if pos + 2 > script_pubkey.len() { + break; + } + let push_len = u16::from_le_bytes([script_pubkey[pos], script_pubkey[pos + 1]]) as usize; + pos += 2; + match pos.checked_add(push_len) { + Some(end) if end <= script_pubkey.len() => pos = end, + _ => break, + } + }, + OP_PUSHDATA4 => { + if pos + 4 > script_pubkey.len() { + break; + } + let push_len = u32::from_le_bytes([ + script_pubkey[pos], + script_pubkey[pos + 1], + script_pubkey[pos + 2], + script_pubkey[pos + 3], + ]) as usize; + pos += 4; + match pos.checked_add(push_len) { + Some(end) if end <= script_pubkey.len() => pos = end, + _ => break, + } + }, + OP_PUSHINPUTREF + | OP_PUSHINPUTREFSINGLETON + | OP_REQUIREINPUTREF + | OP_DISALLOWPUSHINPUTREF + | OP_DISALLOWPUSHINPUTREFSIBLING => { + if pos + 36 > script_pubkey.len() { + break; + } + if matches!(opcode, OP_PUSHINPUTREF | OP_PUSHINPUTREFSINGLETON) { + let mut reference = [0u8; 36]; + reference.copy_from_slice(&script_pubkey[pos..pos + 36]); + refs.insert(reference); + } + pos += 36; + }, + _ => (), + } + } + + refs.into_iter().collect() +} + fn blake_2b_256_personal(input: &[u8], personal: &[u8]) -> Result { let bytes: [u8; 32] = Blake2b::new() .hash_length(32) @@ -631,14 +794,15 @@ fn blake_2b_256_personal(input: &[u8], personal: &[u8]) -> Result #[cfg(test)] mod tests { use super::{ - blake_2b_256_personal, Sighash, SighashBase, SignatureVersion, TransactionInputSigner, UnsignedTransactionInput, + blake_2b_256_personal, sorted_push_refs_rxd, Sighash, SighashBase, SignatureVersion, TransactionInputSigner, + UnsignedTransactionInput, }; use bytes::Bytes; use chain::{OutPoint, Transaction, TransactionOutput}; use hash::{H160, H256}; use keys::{ prefixes::{BTC_PREFIXES, T_BTC_PREFIXES}, - Address, AddressHashEnum, Private, + Address, AddressHashEnum, Private, Public, Signature, }; use script::Script; use ser::deserialize; @@ -792,6 +956,77 @@ mod tests { assert!(Sighash::is_defined(SignatureVersion::ForkId, 0x00000081)); assert!(Sighash::is_defined(SignatureVersion::ForkId, 0x000000C2)); assert!(Sighash::is_defined(SignatureVersion::ForkId, 0x00000043)); + + assert!(Sighash::is_defined(SignatureVersion::ForkIdRxd, 0x00000081)); + assert!(Sighash::is_defined(SignatureVersion::ForkIdRxd, 0x000000C2)); + assert!(Sighash::is_defined(SignatureVersion::ForkIdRxd, 0x00000043)); + } + + #[test] + fn test_rxd_forkid_sighash_vector() { + const SPEND_TX_HEX: &str = "0100000002502d0525588143dee5d3857b6b901805fb4ec53cca16b374f25bf8c3e12644ee010000006a47304402204715bf639b322d08cfabb2b1b7af1ba8b6b3d571a3629a2ae5faa39b8483942f0220424747e9e1cda6e034501171292fac690da050d5221cbfd318d880eb9701405541210275c802fa50d9a1f2b89c7e43b74c77e8826209b8aa79ed144c6768b9a6f262a1ffffffff457a0a555e6fbdaf5e5f2c241847026e1cf38ee3f523ebcd0dc27683fdcd4c55000000006a47304402200be359814746c94556d80fae90f02cc9bcff50a60a1a87dadce7579eb986d783022028f7d7a39561fbacde025a91b41b6279d4ffda991d9ef69804bde990f663c1df41210275c802fa50d9a1f2b89c7e43b74c77e8826209b8aa79ed144c6768b9a6f262a1ffffffff0300ca9a3b0000000017a914cf53278b47afd12ffd864a00090b6df471d22a16870000000000000000166a14bddadc147d635060f518c7d59e481090dc066f8bdb1ae329010000001976a914d4466bdfcc471b6207a108289b847d562044539288acd8c89169"; + const INPUT_INDEX: usize = 0; + const INPUT_AMOUNT_SATS: u64 = 998_119_699; + const PREVOUT_SCRIPT_CODE_HEX: &str = "76a914d4466bdfcc471b6207a108289b847d562044539288ac"; + const SIGHASH_U32: u32 = 0x41; + const DER_SIGNATURE_HEX: &str = + "304402204715bf639b322d08cfabb2b1b7af1ba8b6b3d571a3629a2ae5faa39b8483942f0220424747e9e1cda6e034501171292fac690da050d5221cbfd318d880eb97014055"; + const PUBKEY_HEX: &str = "0275c802fa50d9a1f2b89c7e43b74c77e8826209b8aa79ed144c6768b9a6f262a1"; + const EXPECTED_SIGHASH_HEX: &str = "201a2654d0ed04643080bbdda4acdf40a3d31a6a2e0c486270476fd3c6409187"; + + let tx: Transaction = SPEND_TX_HEX.into(); + let mut signer: TransactionInputSigner = tx.into(); + signer.inputs[INPUT_INDEX].amount = INPUT_AMOUNT_SATS; + + let script_code: Script = PREVOUT_SCRIPT_CODE_HEX.into(); + let sighash = signer.signature_hash( + INPUT_INDEX, + INPUT_AMOUNT_SATS, + &script_code, + SignatureVersion::ForkIdRxd, + SIGHASH_U32, + ); + + let signature: Signature = DER_SIGNATURE_HEX.into(); + let pubkey_bytes: Bytes = PUBKEY_HEX.into(); + let pubkey = Public::from_slice(&pubkey_bytes).expect("valid compressed pubkey"); + + assert!(pubkey + .verify(&sighash, &signature) + .expect("signature verification should not error")); + + let expected_sighash: H256 = EXPECTED_SIGHASH_HEX.into(); + assert_eq!( + sighash, expected_sighash, + "RXD sighash mismatch: computed {}, expected {}", + sighash, expected_sighash + ); + } + + #[test] + fn test_sorted_push_refs_rxd_extracts_and_sorts_push_refs() { + const OP_PUSHINPUTREF: u8 = 0xd0; + const OP_REQUIREINPUTREF: u8 = 0xd1; + const OP_PUSHINPUTREFSINGLETON: u8 = 0xd8; + + let ref_a = [0x11u8; 36]; + let ref_b = [0x22u8; 36]; + let ref_ignored = [0x33u8; 36]; + + let mut script_pubkey = Vec::new(); + script_pubkey.extend_from_slice(&[0x51]); + script_pubkey.push(OP_PUSHINPUTREF); + script_pubkey.extend_from_slice(&ref_b); + script_pubkey.push(OP_REQUIREINPUTREF); + script_pubkey.extend_from_slice(&ref_ignored); + script_pubkey.push(OP_PUSHINPUTREFSINGLETON); + script_pubkey.extend_from_slice(&ref_a); + script_pubkey.push(OP_PUSHINPUTREF); + script_pubkey.extend_from_slice(&ref_a); + + let refs = sorted_push_refs_rxd(&script_pubkey); + + assert_eq!(refs, vec![ref_a, ref_b]); } #[test]