diff --git a/crates/bitcoin/src/address.rs b/crates/bitcoin/src/address.rs index 28809257cd..4deeee5653 100644 --- a/crates/bitcoin/src/address.rs +++ b/crates/bitcoin/src/address.rs @@ -16,8 +16,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} @@ -25,13 +25,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 { @@ -42,6 +48,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; const MAX_ADDRESS_BYTES: usize = HASH256_SIZE_HEX as usize + 2; // max length is for P2WSHv0; see the match below let bytes = script.as_bytes(); @@ -67,6 +74,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), } } @@ -105,6 +115,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 + } } } @@ -123,7 +140,7 @@ impl Address { pub fn is_zero(&self) -> bool { match self { Self::P2PKH(hash) | Self::P2SH(hash) | Self::P2WPKHv0(hash) => hash.is_zero(), - Self::P2WSHv0(hash) => hash.is_zero(), + Self::P2WSHv0(hash) | Self::P2TRv1(hash) => hash.is_zero(), } } } @@ -396,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 6fc9510323..d4b550558d 100644 --- a/crates/bitcoin/src/script.rs +++ b/crates/bitcoin/src/script.rs @@ -58,20 +58,21 @@ impl Script { pub fn is_p2wpkh_v0(&self) -> bool { // first byte is version - self.len() == P2WPKH_V0_SCRIPT_SIZE 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() == P2WSH_V0_SCRIPT_SIZE 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() == 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 @@ -80,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 42e9f01dfc..5a4455b3e0 100644 --- a/crates/bitcoin/src/types.rs +++ b/crates/bitcoin/src/types.rs @@ -169,15 +169,22 @@ pub enum OpCode { /// Custom Types // 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 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_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 @@ -1143,6 +1150,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