Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support taproot addresses #607

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
75 changes: 70 additions & 5 deletions crates/bitcoin/src/address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,28 @@ 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}
// output: OP_HASH160 {hash160(redeem_script)} OP_EQUAL
// 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 {
Expand All @@ -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();
Expand All @@ -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),
}
}
Expand Down Expand Up @@ -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
}
}
}

Expand All @@ -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(),
}
}
}
Expand Down Expand Up @@ -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());
}
}
17 changes: 9 additions & 8 deletions crates/bitcoin/src/script.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
32 changes: 28 additions & 4 deletions crates/bitcoin/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down