use bitcoin::key::{Secp256k1, UntweakedPublicKey};
use bitcoin::secp256k1::XOnlyPublicKey;
use bitcoin::taproot::{LeafVersion, TaprootBuilder, TaprootSpendInfo};
use bitcoin::{Address, KnownHrp, ScriptBuf};
use serde_json::Value;
const FIXTURE: &str = include_str!("vectors/bip341-wallet-test-vectors.json");
const FIXTURE_SHA256: &str = "403e19fb81dd1f31e745699216308f61fb403774b2aafa87b631b8f7c042d37f";
fn vectors() -> Vec<Value> {
let root: Value = serde_json::from_str(FIXTURE).expect("fixture parses");
root["scriptPubKey"]
.as_array()
.expect("scriptPubKey array")
.clone()
}
#[test]
fn fixture_sha256_pin() {
use bitcoin::hashes::{Hash, sha256};
let actual = sha256::Hash::hash(FIXTURE.as_bytes()).to_string();
assert_eq!(
actual, FIXTURE_SHA256,
"BIP-341 fixture SHA drifted; vectors may have changed upstream — \
re-audit before bumping FIXTURE_SHA256",
);
}
#[test]
fn scriptpubkey_array_length_is_seven() {
assert_eq!(vectors().len(), 7);
}
fn walk_tree(tree: &Value, depth: u8, leaves: &mut Vec<(u8, ScriptBuf, LeafVersion)>) {
if tree.is_null() {
return;
}
if let Some(arr) = tree.as_array() {
assert_eq!(arr.len(), 2, "branch nodes must have exactly 2 children");
walk_tree(&arr[0], depth + 1, leaves);
walk_tree(&arr[1], depth + 1, leaves);
return;
}
let obj = tree.as_object().expect("leaf is an object");
let script_hex = obj["script"].as_str().expect("script is hex string");
let leaf_ver_u8 = obj["leafVersion"].as_u64().expect("leafVersion is u8") as u8;
let script = ScriptBuf::from_bytes(hex::decode(script_hex).expect("script hex parses"));
let leaf_ver = LeafVersion::from_consensus(leaf_ver_u8)
.unwrap_or_else(|_| panic!("leaf version {leaf_ver_u8:#x} rejected by bitcoin v0.32"));
leaves.push((depth, script, leaf_ver));
}
fn derive(vector: &Value) -> (XOnlyPublicKey, String) {
let secp = Secp256k1::verification_only();
let internal_hex = vector["given"]["internalPubkey"]
.as_str()
.expect("internalPubkey hex");
let internal_bytes = hex::decode(internal_hex).expect("internalPubkey hex parses");
let internal: UntweakedPublicKey =
XOnlyPublicKey::from_slice(&internal_bytes).expect("32-byte x-only pubkey");
let tree = &vector["given"]["scriptTree"];
if tree.is_null() {
let addr = Address::p2tr(&secp, internal, None, KnownHrp::Mainnet);
let info: TaprootSpendInfo = TaprootBuilder::new()
.finalize(&secp, internal)
.expect("empty builder finalizes for key-spend-only");
let tweaked = info.output_key().to_x_only_public_key();
return (tweaked, addr.to_string());
}
let mut leaves = Vec::new();
walk_tree(tree, 0, &mut leaves);
assert!(!leaves.is_empty(), "non-null tree must yield ≥1 leaf");
let mut builder = TaprootBuilder::new();
for (depth, script, leaf_ver) in leaves {
builder = builder
.add_leaf_with_ver(depth, script, leaf_ver)
.expect("DFS order valid for the BIP-341 tree shape");
}
let info: TaprootSpendInfo = builder
.finalize(&secp, internal)
.expect("script-tree builder finalizes against internal key");
let tweaked = info.output_key().to_x_only_public_key();
let addr = Address::p2tr_tweaked(info.output_key(), KnownHrp::Mainnet);
(tweaked, addr.to_string())
}
fn assert_vector(i: usize) {
let vectors = vectors();
let v = &vectors[i];
let (tweaked, addr) = derive(v);
let expected_tweaked_hex = v["intermediary"]["tweakedPubkey"]
.as_str()
.expect("intermediary.tweakedPubkey hex");
let expected_addr = v["expected"]["bip350Address"]
.as_str()
.expect("expected.bip350Address");
let actual_tweaked_hex = hex::encode(tweaked.serialize());
assert_eq!(
actual_tweaked_hex, expected_tweaked_hex,
"vector {i} tweaked pubkey mismatch"
);
assert_eq!(addr, expected_addr, "vector {i} bech32m address mismatch");
}
#[test]
fn vector_0_key_spend_only() {
assert_vector(0);
}
#[test]
fn vector_1_single_leaf() {
assert_vector(1);
}
#[test]
fn vector_2_single_leaf() {
assert_vector(2);
}
#[test]
fn vector_3_balanced_two_leaves() {
assert_vector(3);
}
#[test]
fn vector_4_balanced_two_leaves() {
assert_vector(4);
}
#[test]
fn vector_5_balanced_two_leaves() {
assert_vector(5);
}
#[test]
fn vector_6_unbalanced_left_leaf_right_subtree() {
assert_vector(6);
}