use std::io::Cursor;
use bitcoin::{
ecdsa::Signature as EcdsaSignature,
opcodes,
script::Instruction,
sighash::{EcdsaSighashType, TapSighashType},
taproot::Signature as SchnorrSignature,
Address, Network, Script, ScriptBuf, TxIn, Txid,
};
use ciborium::de::from_reader;
use xxhash_rust::xxh3::xxh3_128;
use crate::types::{Amount, UtxoKey};
pub fn extract_address(script: &Script, network: Network) -> Option<String> {
Address::from_script(script, network)
.ok()
.map(|addr| addr.to_string())
}
fn looks_like_der_signature(data: &[u8]) -> bool {
(9..=74).contains(&data.len()) && data.first() == Some(&0x30)
}
pub fn compute_utxo_key(txid: &Txid, vout: u32) -> UtxoKey {
let mut payload = [0u8; 36];
payload[..32].copy_from_slice(txid.as_ref());
payload[32..].copy_from_slice(&vout.to_le_bytes());
let hash = xxh3_128(&payload).to_le_bytes();
let mut key = [0u8; 12];
key.copy_from_slice(&hash[..12]);
key
}
pub fn leading_zero_count(txid: &Txid) -> u8 {
let mut count: u8 = 0;
let bytes: &[u8] = txid.as_ref();
for &byte in bytes.iter().rev() {
if byte == 0 {
count += 2;
continue;
}
if byte >> 4 == 0 {
count += 1;
}
break;
}
count
}
pub fn parse_op_return(script: &ScriptBuf, prefix: &[u8]) -> Option<Vec<u64>> {
let mut instructions = script.instructions();
let op_return = instructions.next()?;
match op_return.ok()? {
Instruction::Op(opcodes::all::OP_RETURN) => {}
_ => return None,
}
let push = instructions.next()?;
let data = match push.ok()? {
Instruction::PushBytes(bytes) => bytes.as_bytes(),
_ => return None,
};
if data.len() < prefix.len() || &data[..prefix.len()] != prefix {
return None;
}
let mut reader = Cursor::new(&data[prefix.len()..]);
from_reader::<Vec<u64>, _>(&mut reader).ok()
}
pub fn calculate_reward(
zero_count: u8,
max_zero_count: u8,
min_zero_count: u8,
base_reward: Amount,
) -> Amount {
if zero_count < min_zero_count {
return 0;
}
let diff = max_zero_count.saturating_sub(zero_count);
let mut reward = base_reward;
for _ in 0..diff {
reward /= 16;
if reward == 0 {
break;
}
}
reward
}
fn is_ecdsa_sighash_all(sig_bytes: &[u8]) -> bool {
EcdsaSignature::from_slice(sig_bytes)
.map(|sig| sig.sighash_type == EcdsaSighashType::All)
.unwrap_or(false)
}
fn is_schnorr_sighash_all(sig_bytes: &[u8]) -> bool {
SchnorrSignature::from_slice(sig_bytes)
.map(|sig| {
matches!(
sig.sighash_type,
TapSighashType::Default | TapSighashType::All
)
})
.unwrap_or(false)
}
fn extract_scriptsig_signatures(script_sig: &bitcoin::Script) -> Vec<&[u8]> {
let mut signatures = Vec::new();
for instruction in script_sig.instructions().flatten() {
if let Instruction::PushBytes(bytes) = instruction {
let data = bytes.as_bytes();
if data.len() >= 9 && data.len() <= 74 && data.first() == Some(&0x30) {
signatures.push(data);
}
}
}
signatures
}
pub fn all_inputs_sighash_all(inputs: &[TxIn]) -> bool {
for input in inputs {
let has_witness = !input.witness.is_empty();
let has_scriptsig = !input.script_sig.is_empty();
if has_witness {
let witness_len = input.witness.len();
let first_elem = input.witness.nth(0).unwrap_or(&[]);
if witness_len == 1 && (first_elem.len() == 64 || first_elem.len() == 65) {
if !is_schnorr_sighash_all(first_elem) {
return false;
}
} else if witness_len == 2 && (first_elem.len() == 64 || first_elem.len() == 65) {
if !is_schnorr_sighash_all(first_elem) {
return false;
}
} else if witness_len == 2 && looks_like_der_signature(first_elem) {
if !is_ecdsa_sighash_all(first_elem) {
return false;
}
} else if witness_len > 2 {
let last_elem = input.witness.nth(witness_len - 1).unwrap_or(&[]);
if !last_elem.is_empty() && (last_elem[0] & 0xfe) == 0xc0 {
let mut saw_signature = false;
for i in 0..witness_len.saturating_sub(2) {
let elem = input.witness.nth(i).unwrap_or(&[]);
if elem.len() == 64 || elem.len() == 65 {
saw_signature = true;
if !is_schnorr_sighash_all(elem) {
return false;
}
}
}
if !saw_signature {
return false;
}
} else {
let mut saw_signature = false;
for i in 0..witness_len.saturating_sub(1) {
let elem = input.witness.nth(i).unwrap_or(&[]);
if elem.is_empty() {
continue;
}
if elem.len() >= 9 && elem.first() == Some(&0x30) {
saw_signature = true;
if !is_ecdsa_sighash_all(elem) {
return false;
}
}
}
if !saw_signature {
return false;
}
}
} else {
return false;
}
} else if has_scriptsig {
let signatures = extract_scriptsig_signatures(&input.script_sig);
if signatures.is_empty() {
return false;
}
for sig_bytes in signatures {
if !is_ecdsa_sighash_all(sig_bytes) {
return false;
}
}
} else {
return false;
}
}
true
}
#[cfg(test)]
mod tests {
use super::*;
use bitcoin::{
hashes::Hash, opcodes, script::PushBytesBuf, Network, OutPoint, ScriptBuf, Sequence, Txid,
Witness,
};
use ciborium::ser::into_writer;
use std::str::FromStr;
fn txid_from_hex(hex: &str) -> Txid {
Txid::from_str(hex).expect("invalid txid hex")
}
fn txid_from_bytes(bytes: [u8; 32]) -> Txid {
Txid::from_slice(&bytes).expect("invalid txid bytes")
}
fn build_op_return_script(data: &[u8]) -> ScriptBuf {
let push = PushBytesBuf::try_from(data.to_vec()).expect("invalid push data length");
ScriptBuf::builder()
.push_opcode(opcodes::all::OP_RETURN)
.push_slice(push)
.into_script()
}
fn encode_values(values: &[u64]) -> Vec<u8> {
let mut encoded = Vec::new();
into_writer(values, &mut encoded).expect("failed to encode cbor");
encoded
}
#[test]
fn compute_utxo_key_varies_with_inputs() {
let txid_a =
txid_from_hex("0101010101010101010101010101010101010101010101010101010101010101");
let txid_b =
txid_from_hex("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
let key_a0 = compute_utxo_key(&txid_a, 0);
let key_a1 = compute_utxo_key(&txid_a, 1);
let key_b0 = compute_utxo_key(&txid_b, 0);
assert_eq!(key_a0.len(), 12);
assert_ne!(key_a0, key_a1);
assert_ne!(key_a0, key_b0);
}
#[test]
fn leading_zero_count_handles_full_and_partial_bytes() {
let all_zero = txid_from_bytes([0u8; 32]);
assert_eq!(leading_zero_count(&all_zero), 64);
let mut half = [0xffu8; 32];
half[31] = 0x0f;
let half_byte = txid_from_bytes(half);
assert_eq!(leading_zero_count(&half_byte), 1);
let mut bytes = [0xffu8; 32];
bytes[31] = 0xf0;
let non_zero = txid_from_bytes(bytes);
assert_eq!(leading_zero_count(&non_zero), 0);
}
#[test]
fn leading_zero_count_ignores_trailing_zero_bytes() {
let mut bytes = [0xffu8; 32];
bytes[0] = 0x00;
let txid = txid_from_bytes(bytes);
assert_eq!(leading_zero_count(&txid), 0);
}
#[test]
fn parse_op_return_succeeds_with_valid_prefix_and_cbor() {
const PREFIX: &[u8] = b"ZELD";
let mut payload = PREFIX.to_vec();
payload.extend(encode_values(&[1, 2, 3]));
let script = build_op_return_script(&payload);
let parsed = parse_op_return(&script, PREFIX).expect("expected values");
assert_eq!(parsed, vec![1, 2, 3]);
}
#[test]
fn parse_op_return_rejects_missing_op_return() {
let script = ScriptBuf::builder().push_slice(b"data").into_script();
assert!(parse_op_return(&script, b"ZELD").is_none());
}
#[test]
fn parse_op_return_rejects_non_push_instruction() {
let script = ScriptBuf::builder()
.push_opcode(opcodes::all::OP_RETURN)
.push_opcode(opcodes::all::OP_ADD)
.into_script();
assert!(parse_op_return(&script, b"ZELD").is_none());
}
#[test]
fn parse_op_return_enforces_prefix_length() {
let script = build_op_return_script(b"\x01");
assert!(parse_op_return(&script, b"ZELD").is_none());
}
#[test]
fn parse_op_return_enforces_prefix_match() {
const PREFIX: &[u8] = b"ZELD";
let mut payload = b"BADP".to_vec();
payload.extend(encode_values(&[9]));
let script = build_op_return_script(&payload);
assert!(parse_op_return(&script, PREFIX).is_none());
}
#[test]
fn parse_op_return_rejects_invalid_cbor() {
const PREFIX: &[u8] = b"ZELD";
let mut payload = PREFIX.to_vec();
payload.extend([0xff, 0x00]);
let script = build_op_return_script(&payload);
assert!(parse_op_return(&script, PREFIX).is_none());
}
#[test]
fn parse_op_return_rejects_empty_script() {
let script = ScriptBuf::new();
assert!(parse_op_return(&script, b"ZELD").is_none());
}
#[test]
fn parse_op_return_bubbles_error_before_op_return() {
let bytes = vec![opcodes::all::OP_PUSHDATA1.to_u8(), 0x02];
let script = ScriptBuf::from_bytes(bytes);
assert!(parse_op_return(&script, b"ZELD").is_none());
}
#[test]
fn parse_op_return_requires_push_after_op_return() {
let script = ScriptBuf::builder()
.push_opcode(opcodes::all::OP_RETURN)
.into_script();
assert!(parse_op_return(&script, b"ZELD").is_none());
}
#[test]
fn parse_op_return_bubbles_error_after_op_return() {
let bytes = vec![
opcodes::all::OP_RETURN.to_u8(),
opcodes::all::OP_PUSHDATA1.to_u8(),
0x01,
];
let script = ScriptBuf::from_bytes(bytes);
assert!(parse_op_return(&script, b"ZELD").is_none());
}
#[test]
fn calculate_reward_returns_zero_when_below_min() {
assert_eq!(calculate_reward(1, 5, 2, 1_000), 0);
}
#[test]
fn calculate_reward_scales_by_zero_difference() {
let reward = calculate_reward(5, 5, 0, 1_000);
assert_eq!(reward, 1_000);
let scaled = calculate_reward(4, 5, 0, 1_000);
assert_eq!(scaled, 62);
}
#[test]
fn calculate_reward_exhausts_to_zero() {
let reward = calculate_reward(0, 10, 0, 1);
assert_eq!(reward, 0);
}
fn make_witness_input(witness_elements: Vec<Vec<u8>>) -> TxIn {
let mut witness = Witness::new();
for elem in witness_elements {
witness.push(elem);
}
TxIn {
previous_output: OutPoint::null(),
script_sig: ScriptBuf::new(),
sequence: Sequence::MAX,
witness,
}
}
fn make_scriptsig_input(script_sig: ScriptBuf) -> TxIn {
TxIn {
previous_output: OutPoint::null(),
script_sig,
sequence: Sequence::MAX,
witness: Witness::new(),
}
}
fn make_ecdsa_sig_sighash_all() -> Vec<u8> {
let mut sig = vec![
0x30, 0x44, 0x02, 0x20, ];
sig.extend([0x01; 32]); sig.extend([
0x02, 0x20, ]);
sig.extend([0x02; 32]); sig.push(0x01); sig
}
fn make_ecdsa_sig_sighash_none() -> Vec<u8> {
let mut sig = make_ecdsa_sig_sighash_all();
*sig.last_mut().unwrap() = 0x02; sig
}
fn make_short_ecdsa_sig_sighash_all() -> Vec<u8> {
vec![0x30, 0x06, 0x02, 0x01, 0x01, 0x02, 0x01, 0x01, 0x01]
}
fn make_short_malformed_der() -> Vec<u8> {
vec![0x30, 0x02, 0x01, 0x01, 0x01] }
fn make_schnorr_sig_default() -> Vec<u8> {
vec![0x01; 64]
}
fn make_schnorr_sig_sighash_all() -> Vec<u8> {
let mut sig = vec![0x01; 64];
sig.push(0x01); sig
}
fn make_schnorr_sig_sighash_none() -> Vec<u8> {
let mut sig = vec![0x01; 64];
sig.push(0x02); sig
}
fn make_pubkey() -> Vec<u8> {
let mut pk = vec![0x02]; pk.extend([0xab; 32]);
pk
}
#[test]
fn all_inputs_sighash_all_accepts_p2wpkh_with_sighash_all() {
let sig = make_ecdsa_sig_sighash_all();
let pubkey = make_pubkey();
let input = make_witness_input(vec![sig, pubkey]);
assert!(all_inputs_sighash_all(&[input]));
}
#[test]
fn all_inputs_sighash_all_accepts_p2wpkh_with_short_valid_der() {
let sig = make_short_ecdsa_sig_sighash_all();
let pubkey = make_pubkey();
let input = make_witness_input(vec![sig, pubkey]);
assert!(all_inputs_sighash_all(&[input]));
}
#[test]
fn all_inputs_sighash_all_rejects_p2wpkh_with_short_malformed_der() {
let sig = make_short_malformed_der();
let pubkey = make_pubkey();
let input = make_witness_input(vec![sig, pubkey]);
assert!(!all_inputs_sighash_all(&[input]));
}
#[test]
fn all_inputs_sighash_all_rejects_p2wpkh_with_sighash_none() {
let sig = make_ecdsa_sig_sighash_none();
let pubkey = make_pubkey();
let input = make_witness_input(vec![sig, pubkey]);
assert!(!all_inputs_sighash_all(&[input]));
}
#[test]
fn all_inputs_sighash_all_accepts_taproot_with_sighash_default() {
let sig = make_schnorr_sig_default();
let input = make_witness_input(vec![sig]);
assert!(all_inputs_sighash_all(&[input]));
}
#[test]
fn all_inputs_sighash_all_accepts_taproot_with_sighash_all() {
let sig = make_schnorr_sig_sighash_all();
let input = make_witness_input(vec![sig]);
assert!(all_inputs_sighash_all(&[input]));
}
#[test]
fn all_inputs_sighash_all_rejects_taproot_with_sighash_none() {
let sig = make_schnorr_sig_sighash_none();
let input = make_witness_input(vec![sig]);
assert!(!all_inputs_sighash_all(&[input]));
}
#[test]
fn all_inputs_sighash_all_rejects_empty_witness() {
let input = make_witness_input(vec![]);
assert!(!all_inputs_sighash_all(&[input]));
}
#[test]
fn all_inputs_sighash_all_rejects_empty_input() {
let input = TxIn {
previous_output: OutPoint::null(),
script_sig: ScriptBuf::new(),
sequence: Sequence::MAX,
witness: Witness::new(),
};
assert!(!all_inputs_sighash_all(&[input]));
}
#[test]
fn all_inputs_sighash_all_accepts_p2pkh_with_sighash_all() {
let sig = make_ecdsa_sig_sighash_all();
let pubkey = make_pubkey();
let script_sig = ScriptBuf::builder()
.push_slice(PushBytesBuf::try_from(sig).unwrap())
.push_slice(PushBytesBuf::try_from(pubkey).unwrap())
.into_script();
let input = make_scriptsig_input(script_sig);
assert!(all_inputs_sighash_all(&[input]));
}
#[test]
fn all_inputs_sighash_all_rejects_p2pkh_with_sighash_none() {
let sig = make_ecdsa_sig_sighash_none();
let pubkey = make_pubkey();
let script_sig = ScriptBuf::builder()
.push_slice(PushBytesBuf::try_from(sig).unwrap())
.push_slice(PushBytesBuf::try_from(pubkey).unwrap())
.into_script();
let input = make_scriptsig_input(script_sig);
assert!(!all_inputs_sighash_all(&[input]));
}
#[test]
fn all_inputs_sighash_all_accepts_multiple_valid_inputs() {
let sig1 = make_ecdsa_sig_sighash_all();
let pubkey1 = make_pubkey();
let input1 = make_witness_input(vec![sig1, pubkey1]);
let sig2 = make_schnorr_sig_default();
let input2 = make_witness_input(vec![sig2]);
assert!(all_inputs_sighash_all(&[input1, input2]));
}
#[test]
fn all_inputs_sighash_all_rejects_if_any_input_invalid() {
let sig1 = make_ecdsa_sig_sighash_all();
let pubkey1 = make_pubkey();
let input1 = make_witness_input(vec![sig1, pubkey1]);
let sig2 = make_schnorr_sig_sighash_none();
let input2 = make_witness_input(vec![sig2]);
assert!(!all_inputs_sighash_all(&[input1, input2]));
}
#[test]
fn all_inputs_sighash_all_returns_true_for_empty_inputs() {
assert!(all_inputs_sighash_all(&[]));
}
#[test]
fn all_inputs_sighash_all_accepts_p2wsh_multisig_with_sighash_all() {
let sig1 = make_ecdsa_sig_sighash_all();
let sig2 = make_ecdsa_sig_sighash_all();
let redeem_script = vec![0x52, 0x21];
let input = make_witness_input(vec![vec![], sig1, sig2, redeem_script]);
assert!(all_inputs_sighash_all(&[input]));
}
#[test]
fn all_inputs_sighash_all_rejects_p2wsh_multisig_with_mixed_sighash() {
let sig1 = make_ecdsa_sig_sighash_all();
let sig2 = make_ecdsa_sig_sighash_none();
let redeem_script = vec![0x52, 0x21];
let input = make_witness_input(vec![vec![], sig1, sig2, redeem_script]);
assert!(!all_inputs_sighash_all(&[input]));
}
#[test]
fn all_inputs_sighash_all_accepts_taproot_with_annex() {
let sig = make_schnorr_sig_sighash_all();
let annex = vec![0x50, 0x01, 0x02]; let input = make_witness_input(vec![sig, annex]);
assert!(all_inputs_sighash_all(&[input]));
}
#[test]
fn all_inputs_sighash_all_rejects_taproot_with_annex_and_sighash_none() {
let sig = make_schnorr_sig_sighash_none();
let annex = vec![0x50, 0x01, 0x02];
let input = make_witness_input(vec![sig, annex]);
assert!(!all_inputs_sighash_all(&[input]));
}
#[test]
fn all_inputs_sighash_all_accepts_taproot_script_path_with_valid_sigs() {
let sig = make_schnorr_sig_sighash_all();
let script = vec![0x51]; let control_block = vec![0xc0, 0x01, 0x02, 0x03];
let input = make_witness_input(vec![sig, script, control_block]);
assert!(all_inputs_sighash_all(&[input]));
}
#[test]
fn all_inputs_sighash_all_rejects_taproot_script_path_with_invalid_sig() {
let sig = make_schnorr_sig_sighash_none();
let script = vec![0x51]; let control_block = vec![0xc1, 0x01, 0x02, 0x03];
let input = make_witness_input(vec![sig, script, control_block]);
assert!(!all_inputs_sighash_all(&[input]));
}
#[test]
fn all_inputs_sighash_all_rejects_taproot_script_path_without_signatures() {
let stack_value = vec![0x01, 0x02, 0x03]; let script = vec![0x51]; let control_block = vec![0xc0, 0x01, 0x02, 0x03];
let input = make_witness_input(vec![stack_value, script, control_block]);
assert!(!all_inputs_sighash_all(&[input]));
}
#[test]
fn all_inputs_sighash_all_rejects_p2wsh_without_signatures() {
let placeholder = vec![]; let data = vec![0x01, 0x02, 0x03]; let redeem_script = vec![0x51];
let input = make_witness_input(vec![placeholder, data, redeem_script]);
assert!(!all_inputs_sighash_all(&[input]));
}
#[test]
fn all_inputs_sighash_all_rejects_unknown_witness_structure() {
let unknown_data = vec![0x01, 0x02, 0x03]; let input = make_witness_input(vec![unknown_data]);
assert!(!all_inputs_sighash_all(&[input]));
}
#[test]
fn all_inputs_sighash_all_rejects_scriptsig_without_signatures() {
let pubkey = make_pubkey();
let script_sig = ScriptBuf::builder()
.push_slice(PushBytesBuf::try_from(pubkey).unwrap())
.into_script();
let input = make_scriptsig_input(script_sig);
assert!(!all_inputs_sighash_all(&[input]));
}
#[test]
fn all_inputs_sighash_all_ignores_non_signature_pushes_in_scriptsig() {
let sig = make_ecdsa_sig_sighash_all();
let pubkey = make_pubkey();
let extra_data = vec![0x01, 0x02, 0x03];
let script_sig = ScriptBuf::builder()
.push_slice(PushBytesBuf::try_from(sig).unwrap())
.push_slice(PushBytesBuf::try_from(pubkey).unwrap())
.push_slice(PushBytesBuf::try_from(extra_data).unwrap())
.into_script();
let input = make_scriptsig_input(script_sig);
assert!(all_inputs_sighash_all(&[input]));
}
#[test]
fn all_inputs_sighash_all_handles_scriptsig_with_opcodes() {
let sig = make_ecdsa_sig_sighash_all();
let pubkey = make_pubkey();
let script_sig = ScriptBuf::builder()
.push_slice(PushBytesBuf::try_from(sig).unwrap())
.push_slice(PushBytesBuf::try_from(pubkey).unwrap())
.push_opcode(opcodes::all::OP_CHECKSIG)
.into_script();
let input = make_scriptsig_input(script_sig);
assert!(all_inputs_sighash_all(&[input]));
}
#[test]
fn extract_address_returns_none_for_op_return() {
let script = build_op_return_script(b"ZELD");
assert!(extract_address(&script, Network::Bitcoin).is_none());
}
#[test]
fn extract_address_returns_none_for_unknown_script() {
let script = ScriptBuf::builder()
.push_opcode(opcodes::all::OP_CHECKSIG)
.into_script();
assert!(extract_address(&script, Network::Bitcoin).is_none());
}
#[test]
fn extract_address_returns_address_for_p2pkh() {
let hash = [0xab; 20];
let script = ScriptBuf::builder()
.push_opcode(opcodes::all::OP_DUP)
.push_opcode(opcodes::all::OP_HASH160)
.push_slice(hash)
.push_opcode(opcodes::all::OP_EQUALVERIFY)
.push_opcode(opcodes::all::OP_CHECKSIG)
.into_script();
let addr = extract_address(&script, Network::Bitcoin);
assert!(addr.is_some());
assert!(addr.unwrap().starts_with('1')); }
#[test]
fn extract_address_returns_address_for_p2sh() {
let hash = [0xcd; 20];
let script = ScriptBuf::builder()
.push_opcode(opcodes::all::OP_HASH160)
.push_slice(hash)
.push_opcode(opcodes::all::OP_EQUAL)
.into_script();
let addr = extract_address(&script, Network::Bitcoin);
assert!(addr.is_some());
assert!(addr.unwrap().starts_with('3')); }
#[test]
fn extract_address_returns_address_for_p2wpkh() {
let hash = [0xef; 20];
let script = ScriptBuf::builder()
.push_opcode(opcodes::OP_0)
.push_slice(hash)
.into_script();
let addr = extract_address(&script, Network::Bitcoin);
assert!(addr.is_some());
assert!(addr.unwrap().starts_with("bc1q")); }
#[test]
fn extract_address_returns_address_for_p2wsh() {
let hash = [0x12; 32];
let script = ScriptBuf::builder()
.push_opcode(opcodes::OP_0)
.push_slice(hash)
.into_script();
let addr = extract_address(&script, Network::Bitcoin);
assert!(addr.is_some());
assert!(addr.unwrap().starts_with("bc1q")); }
#[test]
fn extract_address_returns_address_for_p2tr() {
let pubkey = [0x34; 32];
let script = ScriptBuf::builder()
.push_opcode(opcodes::all::OP_PUSHNUM_1)
.push_slice(pubkey)
.into_script();
let addr = extract_address(&script, Network::Bitcoin);
assert!(addr.is_some());
assert!(addr.unwrap().starts_with("bc1p")); }
#[test]
fn extract_address_uses_correct_network_prefix() {
let hash = [0xef; 20];
let script = ScriptBuf::builder()
.push_opcode(opcodes::OP_0)
.push_slice(hash)
.into_script();
let mainnet_addr = extract_address(&script, Network::Bitcoin).unwrap();
let testnet_addr = extract_address(&script, Network::Testnet4).unwrap();
assert!(mainnet_addr.starts_with("bc1"));
assert!(testnet_addr.starts_with("tb1"));
}
}