From c59ea7b812957246f7951dadd660ae6db76328d1 Mon Sep 17 00:00:00 2001 From: Gregory Hill Date: Wed, 18 May 2022 14:09:46 +0100 Subject: [PATCH 1/2] feat: support taproot addresses Signed-off-by: Gregory Hill --- crates/bitcoin/src/address.rs | 25 ++++++++-- crates/bitcoin/src/parser.rs | 93 ++++++++++++++++++++++++++++++++++- crates/bitcoin/src/script.rs | 11 ++++- crates/bitcoin/src/types.rs | 32 +++++++++++- 4 files changed, 152 insertions(+), 9 deletions(-) diff --git a/crates/bitcoin/src/address.rs b/crates/bitcoin/src/address.rs index 9ef5fc3251..6663dc73e5 100644 --- a/crates/bitcoin/src/address.rs +++ b/crates/bitcoin/src/address.rs @@ -14,8 +14,8 @@ use secp256k1::{constants::PUBLIC_KEY_SIZE, Error as Secp256k1Error, PublicKey a #[derive(Encode, Decode, Clone, Ord, PartialOrd, PartialEq, Eq, Debug, Copy, TypeInfo, MaxEncodedLen)] #[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize, std::hash::Hash))] pub enum Address { - // input: {signature} {pubkey} - // output: OP_DUP OP_HASH160 {hash160(pubkey)} OP_EQUALVERIFY OP_CHECKSIG + // input: {signature} {pub_key} + // output: OP_DUP OP_HASH160 {hash160(pub_key)} OP_EQUALVERIFY OP_CHECKSIG // witness: <> P2PKH(H160), // input: [redeem_script_sig ...] {redeem_script} @@ -23,13 +23,19 @@ pub enum Address { // witness: P2SH(H160), // input: <> - // output: OP_0 {hash160(pubkey)} - // witness: {signature} {pubkey} + // output: OP_0 {hash160(pub_key)} + // witness: {signature} {pub_key} P2WPKHv0(H160), // input: <> // output: OP_0 {sha256(redeem_script)} // witness: [redeem_script_sig ...] {redeem_script} P2WSHv0(H256), + // input: <> + // output: OP_1 {tweaked_pub_key} + // witness: + // - key path: {signature} + // - script path: [arguments ...] {script} {untweaked_pub_key} + P2TRv1(H256), } impl Address { @@ -40,6 +46,7 @@ impl Address { const OP_CHECK_SIG: u8 = OpCode::OpCheckSig as u8; const OP_EQUAL: u8 = OpCode::OpEqual as u8; const OP_0: u8 = OpCode::Op0 as u8; + const OP_1: u8 = OpCode::Op1 as u8; match script.as_bytes() { &[OP_DUP, OP_HASH_160, HASH160_SIZE_HEX, ref addr @ .., OP_EQUAL_VERIFY, OP_CHECK_SIG] @@ -56,6 +63,9 @@ impl Address { &[OP_0, HASH160_SIZE_HEX, ref addr @ ..] if addr.len() == HASH160_SIZE_HEX as usize => { Ok(Self::P2WPKHv0(H160::from_slice(addr))) } + &[OP_1, HASH256_SIZE_HEX, ref addr @ ..] if addr.len() == HASH256_SIZE_HEX as usize => { + Ok(Self::P2TRv1(H256::from_slice(addr))) + } _ => Err(Error::InvalidBtcAddress), } } @@ -94,6 +104,13 @@ impl Address { script.append(script_hash); script } + Self::P2TRv1(tweaked_pub_key) => { + let mut script = Script::new(); + script.append(OpCode::Op1); + script.append(HASH256_SIZE_HEX); + script.append(tweaked_pub_key); + script + } } } diff --git a/crates/bitcoin/src/parser.rs b/crates/bitcoin/src/parser.rs index 2779e4a707..37e09bf805 100644 --- a/crates/bitcoin/src/parser.rs +++ b/crates/bitcoin/src/parser.rs @@ -9,7 +9,7 @@ use sha2::{Digest, Sha256}; #[cfg(test)] use mocktopus::macros::mockable; -use crate::{Error, SetCompact}; +use crate::{Error, Script, SetCompact}; use sp_core::U256; use sp_std::{prelude::*, vec}; @@ -436,6 +436,41 @@ fn parse_transaction_output(raw_output: &[u8]) -> Result<(TransactionOutput, usi )) } +#[allow(dead_code)] +pub(crate) fn extract_witness_standard(witness_stack: Vec>, prev_script: Script) -> Result { + let witness_last = witness_stack.last().cloned().unwrap_or_default(); + if prev_script.is_p2wpkh_v0() { + // https://github.com/bitcoin/bitcoin/blob/d492dc1cdaabdc52b0766bf4cba4bd73178325d0/src/script/standard.cpp#L187 + // TODO: verify input + extract_address_hash_witness(&witness_last) + } else if prev_script.is_p2wsh_v0() { + // https://github.com/bitcoin/bitcoin/blob/623745ca74cf3f54b474dac106f5802b7929503f/src/policy/policy.cpp#L237 + if witness_last.len() > MAX_STANDARD_P2WSH_SCRIPT_SIZE || witness_stack.len() > MAX_STANDARD_P2WSH_STACK_ITEMS { + Err(Error::MalformedTransaction) + } else { + // TODO: verify input + extract_address_hash_witness(&witness_last) + } + } else if prev_script.is_p2tr_v1() { + // https://github.com/bitcoin/bitcoin/blob/623745ca74cf3f54b474dac106f5802b7929503f/src/policy/policy.cpp#L252 + if witness_last.get(0).map(|i| i == &ANNEX_TAG).unwrap_or_default() { + Err(Error::MalformedTransaction) + } else if witness_stack.len() >= 2 { + // script path spend + // TODO: verify Q = tweak(P,S) + Address::from_script_pub_key(&prev_script) + } else if witness_stack.len() == 1 { + // key path spend + // TODO: Verify(pk, m, sig) + Address::from_script_pub_key(&prev_script) + } else { + Err(Error::MalformedTransaction) + } + } else { + Err(Error::MalformedTransaction) + } +} + pub(crate) fn extract_address_hash_witness>(witness_script: B) -> Result { let witness_script = witness_script.as_ref(); // first check if the witness is the compressed public key @@ -863,6 +898,62 @@ pub(crate) mod tests { assert_eq!(&extr_address, &address); } + #[test] + fn test_extract_witness_standard_taproot() { + // key path + assert_ok!( + extract_witness_standard( + vec![vec![ + 19, 72, 150, 196, 44, 217, 86, 128, 176, 72, 132, 88, 71, 200, 5, 71, 86, 134, 31, 250, 183, 212, + 171, 171, 114, 246, 80, 141, 103, 209, 236, 12, 89, 2, 135, 236, 33, 97, 221, 120, 132, 152, 50, + 134, 225, 205, 86, 206, 101, 192, 138, 36, 238, 4, 118, 237, 233, 38, 120, 169, 59, 27, 24, 12, + ]], + Script { + bytes: vec![ + 81, 32, 95, 66, 55, 189, 125, 174, 87, 107, 52, 171, 200, 169, 198, 250, 79, 14, 71, 135, 192, + 66, 52, 202, 150, 62, 158, 150, 200, 249, 182, 123, 86, 209 + ] + } + ), + Address::P2TRv1(H256::from_slice( + &hex::decode("5f4237bd7dae576b34abc8a9c6fa4f0e4787c04234ca963e9e96c8f9b67b56d1").unwrap() + )) + ); + + // script path + assert_ok!( + extract_witness_standard( + vec![ + vec![ + 123, 93, 97, 74, 70, 16, 191, 145, 150, 119, 87, 145, 252, 197, 137, 89, 124, 160, 102, 220, + 209, 0, 72, 224, 4, 205, 76, 115, 65, 187, 75, 185, 12, 238, 71, 5, 25, 47, 63, 125, 181, 36, + 232, 6, 122, 82, 34, 199, 240, 155, 175, 41, 239, 107, 128, 91, 131, 39, 236, 209, 229, 171, + 131, 202 + ], + vec![ + 32, 245, 176, 89, 185, 167, 34, 152, 204, 190, 255, 245, 157, 155, 148, 63, 126, 15, 201, 29, + 138, 59, 148, 74, 149, 231, 182, 57, 12, 201, 158, 181, 244, 172 + ], + vec![ + 192, 217, 223, 223, 15, 227, 200, 62, 152, 112, 9, 93, 103, 255, 245, 154, 128, 86, 218, 210, + 140, 109, 251, 148, 75, 183, 28, 246, 75, 144, 172, 233, 167, 119, 107, 34, 161, 24, 95, 178, + 220, 149, 36, 246, 177, 120, 226, 105, 49, 137, 191, 1, 101, 93, 127, 56, 240, 67, 146, 54, + 104, 220, 90, 244, 91 + ] + ], + Script { + bytes: vec![ + 81, 32, 95, 66, 55, 189, 127, 147, 198, 148, 3, 163, 12, 107, 100, 31, 39, 204, 245, 32, 16, + 144, 21, 47, 207, 21, 150, 71, 66, 33, 48, 120, 49, 195 + ] + } + ), + Address::P2TRv1(H256::from_slice( + &hex::decode("5f4237bd7f93c69403a30c6b641f27ccf5201090152fcf1596474221307831c3").unwrap() + )) + ); + } + /* #[test] fn test_extract_address_invalid_p2pkh_fails() { diff --git a/crates/bitcoin/src/script.rs b/crates/bitcoin/src/script.rs index def1f97940..e8f89761ec 100644 --- a/crates/bitcoin/src/script.rs +++ b/crates/bitcoin/src/script.rs @@ -39,18 +39,25 @@ impl Script { pub fn is_p2wpkh_v0(&self) -> bool { // first byte is version - self.len() == P2WPKH_V0_SCRIPT_SIZE as usize + self.len() == WITNESS_V0_KEYHASH_SIZE + 2 as usize && self.bytes[0] == OpCode::Op0 as u8 && self.bytes[1] == HASH160_SIZE_HEX } pub fn is_p2wsh_v0(&self) -> bool { // first byte is version - self.len() == P2WSH_V0_SCRIPT_SIZE as usize + self.len() == WITNESS_V0_SCRIPTHASH_SIZE + 2 as usize && self.bytes[0] == OpCode::Op0 as u8 && self.bytes[1] == HASH256_SIZE_HEX } + pub fn is_p2tr_v1(&self) -> bool { + // first byte is version + self.len() == WITNESS_V1_TAPROOT_SIZE + 2 as usize + && self.bytes[0] == OpCode::Op1 as u8 + && self.bytes[1] == HASH256_SIZE_HEX + } + pub fn is_p2pkh(&self) -> bool { self.len() == P2PKH_SCRIPT_SIZE as usize && self.bytes[0] == OpCode::OpDup as u8 diff --git a/crates/bitcoin/src/types.rs b/crates/bitcoin/src/types.rs index 52669e30be..e39cfb37fa 100644 --- a/crates/bitcoin/src/types.rs +++ b/crates/bitcoin/src/types.rs @@ -234,12 +234,23 @@ impl sp_std::fmt::Debug for RawBlockHeader { // Constants pub const P2PKH_SCRIPT_SIZE: u32 = 25; pub const P2SH_SCRIPT_SIZE: u32 = 23; -pub const P2WPKH_V0_SCRIPT_SIZE: u32 = 22; -pub const P2WSH_V0_SCRIPT_SIZE: u32 = 34; pub const HASH160_SIZE_HEX: u8 = 0x14; pub const HASH256_SIZE_HEX: u8 = 0x20; pub const MAX_OPRETURN_SIZE: usize = 83; +/** Signature hash sizes */ +pub const WITNESS_V0_SCRIPTHASH_SIZE: usize = 32; +pub const WITNESS_V0_KEYHASH_SIZE: usize = 20; +pub const WITNESS_V1_TAPROOT_SIZE: usize = 32; + +// https://github.com/bitcoin/bitcoin/blob/623745ca74cf3f54b474dac106f5802b7929503f/src/policy/policy.h#L40 +pub const MAX_STANDARD_P2WSH_STACK_ITEMS: usize = 100; +// https://github.com/bitcoin/bitcoin/blob/623745ca74cf3f54b474dac106f5802b7929503f/src/policy/policy.h#L46 +pub const MAX_STANDARD_P2WSH_SCRIPT_SIZE: usize = 3600; + +// https://github.com/bitcoin/bitcoin/blob/d492dc1cdaabdc52b0766bf4cba4bd73178325d0/src/script/script.h#L54 +pub const ANNEX_TAG: u8 = 0x50; + /// Structs /// Bitcoin Basic Block Headers @@ -1111,6 +1122,23 @@ mod tests { assert_eq!(&extr_address, &address); } + #[test] + fn extract_address_p2tr_output() { + // 33e794d097969002ee05d336686fc03c9e15a597c1b9827669460fac98799036 + let raw_tx = "01000000000101d1f1c1f8cdf6759167b90f52c9ad358a369f95284e841d7a2536cef31c0549580100000000fdffffff020000000000000000316a2f49206c696b65205363686e6f7272207369677320616e6420492063616e6e6f74206c69652e204062697462756734329e06010000000000225120a37c3903c8d0db6512e2b40b0dffa05e5a3ab73603ce8c9c4b7771e5412328f90140a60c383f71bac0ec919b1d7dbc3eb72dd56e7aa99583615564f9f99b8ae4e837b758773a5b2e4c51348854c8389f008e05029db7f464a5ff2e01d5e6e626174affd30a00"; + let tx_bytes = hex::decode(&raw_tx).unwrap(); + let transaction = parse_transaction(&tx_bytes).unwrap(); + + let address = Address::P2TRv1(H256([ + 163, 124, 57, 3, 200, 208, 219, 101, 18, 226, 180, 11, 13, 255, 160, 94, 90, 58, 183, 54, 3, 206, 140, 156, + 75, 119, 113, 229, 65, 35, 40, 249, + ])); + + let extr_address = transaction.outputs[1].extract_address().unwrap(); + + assert_eq!(&extr_address, &address); + } + #[test] fn p2pk_not_allowed() { // source: https://blockstream.info/tx/f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16?expand From 99f74d273ea1e932e7dcdb7c50fe0b86a8e49a32 Mon Sep 17 00:00:00 2001 From: Gregory Hill Date: Wed, 2 Aug 2023 15:43:23 +0100 Subject: [PATCH 2/2] test: convert addresses Signed-off-by: Gregory Hill --- crates/bitcoin/src/address.rs | 48 +++++++++++++++++++++++++++++++++++ crates/bitcoin/src/script.rs | 16 ++++-------- crates/bitcoin/src/types.rs | 10 +++++--- 3 files changed, 60 insertions(+), 14 deletions(-) diff --git a/crates/bitcoin/src/address.rs b/crates/bitcoin/src/address.rs index cf8452cde8..4deeee5653 100644 --- a/crates/bitcoin/src/address.rs +++ b/crates/bitcoin/src/address.rs @@ -413,4 +413,52 @@ mod tests { ]) ); } + + #[test] + fn test_convert_address_script() { + // 1MsmX1jpgyJY3h8det2VZz9NYXs6WhpjdT + let script = Script { + bytes: hex::decode("76a914e4fc799e2e718d64064af4cd15b2a6c11780fe2a88ac").unwrap(), + }; + assert!(script.is_p2pkh()); + let address = Address::from_script_pub_key(&script).unwrap(); + assert!(matches!(address, Address::P2PKH(_))); + assert_eq!(script, address.to_script_pub_key()); + + // 3NZbxHNESLkkAPCaTgrgSZQgkmhnv2cdxz + let script = Script { + bytes: hex::decode("a914e4f3b8771c0eff8645a9669eef1fb1ea0cf1dec187").unwrap(), + }; + assert!(script.is_p2sh()); + let address = Address::from_script_pub_key(&script).unwrap(); + assert!(matches!(address, Address::P2SH(_))); + assert_eq!(script, address.to_script_pub_key()); + + // bc1q4m304aj7c3xcxaqdz9kl6axnex2gkufmh7rsqw + let script = Script { + bytes: hex::decode("0014aee2faf65ec44d83740d116dfd74d3c9948b713b").unwrap(), + }; + assert!(script.is_p2wpkh_v0()); + let address = Address::from_script_pub_key(&script).unwrap(); + assert!(matches!(address, Address::P2WPKHv0(_))); + assert_eq!(script, address.to_script_pub_key()); + + // bc1qgdjqv0av3q56jvd82tkdjpy7gdp9ut8tlqmgrpmv24sq90ecnvqqjwvw97 + let script = Script { + bytes: hex::decode("00204364063fac8829a931a752ecd9049e43425e2cebf83681876c556002bf389b00").unwrap(), + }; + assert!(script.is_p2wsh_v0()); + let address = Address::from_script_pub_key(&script).unwrap(); + assert!(matches!(address, Address::P2WSHv0(_))); + assert_eq!(script, address.to_script_pub_key()); + + // bc1pq2cealz0zkvse0sxus2hwx8jquchtyvs64v4cwqnelrhs3helunsxyral2 + let script = Script { + bytes: hex::decode("512002b19efc4f15990cbe06e4157718f20731759190d5595c3813cfc77846f9ff27").unwrap(), + }; + assert!(script.is_p2tr_v1()); + let address = Address::from_script_pub_key(&script).unwrap(); + assert!(matches!(address, Address::P2TRv1(_))); + assert_eq!(script, address.to_script_pub_key()); + } } diff --git a/crates/bitcoin/src/script.rs b/crates/bitcoin/src/script.rs index b4e027e4e7..d4b550558d 100644 --- a/crates/bitcoin/src/script.rs +++ b/crates/bitcoin/src/script.rs @@ -58,27 +58,21 @@ impl Script { pub fn is_p2wpkh_v0(&self) -> bool { // first byte is version - self.len() == WITNESS_V0_KEYHASH_SIZE + 2 as usize - && self.bytes[0] == OpCode::Op0 as u8 - && self.bytes[1] == HASH160_SIZE_HEX + self.len() == P2WPKH_V0_SCRIPT_SIZE && self.bytes[0] == OpCode::Op0 as u8 && self.bytes[1] == HASH160_SIZE_HEX } pub fn is_p2wsh_v0(&self) -> bool { // first byte is version - self.len() == WITNESS_V0_SCRIPTHASH_SIZE + 2 as usize - && self.bytes[0] == OpCode::Op0 as u8 - && self.bytes[1] == HASH256_SIZE_HEX + self.len() == P2WSH_V0_SCRIPT_SIZE && self.bytes[0] == OpCode::Op0 as u8 && self.bytes[1] == HASH256_SIZE_HEX } pub fn is_p2tr_v1(&self) -> bool { // first byte is version - self.len() == WITNESS_V1_TAPROOT_SIZE + 2 as usize - && self.bytes[0] == OpCode::Op1 as u8 - && self.bytes[1] == HASH256_SIZE_HEX + self.len() == P2TR_V1_SCRIPT_SIZE && self.bytes[0] == OpCode::Op1 as u8 && self.bytes[1] == HASH256_SIZE_HEX } pub fn is_p2pkh(&self) -> bool { - self.len() == P2PKH_SCRIPT_SIZE as usize + self.len() == P2PKH_SCRIPT_SIZE && self.bytes[0] == OpCode::OpDup as u8 && self.bytes[1] == OpCode::OpHash160 as u8 && self.bytes[2] == HASH160_SIZE_HEX @@ -87,7 +81,7 @@ impl Script { } pub fn is_p2sh(&self) -> bool { - self.len() == P2SH_SCRIPT_SIZE as usize + self.len() == P2SH_SCRIPT_SIZE && self.bytes[0] == OpCode::OpHash160 as u8 && self.bytes[1] == HASH160_SIZE_HEX && self.bytes[22] == OpCode::OpEqual as u8 diff --git a/crates/bitcoin/src/types.rs b/crates/bitcoin/src/types.rs index c2421f14e9..5a4455b3e0 100644 --- a/crates/bitcoin/src/types.rs +++ b/crates/bitcoin/src/types.rs @@ -169,18 +169,22 @@ pub enum OpCode { /// Custom Types // Constants -pub const P2PKH_SCRIPT_SIZE: u32 = 25; -pub const P2SH_SCRIPT_SIZE: u32 = 23; +pub const P2PKH_SCRIPT_SIZE: usize = 25; +pub const P2SH_SCRIPT_SIZE: usize = 23; pub const HASH160_SIZE_HEX: u8 = 0x14; pub const HASH256_SIZE_HEX: u8 = 0x20; // TODO: reduce to H256 size + op code pub const MAX_OPRETURN_SIZE: usize = 83; // https://github.com/bitcoin/bitcoin/blob/2fa60f0b683cefd7956273986dafe3bde00c98fd/src/script/interpreter.h#L225-L227 -pub const WITNESS_V0_SCRIPTHASH_SIZE: usize = 32; pub const WITNESS_V0_KEYHASH_SIZE: usize = 20; +pub const WITNESS_V0_SCRIPTHASH_SIZE: usize = 32; pub const WITNESS_V1_TAPROOT_SIZE: usize = 32; +pub const P2WPKH_V0_SCRIPT_SIZE: usize = WITNESS_V0_KEYHASH_SIZE + 2; +pub const P2WSH_V0_SCRIPT_SIZE: usize = WITNESS_V0_SCRIPTHASH_SIZE + 2; +pub const P2TR_V1_SCRIPT_SIZE: usize = WITNESS_V1_TAPROOT_SIZE + 2; + /// Structs /// Bitcoin Basic Block Headers