From d347558a2e65cefc5a42cd22653676cafcb606e1 Mon Sep 17 00:00:00 2001 From: Thomzin Date: Fri, 18 Jul 2025 16:46:06 +0800 Subject: [PATCH 1/7] repairing TapTree & ControlBlockFactory, adding tests --- Cargo.lock | 70 ++++++++---- Cargo.toml | 6 +- derive/Cargo.toml | 2 + derive/src/taptree.rs | 255 ++++++++++++++++++++++++++++++++++++++---- 4 files changed, 286 insertions(+), 47 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b019098..1e3e105 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -121,12 +121,29 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" +[[package]] +name = "bitcoin" +version = "0.29.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0694ea59225b0c5f3cb405ff3f670e4828358ed26aec49dc352f730f0cb1a8a3" +dependencies = [ + "bech32", + "bitcoin_hashes 0.11.0", + "secp256k1 0.24.3", +] + [[package]] name = "bitcoin-io" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b47c4ab7a93edb0c7198c5535ed9b52b63095f4e9b45279c6736cec4b856baf" +[[package]] +name = "bitcoin_hashes" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90064b8dee6815a6470d60bad07bbbaee885c0e12d04177138fa3291a01b7bc4" + [[package]] name = "bitcoin_hashes" version = "0.14.0" @@ -155,15 +172,14 @@ dependencies = [ [[package]] name = "bp-consensus" version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22bbb56809c40565d6085b4211ee2d25a7b5e84848960678ff748e0a8707552f" +source = "git+https://github.com/BP-WG/bp-core#34f958bedaa3ff245932492f936cfcb0bcbe14a6" dependencies = [ "amplify", "chrono", "commit_verify", "getrandom 0.2.16", "getrandom 0.3.3", - "secp256k1", + "secp256k1 0.30.0", "serde", "strict_encoding", "wasm-bindgen", @@ -172,8 +188,7 @@ dependencies = [ [[package]] name = "bp-core" version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a28cb4d675dd49dddd705a29d868d3e3b8f217639fd6f9cbfcbf435236eef77" +source = "git+https://github.com/BP-WG/bp-core#34f958bedaa3ff245932492f936cfcb0bcbe14a6" dependencies = [ "bp-consensus", "bp-dbc", @@ -189,8 +204,7 @@ dependencies = [ [[package]] name = "bp-dbc" version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8aa1f2d9e00a4f2b107d2df25e0d804cfb87a175a37ee503b982b5784e892a6d" +source = "git+https://github.com/BP-WG/bp-core#34f958bedaa3ff245932492f936cfcb0bcbe14a6" dependencies = [ "amplify", "base85", @@ -198,7 +212,7 @@ dependencies = [ "commit_verify", "getrandom 0.2.16", "getrandom 0.3.3", - "secp256k1", + "secp256k1 0.30.0", "serde", "strict_encoding", "wasm-bindgen", @@ -209,11 +223,13 @@ name = "bp-derive" version = "0.12.0-rc.3" dependencies = [ "amplify", + "bitcoin", "bp-consensus", "bp-invoice", "commit_verify", "hmac", "indexmap", + "secp256k1 0.30.0", "serde", "sha2", ] @@ -233,8 +249,7 @@ dependencies = [ [[package]] name = "bp-seals" version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "565ffa069d6425b01630c68dbf1088a88786bd4b794ebf1f37a1d6edbbc29817" +source = "git+https://github.com/BP-WG/bp-core#34f958bedaa3ff245932492f936cfcb0bcbe14a6" dependencies = [ "amplify", "bp-consensus", @@ -262,7 +277,7 @@ dependencies = [ "getrandom 0.3.3", "psbt", "rand 0.9.1", - "secp256k1", + "secp256k1 0.30.0", "serde", "wasm-bindgen", "wasm-bindgen-test", @@ -682,18 +697,37 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "secp256k1" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1629c9c557ef9b293568b338dddfc8208c98a18c59d722a9d53f859d9c9b62" +dependencies = [ + "bitcoin_hashes 0.11.0", + "secp256k1-sys 0.6.1", +] + [[package]] name = "secp256k1" version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" dependencies = [ - "bitcoin_hashes", + "bitcoin_hashes 0.14.0", "rand 0.8.5", - "secp256k1-sys", + "secp256k1-sys 0.10.1", "serde", ] +[[package]] +name = "secp256k1-sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83080e2c2fc1006e625be82e5d1eb6a43b7fd9578b617fcc55814daf286bba4b" +dependencies = [ + "cc", +] + [[package]] name = "secp256k1-sys" version = "0.10.1" @@ -1184,13 +1218,3 @@ dependencies = [ "quote", "syn 2.0.101", ] - -[[patch.unused]] -name = "bp-consensus" -version = "0.12.0-rc.2" -source = "git+https://github.com/BP-WG/bp-core#43a25d025691626c01794a49b2a177db4fa5bceb" - -[[patch.unused]] -name = "bp-core" -version = "0.12.0-rc.2" -source = "git+https://github.com/BP-WG/bp-core#43a25d025691626c01794a49b2a177db4fa5bceb" diff --git a/Cargo.toml b/Cargo.toml index fe7b1d8..a03027a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ license = "Apache-2.0" [workspace.dependencies] amplify = "4.9.0" bech32 = "0.9.1" -secp256k1 = "0.30.0" # 0.31 breaks WASM +secp256k1 = { version = "0.30.0" , features = ["rand"]} # 0.31 breaks WASM strict_encoding = "2.9.1" commit_verify = "0.12.0" bp-consensus = "0.12.0" @@ -74,5 +74,5 @@ getrandom2 = { package = "getrandom", version = "0.2", features = ["js"] } wasm-bindgen-test = "0.3" [patch.crates-io] -bp-consensus = { git = "https://github.com/BP-WG/bp-core" } -bp-core = { git = "https://github.com/BP-WG/bp-core" } +bp-consensus = { git = "https://github.com/BP-WG/bp-core", version = "0.12.0"} +bp-core = { git = "https://github.com/BP-WG/bp-core", version = "0.12.0" } diff --git a/derive/Cargo.toml b/derive/Cargo.toml index ec00b5c..311a594 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -24,6 +24,8 @@ sha2 = "0.10.8" hmac = "0.12.1" indexmap = { workspace = true } serde = { workspace = true, optional = true } +secp256k1 = { version = "0.30.0", features = ["rand"] } +bitcoin = "0.29" [features] default = [] diff --git a/derive/src/taptree.rs b/derive/src/taptree.rs index d5c7880..216e53d 100644 --- a/derive/src/taptree.rs +++ b/derive/src/taptree.rs @@ -26,8 +26,8 @@ use std::{slice, vec}; use amplify::num::u7; use bc::{ - ControlBlock, InternalPk, LeafScript, OutputPk, Parity, TapLeafHash, TapMerklePath, - TapNodeHash, TapScript, + ControlBlock, InternalPk, LeafScript, OutputPk, Parity, + TapLeafHash, TapMerklePath, TapNodeHash, TapScript, TapBranchHash, }; use commit_verify::merkle::MerkleBuoy; @@ -58,7 +58,7 @@ pub struct UnfinalizedTree(pub u7); #[derive(Clone, Eq, PartialEq, Debug, Default)] pub struct TapTreeBuilder { leaves: Vec>, - buoy: MerkleBuoy, + buoy: MerkleBuoy, finalized: bool, } @@ -66,7 +66,7 @@ impl TapTreeBuilder { pub fn new() -> Self { Self { leaves: none!(), - buoy: default!(), + buoy: default!(), finalized: false, } } @@ -74,7 +74,7 @@ impl TapTreeBuilder { pub fn with_capacity(capacity: usize) -> Self { Self { leaves: Vec::with_capacity(capacity), - buoy: zero!(), + buoy: zero!(), finalized: false, } } @@ -134,17 +134,83 @@ impl<'a, L> IntoIterator for &'a TapTree { impl TapTree { pub fn with_single_leaf(leaf: impl Into) -> TapTree { Self(vec![LeafInfo { - depth: u7::ZERO, + depth: u7::ZERO, script: leaf.into(), }]) } pub fn merkle_root(&self) -> TapNodeHash { - if self.0.len() == 1 { - TapLeafHash::with_leaf_script(&self.0[0].script).into() - } else { - todo!("#10 implement TapTree::merkle_root for trees with more than one leaf") + let mut stack: Vec<(u7, TapNodeHash)> = Vec::new(); + + for leaf in &self.0 { + let leaf_hash: TapNodeHash = + TapLeafHash::with_leaf_script(&leaf.script).into(); + let depth = leaf.depth; + stack.push((depth, leaf_hash)); + + while stack.len() >= 2 { + let len = stack.len(); + let (d1, _) = stack[len - 1]; + let (d2, _) = stack[len - 2]; + if d1 != d2 { break; } + + let (_, right) = stack.pop().unwrap(); + let (_, left ) = stack.pop().unwrap(); + let parent_depth = d1 - u7::ONE; + let parent_hash: TapNodeHash = + TapBranchHash::with_nodes(left, right).into(); + stack.push((parent_depth, parent_hash)); + } } + + debug_assert!( + stack.len() == 1 && stack[0].0 == u7::ZERO, + "invalid tap tree: unbalanced leaves" + ); + stack[0].1 + } + + /// 返回第 `index` 号叶子的脚本路径(只含 sibling branch hashes) + pub fn merkle_path(&self, index: usize) -> TapMerklePath { + let mut stack: Vec<(u7, TapNodeHash, Vec, bool)> = Vec::new(); + + for (i, leaf) in self.0.iter().enumerate() { + let leaf_hash: TapNodeHash = TapLeafHash::with_leaf_script(&leaf.script).into(); + let is_target = i == index; + stack.push((leaf.depth, leaf_hash, Vec::new(), is_target)); + + while stack.len() >= 2 { + let len = stack.len(); + let (d1, h1, _, _) = stack[len - 1].clone(); + let (d2, h2, _, _) = stack[len - 2].clone(); + if d1 != d2 { break; } + + let (_dr, _hr, mut path_r, target_r) = stack.pop().unwrap(); + let (_dl, _hl, mut path_l, target_l) = stack.pop().unwrap(); + + let branch_hash = TapBranchHash::with_nodes(h2, h1); + let parent_hash: TapNodeHash = branch_hash.clone().into(); + let parent_depth = d1 - u7::ONE; + + if target_l { + path_l.push(branch_hash.clone()); + } + if target_r { + path_r.push(branch_hash.clone()); + } + + let parent_target = target_l || target_r; + let parent_path = if target_l { path_l } else { path_r }; + + stack.push((parent_depth, parent_hash, parent_path, parent_target)); + } + } + + debug_assert!(stack.len() == 1, "unbalanced tap tree"); + let (_d, _h, path, _t) = stack.pop().unwrap(); + + TapMerklePath::try_from(path) + .expect("tap merkle path length must be within [0..128]") } } @@ -167,7 +233,7 @@ impl TapTree { TapTree( self.into_iter() .map(|leaf| LeafInfo { - depth: leaf.depth, + depth: leaf.depth, script: f(leaf.script), }) .collect(), @@ -178,8 +244,8 @@ impl TapTree { impl Display for TapTree { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { let mut buoy = MerkleBuoy::::default(); - let mut depth = u7::ZERO; + for leaf in &self.0 { for _ in depth.into_u8()..leaf.depth.into_u8() { f.write_char('{')?; @@ -194,11 +260,94 @@ impl Display for TapTree { } debug_assert_ne!(buoy.level(), u7::ZERO); } + debug_assert_eq!(buoy.level(), u7::ZERO); Ok(()) } } +#[cfg(test)] +mod taptree_tests { + use super::*; // TapTree, merkle_root, merkle_path + use amplify::num::u7; + use std::convert::TryFrom; + use bc::{TapBranchHash, TapLeafHash, TapNodeHash, TapMerklePath, TapScript, TapCode}; + + /// 构造一个 LeafInfo:用 TapScript + TapCode + fn make_leaf(depth: u7, ops: &[TapCode]) -> LeafInfo { + let mut ts = TapScript::new(); + for &op in ops { + ts.push_opcode(op); + } + LeafInfo::tap_script(depth, ts) + } + + #[test] + fn single_leaf_merkle() { + // 用 PushNum1 来测试 + let leaf = make_leaf(u7::ZERO, &[TapCode::PushNum1]); + let tree = TapTree(vec![leaf.clone()]); + + let expected = TapNodeHash::from(TapLeafHash::with_leaf_script(&leaf.script)); + assert_eq!(tree.merkle_root(), expected); + + let empty = TapMerklePath::try_from(vec![]).unwrap(); + assert_eq!(tree.merkle_path(0), empty); + } + + #[test] + fn two_leaves_merkle_and_path() { + let depth = u7::ONE; + // 第一个叶子用 PushNum1,第二个叶子用 PushNum2 + let l0 = make_leaf(depth, &[TapCode::PushNum1]); + let l1 = make_leaf(depth, &[TapCode::PushNum2]); + let tree = TapTree(vec![l0.clone(), l1.clone()]); + + let h0: TapNodeHash = TapLeafHash::with_leaf_script(&l0.script).into(); + let h1: TapNodeHash = TapLeafHash::with_leaf_script(&l1.script).into(); + let branch = TapBranchHash::with_nodes(h0, h1); + let expected_root: TapNodeHash = branch.clone().into(); + assert_eq!(tree.merkle_root(), expected_root); + + let p0 = TapMerklePath::try_from(vec![branch.clone()]).unwrap(); + let p1 = TapMerklePath::try_from(vec![branch]).unwrap(); + assert_eq!(tree.merkle_path(0), p0); + assert_eq!(tree.merkle_path(1), p1); + } + + #[test] + fn unbalanced_tree_merkle_and_path() { + // 三叶不平衡:depth=[2,2,1] + let d2 = u7::try_from(2u8).unwrap(); + let d1 = u7::ONE; + // 前两叶都用 PushNum1,第三叶用 PushNum2 + let l0 = make_leaf(d2, &[TapCode::PushNum1]); + let l1 = make_leaf(d2, &[TapCode::PushNum1]); + let l2 = make_leaf(d1, &[TapCode::PushNum2]); + let tree = TapTree(vec![l0.clone(), l1.clone(), l2.clone()]); + + let h0: TapNodeHash = TapLeafHash::with_leaf_script(&l0.script).into(); + let h1: TapNodeHash = TapLeafHash::with_leaf_script(&l1.script).into(); + let h2: TapNodeHash = TapLeafHash::with_leaf_script(&l2.script).into(); + let branch1 = TapBranchHash::with_nodes(h0, h1); + let node1: TapNodeHash = branch1.clone().into(); + let branch2 = TapBranchHash::with_nodes(node1, h2); + + let expected_root: TapNodeHash = branch2.clone().into(); + assert_eq!(tree.merkle_root(), expected_root); + + let p01 = TapMerklePath::try_from(vec![branch1.clone(), branch2.clone()]).unwrap(); + let p2 = TapMerklePath::try_from(vec![branch2]).unwrap(); + + assert_eq!(tree.merkle_path(0), p01); + assert_eq!(tree.merkle_path(1), p01); + assert_eq!(tree.merkle_path(2), p2); + } +} + + + + #[derive(Clone, Eq, PartialEq, Hash, Debug)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase"))] pub struct LeafInfo { @@ -224,41 +373,47 @@ pub struct ControlBlockFactory { merkle_root: TapNodeHash, #[getter(skip)] - merkle_path: TapMerklePath, + merkle_paths: Vec, #[getter(skip)] - remaining_leaves: Vec, + remaining: Vec>, } impl ControlBlockFactory { #[inline] - pub fn with(internal_pk: InternalPk, tap_tree: TapTree) -> Self { + pub fn with(internal_pk: InternalPk, tap_tree: TapTree) -> Self { let merkle_root = tap_tree.merkle_root(); let (output_pk, parity) = internal_pk.to_output_pk(Some(merkle_root)); + let remaining_leaves = tap_tree.clone().into_vec(); + let merkle_paths = (0 .. remaining_leaves.len()) + .map(|i| tap_tree.merkle_path(i)) + .collect(); ControlBlockFactory { internal_pk, output_pk, parity, merkle_root, - merkle_path: empty!(), - remaining_leaves: tap_tree.into_vec(), + merkle_paths, + remaining: remaining_leaves, } } #[inline] - pub fn into_remaining_leaves(self) -> Vec { self.remaining_leaves } + pub fn into_remaining_leaves(self) -> Vec { self.remaining } } impl Iterator for ControlBlockFactory { type Item = (ControlBlock, LeafScript); - fn next(&mut self) -> Option { - let leaf = self.remaining_leaves.pop()?; + // Pop leaf and its path together + let leaf = self.remaining.pop()?; + let path = self.merkle_paths.pop()?; let leaf_script = leaf.script; + // Build control block with the correct path let control_block = ControlBlock::with( leaf_script.version, self.internal_pk, self.parity, - self.merkle_path.clone(), + path, ); Some((control_block, leaf_script)) } @@ -287,3 +442,61 @@ impl TapDerivation { } } } + + + +#[cfg(test)] +mod control_block_factory_tests { + use super::*; // ControlBlockFactory + use amplify::num::u7; + use std::convert::TryFrom; + use crate::taptree::TapTree; + use bc::{InternalPk, LeafVer, ScriptBytes, TapBranchHash, TapLeafHash, TapNodeHash}; + + /// 固定 X-only pubkey(32×0x02) + fn dummy_internal_pk() -> InternalPk { + InternalPk::from_byte_array([0x02u8; 32]).unwrap() + } + + #[test] + fn factory_preserves_paths_and_versions() { + let depth = u7::ONE; + let leaves: Vec> = vec![ + LeafInfo { + script: LeafScript::new( + LeafVer::from_consensus_u8(0xc0).unwrap(), + ScriptBytes::try_from(vec![10]).unwrap(), + ), + depth, + }, + LeafInfo { + script: LeafScript::new( + LeafVer::from_consensus_u8(0xc0).unwrap(), + ScriptBytes::try_from(vec![20]).unwrap(), + ), + depth, + }, + ]; + let clone_leaves = leaves.clone(); + + // 构造工厂并收集 + let items: Vec<_> = + ControlBlockFactory::with(dummy_internal_pk(), TapTree(leaves)).collect(); + assert_eq!(items.len(), clone_leaves.len()); + + // 手算根哈希 + let h0 = TapLeafHash::with_leaf_script(&clone_leaves[0].script).into(); + let h1 = TapLeafHash::with_leaf_script(&clone_leaves[1].script).into(); + let branch = TapBranchHash::with_nodes(h0, h1); + let expected_root: TapNodeHash = branch.clone().into(); + let tree = TapTree(clone_leaves.clone()); + assert_eq!(tree.merkle_root(), expected_root); + + // 对比每个 ControlBlock 的脚本 & 路径 + for (idx, (cb, ls)) in items.into_iter().enumerate() { + assert_eq!(ls.version, tree.0[idx].script.version); + let expected_path = tree.merkle_path(idx); + assert_eq!(cb.merkle_branch, expected_path); + } + } +} From b4cd07b9c45a5a57ec882787d80d9bfff261ab58 Mon Sep 17 00:00:00 2001 From: Thomzin Date: Sun, 20 Jul 2025 14:34:32 +0800 Subject: [PATCH 2/7] Corrected merkle_path merging logic; added negative test; other minor optimizations --- derive/src/taptree.rs | 99 +++++++++++++++++++++++++++++++++---------- 1 file changed, 76 insertions(+), 23 deletions(-) diff --git a/derive/src/taptree.rs b/derive/src/taptree.rs index 216e53d..81a20be 100644 --- a/derive/src/taptree.rs +++ b/derive/src/taptree.rs @@ -170,8 +170,9 @@ impl TapTree { stack[0].1 } - /// 返回第 `index` 号叶子的脚本路径(只含 sibling branch hashes) + /// Returns the script path of leaf `index` (only sibling branch hashes are included) pub fn merkle_path(&self, index: usize) -> TapMerklePath { + // Save (depth, node_hash, path_vec, is_target_leaf) on the stack let mut stack: Vec<(u7, TapNodeHash, Vec, bool)> = Vec::new(); for (i, leaf) in self.0.iter().enumerate() { @@ -179,19 +180,21 @@ impl TapTree { let is_target = i == index; stack.push((leaf.depth, leaf_hash, Vec::new(), is_target)); - while stack.len() >= 2 { - let len = stack.len(); - let (d1, h1, _, _) = stack[len - 1].clone(); - let (d2, h2, _, _) = stack[len - 2].clone(); - if d1 != d2 { break; } - - let (_dr, _hr, mut path_r, target_r) = stack.pop().unwrap(); - let (_dl, _hl, mut path_l, target_l) = stack.pop().unwrap(); - - let branch_hash = TapBranchHash::with_nodes(h2, h1); + // As long as the top two items of the stack have the same depth, they are merged + // —— Here both length and depth are checked + while stack.len() >= 2 + && stack[stack.len() - 1].0 == stack[stack.len() - 2].0 + { + // Note the order of pop: right first, then left + let (_dr, hr, mut path_r, target_r) = stack.pop().unwrap(); + let (_dl, hl, mut path_l, target_l) = stack.pop().unwrap(); + + // Use hl, hr to calculate branch and parent node hashes + let branch_hash = TapBranchHash::with_nodes(hl, hr); let parent_hash: TapNodeHash = branch_hash.clone().into(); - let parent_depth = d1 - u7::ONE; + let parent_depth = _dr - u7::ONE; // _dr == depth of children + // Push the branch hash onto the "path" that contains the target leaf if target_l { path_l.push(branch_hash.clone()); } @@ -206,12 +209,12 @@ impl TapTree { } } + // At this point, only the root node remains on the top of the stack debug_assert!(stack.len() == 1, "unbalanced tap tree"); let (_d, _h, path, _t) = stack.pop().unwrap(); - - TapMerklePath::try_from(path) - .expect("tap merkle path length must be within [0..128]") + TapMerklePath::try_from(path).expect("path length within [0..128]") } + } impl TapTree { @@ -273,7 +276,7 @@ mod taptree_tests { use std::convert::TryFrom; use bc::{TapBranchHash, TapLeafHash, TapNodeHash, TapMerklePath, TapScript, TapCode}; - /// 构造一个 LeafInfo:用 TapScript + TapCode + /// Construct a LeafInfo: Use TapScript + TapCode fn make_leaf(depth: u7, ops: &[TapCode]) -> LeafInfo { let mut ts = TapScript::new(); for &op in ops { @@ -284,7 +287,7 @@ mod taptree_tests { #[test] fn single_leaf_merkle() { - // 用 PushNum1 来测试 + // Test with PushNum1 let leaf = make_leaf(u7::ZERO, &[TapCode::PushNum1]); let tree = TapTree(vec![leaf.clone()]); @@ -298,7 +301,7 @@ mod taptree_tests { #[test] fn two_leaves_merkle_and_path() { let depth = u7::ONE; - // 第一个叶子用 PushNum1,第二个叶子用 PushNum2 + // The first leaf uses PushNum1, the second leaf uses PushNum2 let l0 = make_leaf(depth, &[TapCode::PushNum1]); let l1 = make_leaf(depth, &[TapCode::PushNum2]); let tree = TapTree(vec![l0.clone(), l1.clone()]); @@ -310,14 +313,16 @@ mod taptree_tests { assert_eq!(tree.merkle_root(), expected_root); let p0 = TapMerklePath::try_from(vec![branch.clone()]).unwrap(); + // The sibling path of leaf 0 let p1 = TapMerklePath::try_from(vec![branch]).unwrap(); + // The sibling path of leaf 1 assert_eq!(tree.merkle_path(0), p0); assert_eq!(tree.merkle_path(1), p1); } #[test] fn unbalanced_tree_merkle_and_path() { - // 三叶不平衡:depth=[2,2,1] + // Three-leaf imbalance:depth=[2,2,1] let d2 = u7::try_from(2u8).unwrap(); let d1 = u7::ONE; // 前两叶都用 PushNum1,第三叶用 PushNum2 @@ -453,7 +458,7 @@ mod control_block_factory_tests { use crate::taptree::TapTree; use bc::{InternalPk, LeafVer, ScriptBytes, TapBranchHash, TapLeafHash, TapNodeHash}; - /// 固定 X-only pubkey(32×0x02) + /// Fixed X-only pubkey (32×0x02) fn dummy_internal_pk() -> InternalPk { InternalPk::from_byte_array([0x02u8; 32]).unwrap() } @@ -479,12 +484,12 @@ mod control_block_factory_tests { ]; let clone_leaves = leaves.clone(); - // 构造工厂并收集 + // Constructing factory and collecting let items: Vec<_> = ControlBlockFactory::with(dummy_internal_pk(), TapTree(leaves)).collect(); assert_eq!(items.len(), clone_leaves.len()); - // 手算根哈希 + // Hand-Calculated Root Hash let h0 = TapLeafHash::with_leaf_script(&clone_leaves[0].script).into(); let h1 = TapLeafHash::with_leaf_script(&clone_leaves[1].script).into(); let branch = TapBranchHash::with_nodes(h0, h1); @@ -492,7 +497,7 @@ mod control_block_factory_tests { let tree = TapTree(clone_leaves.clone()); assert_eq!(tree.merkle_root(), expected_root); - // 对比每个 ControlBlock 的脚本 & 路径 + // Compare the scripts & paths of each ControlBlock for (idx, (cb, ls)) in items.into_iter().enumerate() { assert_eq!(ls.version, tree.0[idx].script.version); let expected_path = tree.merkle_path(idx); @@ -500,3 +505,51 @@ mod control_block_factory_tests { } } } +#[cfg(test)] +mod negative_tests { + use super::*; + use amplify::num::u7; + use std::convert::TryFrom; + use bc::{LeafScript, LeafVer, ScriptBytes}; + + #[test] + #[should_panic(expected = "unbalanced tap tree")] + fn merkle_path_empty_tree_panics() { + // If the merkle path is called directly on an empty tree, it will panic "unbalanced tap tree" because there is no root node. + let empty: TapTree = TapTree(vec![]); + let _ = empty.merkle_path(0); + } + + #[test] + fn tree_from_no_leaves_err() { + let err = TapTree::from_leaves(std::iter::empty::>()); + assert!(matches!(err, Err(InvalidTree::Unfinalized(_)))); + } + + #[test] + fn leaf_depth_overflow_err() { + assert!(u7::try_from(128u8).is_err()); + } + + #[test] + fn duplicate_leaves_nonzero_depth_ok() { + // The same script has a depth of 1 and is repeated twice without underflow. + let depth = u7::ONE; + let script = LeafScript::new( + LeafVer::from_consensus_u8(0xc0).unwrap(), + ScriptBytes::try_from(vec![]).unwrap(), + ); + let leaf = LeafInfo { depth, script }; + let tree = TapTree(vec![leaf.clone(), leaf.clone()]); + + // The root hash should be the result of merging two identical leaf hashes. + let h = TapLeafHash::with_leaf_script(&leaf.script).into(); + let expected_root: TapNodeHash = TapBranchHash::with_nodes(h, h).into(); + assert_eq!(tree.merkle_root(), expected_root); + + // The corresponding two paths have only this one branch + let expected_path = TapMerklePath::try_from(vec![TapBranchHash::with_nodes(h, h)]).unwrap(); + assert_eq!(tree.merkle_path(0), expected_path); + assert_eq!(tree.merkle_path(1), expected_path); + } +} From 6b503bed4789cb4a03bfe05efbe680c4cdbb958e Mon Sep 17 00:00:00 2001 From: Thomzin Date: Tue, 5 Aug 2025 16:55:07 +0800 Subject: [PATCH 3/7] fra testing --- derive/Cargo.toml | 5 +- derive/examples/fra_demo.rs | 161 +++++++++ derive/examples/fra_demo1.rs | 142 ++++++++ derive/examples/fra_demo2.rs | 237 +++++++++++++ derive/examples/simple_test.rs | 94 +++++ derive/my_changes.diff | 620 +++++++++++++++++++++++++++++++++ derive/src/fra.rs | 360 +++++++++++++++++++ derive/src/lib.rs | 1 + derive/src/taptree.rs | 71 ++-- derive/taptree_blame.txt | 546 +++++++++++++++++++++++++++++ 10 files changed, 2202 insertions(+), 35 deletions(-) create mode 100644 derive/examples/fra_demo.rs create mode 100644 derive/examples/fra_demo1.rs create mode 100644 derive/examples/fra_demo2.rs create mode 100644 derive/examples/simple_test.rs create mode 100644 derive/my_changes.diff create mode 100644 derive/src/fra.rs create mode 100644 derive/taptree_blame.txt diff --git a/derive/Cargo.toml b/derive/Cargo.toml index 311a594..0dfb935 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -25,7 +25,10 @@ hmac = "0.12.1" indexmap = { workspace = true } serde = { workspace = true, optional = true } secp256k1 = { version = "0.30.0", features = ["rand"] } -bitcoin = "0.29" +bitcoin = "0.32" +strict_encoding = { workspace = true} +bitcoincore-rpc = "0.19" +rand = "0.8.5" # RPC 调用 [features] default = [] diff --git a/derive/examples/fra_demo.rs b/derive/examples/fra_demo.rs new file mode 100644 index 0000000..cf1c021 --- /dev/null +++ b/derive/examples/fra_demo.rs @@ -0,0 +1,161 @@ +use std::{thread::sleep, time::Duration, str::FromStr}; + +use bitcoincore_rpc::{Auth, Client, RpcApi}; +use bitcoincore_rpc::bitcoin::{ + Address as RpcAddress, Amount, Network, OutPoint, Transaction, TxIn, TxOut, + Sequence, Witness, absolute::LockTime, transaction::Version,PrivateKey, + hashes::Hash, +}; +use bitcoincore_rpc::json::AddressType; + +// --- 使用 bp-std 生态系统内的类型 --- +use derive::{self, fra::{FraAction, build_fra_script, build_fra_control_blocks}, base58::encode, KeyOrigin, XOnlyPk, Xpriv, XpubDerivable}; +use bc::{self, ConsensusEncode, ScriptPubkey, SighashCache, SighashFlag, SighashType, TapSighash, TapMerklePath, TapNodeHash , TapBranchHash}; +use invoice::{Address, AddressNetwork, AddressPayload}; +use secp256k1::{Secp256k1, Message, Keypair, SecretKey, PublicKey}; +use amplify::{Wrapper, ByteArray, hex}; // [修复] 导入 Wrapper, ByteArray 和 hex + + +// `bitcoincore-rpc` 生态系统使用的版本(通过其依赖 `bitcoin`) +use bitcoincore_rpc::bitcoin::secp256k1 as bitcoin_secp; + + + +// ... (所有帮助函数保持不变) ... +fn to_bc_outpoint(rpc_outpoint: OutPoint) -> bc::Outpoint { + bc::Outpoint::new( + bc::Txid::from_byte_array(rpc_outpoint.txid.to_byte_array()), + bc::Vout::from_u32(rpc_outpoint.vout) + ) +} + +fn to_bc_txout(rpc_txout: bitcoincore_rpc::bitcoin::TxOut) -> bc::TxOut { + bc::TxOut { + value: bc::Sats::from(rpc_txout.value.to_sat()), + script_pubkey: ScriptPubkey::from_inner( + bc::ScriptBytes::try_from(rpc_txout.script_pubkey.to_bytes()).unwrap() + ), + } +} + +fn calculate_merkle_root(path: &TapMerklePath, leaf_hash: bc::TapLeafHash) -> TapNodeHash { + let mut current_hash: TapNodeHash = leaf_hash.into(); + for sibling_hash in path.iter() { + current_hash = bc::TapBranchHash::with_nodes(current_hash, (*sibling_hash).into()).into(); + } + current_hash +} + + +fn main() -> Result<(), Box> { + let rpc = Client::new( + "http://127.0.0.1:18443/wallet/legacy_true", + Auth::UserPass("foo".into(), "bar".into()), + )?; + rpc.import_private_key(&PrivateKey::from_wif("cVkVW14o6zBGyhaV2xqMsGEqYijB6jzsK5EkNcmzBPJejGmcBrMQ")?, None, None)?; + rpc.import_private_key(&PrivateKey::from_wif("cPMvbJRTmycMYU3pQ3dTfCwzVEtyVqpEeV3LWNaT1pzHhax2FKZF")?, None, None)?; + + let coinbase_addr = rpc.get_new_address(None, Some(AddressType::Legacy))?.assume_checked(); + rpc.generate_to_address(101, &coinbase_addr)?; + sleep(Duration::from_secs(3)); + + let secp = Secp256k1::new(); + + // 1. 生成内部密钥 + let internal_keypair = Keypair::new(&secp, &mut rand::thread_rng()); + // [最终修复] 显式地从 PublicKey 获取 XOnlyPublicKey,与 simple_test.rs 保持一致 + let internal_public_key = PublicKey::from_keypair(&internal_keypair); + let (internal_pk_xonly, _) = internal_public_key.x_only_public_key(); + let internal_pk = bc::InternalPk::from(XOnlyPk::from(internal_pk_xonly)); + + // 2. 解析发送方/接收方私钥 + let sender_secret_rpc = PrivateKey::from_wif("cVkVW14o6zBGyhaV2xqMsGEqYijB6jzsK5EkNcmzBPJejGmcBrMQ")?.inner; + let sender_secret = SecretKey::from_slice(&sender_secret_rpc.secret_bytes())?; + let sender_keypair = Keypair::from_secret_key(&secp, &sender_secret); + let sender_public_key = PublicKey::from_keypair(&sender_keypair); + let (sender_pk_xonly, _) = sender_public_key.x_only_public_key(); + + let recv_secret_rpc = PrivateKey::from_wif("cPMvbJRTmycMYU3pQ3dTfCwzVEtyVqpEeV3LWNaT1pzHhax2FKZF")?.inner; + let recv_secret = SecretKey::from_slice(&recv_secret_rpc.secret_bytes())?; + let recv_keypair = Keypair::from_secret_key(&secp, &recv_secret); + let receiver_public_key = PublicKey::from_keypair(&recv_keypair); + let (receiver_pk_xonly, _) = receiver_public_key.x_only_public_key(); + + // 3. 构建脚本和叶子 + let action = FraAction::Transfer { + asset_id: [0u8; 32], + amount: 1000, + receiver: XOnlyPk::from(receiver_pk_xonly), + sender: XOnlyPk::from(sender_pk_xonly), + }; + let tap_script = derive::fra::build_fra_script(action); + let leaf_script = bc::LeafScript::from_tap_script(tap_script.clone()); + let leaf_hash = leaf_script.tap_leaf_hash(); + + // 4. 手动构建 Taproot 地址 (对于单一脚本,默克尔根就是叶子哈希) + let (output_pk, output_pk_parity) = internal_pk.to_output_pk(Some(leaf_hash.into())); + let fra_addr = Address::new(AddressPayload::Tr(output_pk), AddressNetwork::Regtest); + let fra_spk = fra_addr.script_pubkey(); + + // 5. 注资交易... + let fund_utxo = rpc.list_unspent(None, None, None, None, None)?.into_iter().find(|u| u.amount.to_sat() >= 100_000).unwrap(); + let rpc_addr = RpcAddress::from_str(&fra_addr.to_string())?.assume_checked(); + let fid = rpc.send_to_address(&rpc_addr, Amount::from_sat(fund_utxo.amount.to_sat() - 10000), None, None, None, None, None, None)?; + rpc.generate_to_address(1, &coinbase_addr)?; + + // 6. 准备花费交易... + let funding_tx = rpc.get_raw_transaction(&fid, None)?; + let (fra_vout, fra_txout) = funding_tx.output.iter().enumerate().find(|(_, o)| o.script_pubkey.to_bytes() == fra_spk.as_slice()).unwrap(); + let fra_outpoint = OutPoint { txid: fid, vout: fra_vout as u32 }; + + let dest_addr = rpc.get_new_address(None, Some(AddressType::Legacy))?.assume_checked(); + let mut spend_tx = bc::Tx { + version: bc::TxVer::V2, + lock_time: bc::LockTime::ZERO, + inputs: bc::VarIntArray::from_iter_checked([bc::TxIn { + prev_output: to_bc_outpoint(fra_outpoint), + sig_script: bc::SigScript::new(), + sequence: bc::SeqNo::from_consensus_u32(0xFFFF_FFFF), + witness: bc::Witness::new(), + }]), + outputs: bc::VarIntArray::from_iter_checked([bc::TxOut { + value: bc::Sats::from(fra_txout.value.to_sat() - 10000), + script_pubkey: ScriptPubkey::from_inner(bc::ScriptBytes::try_from(dest_addr.script_pubkey().to_bytes()).unwrap()), + }]), + }; + + // 7. 计算 Sighash + let prevout_bc = to_bc_txout(fra_txout.clone()); + let mut cache = SighashCache::new(&mut spend_tx, vec![prevout_bc])?; + let sighash = cache.tap_sighash_script(0, leaf_hash, None)?; + let msg = Message::from(sighash); + + // 8. 签名 + let sig_sender = secp.sign_schnorr(msg.as_ref(), &sender_keypair); + let sig_receiver = secp.sign_schnorr(msg.as_ref(), &recv_keypair); + + // 9. 手动构建 Control Block (对于单一脚本,路径为空) + let merkle_path = TapMerklePath::try_from(Vec::new())?; + let control_block = bc::ControlBlock::with( + leaf_script.version, + internal_pk, + output_pk_parity, + merkle_path, + ); + let control_block_bytes = control_block.consensus_serialize(); + + // 10. 组装 Witness + spend_tx.inputs[0].witness = bc::Witness::from_consensus_stack(vec![ + sig_receiver.as_ref().to_vec(), + sig_sender.as_ref().to_vec(), + tap_script.to_vec(), + control_block_bytes, + ]); + + // 11. 广播 + let raw_spend_tx = spend_tx.consensus_serialize(); + let sid = rpc.send_raw_transaction(&raw_spend_tx)?; + println!("\n🎉 成功! 花费交易已广播: {}", sid); + + Ok(()) +} \ No newline at end of file diff --git a/derive/examples/fra_demo1.rs b/derive/examples/fra_demo1.rs new file mode 100644 index 0000000..3e2794f --- /dev/null +++ b/derive/examples/fra_demo1.rs @@ -0,0 +1,142 @@ +// derive/examples/fra_demo.rs + +use std::str::FromStr; +use strict_encoding::StreamWriter; +use derive::secp256k1::{Secp256k1, Keypair}; +use rand::thread_rng; +use bitcoincore_rpc::{Auth, Client, RpcApi}; + +// 全部从 bitcoincore_rpc::bitcoin 引入,不要再用单独的 bitcoin crate +use bitcoincore_rpc::bitcoin::{ + Transaction, TxIn, TxOut, OutPoint, Address, + ScriptBuf, Sequence, Witness, + absolute::LockTime, Amount, + taproot::{TapLeafHash, LeafVersion}, + sighash::{SighashCache, Prevouts, TapSighashType}, + consensus::encode::serialize, + Network, transaction::Version, Script, +}; +// 用 StrictWriter 来做 TypedWrite +use strict_encoding::StrictEncode; + +// 从你的 derive crate 拿到 FRA 相关类型和工厂 +use derive::fra::{FraAction, build_fra_control_blocks}; +use bc::{InternalPk, OutputPk, XOnlyPk}; + +use amplify::num::u7; + +fn main() { + // 1) RPC setup + let rpc = Client::new( + "http://127.0.0.1:18443", + Auth::UserPass("foo".into(), "bar".into()), + ).unwrap(); + + // 2) 拿一个可花 UTXO + let utxo = rpc + .list_unspent(None, None, None, None, None) + .unwrap() + .into_iter() + .next() + .expect("请先在 regtest 挖矿并生产 UTXO"); + let outpoint = OutPoint { txid: utxo.txid, vout: utxo.vout }; + + // 原 UTXO 的脚本和金额 + let prev_amount = utxo.amount; + let prev_script = utxo.script_pub_key.clone(); + + // 构造 TxIn + let txin = TxIn { + previous_output: outpoint, + script_sig: ScriptBuf::new(), + sequence: Sequence(0xFFFF_FFFF), + witness: Witness::new(), + }; + + // 3) 构造普通的发送输出:从 RPC 拿一个新地址,并校验网络 + let recipient = rpc.get_new_address(None, None).unwrap() + .require_network(Network::Regtest).unwrap(); + let fee = 1_000u64; + let send_sat = utxo.amount.to_sat() - fee; + let txout = TxOut { + value: Amount::from_sat(send_sat), + script_pubkey: recipient.script_pubkey(), + }; + + + let mut tx = Transaction { + version: Version(2), + lock_time: LockTime::ZERO, + input: vec![txin], + output: vec![txout], + }; + + // 4) 用随机密钥生成 InternalPk / OutputPk + let secp = Secp256k1::new(); + let mut rng = thread_rng(); + + // Internal key for Taproot tweak + let internal_kp = Keypair::new(&secp, &mut rng); + let (ix, _) = internal_kp.x_only_public_key(); + // From for XOnlyPk, then From for InternalPk: + let internal_pk = InternalPk::from(XOnlyPk::from(ix)); + + // Sender + let sender_kp = Keypair::new(&secp, &mut rng); + let (sx, _) = sender_kp.x_only_public_key(); + let sender_pk = OutputPk::from(XOnlyPk::from(sx)); + + // Receiver + let recv_kp = Keypair::new(&secp, &mut rng); + let (rx, _) = recv_kp.x_only_public_key(); + let receiver_pk = OutputPk::from(XOnlyPk::from(rx)); + + // 5) 构造 FRA Merkle 证明 + let depth = u7::try_from(0).unwrap(); + let action = FraAction::Transfer { + asset_id: [0u8; 32], + amount: 1_000, + receiver: receiver_pk.clone(), + sender: sender_pk.clone(), + }; + let proofs = build_fra_control_blocks(internal_pk, vec![(action, depth)]); + let (control_block, leaf_script) = &proofs[0]; + + // 6) 计算 sighash + let mut cache = SighashCache::new(&tx); + let tapleaf = TapLeafHash::from_script( + &Script::from_bytes(leaf_script.script.as_slice()), + LeafVersion::TapScript, + ); + let sighash = cache.taproot_script_spend_signature_hash( + 0, + &Prevouts::All(&[TxOut { value: prev_amount, script_pubkey: prev_script }]), + tapleaf, + TapSighashType::Default, + ).unwrap(); + + // 7) Schnorr 签名:直接用 TapSighash 的字节切片 + let hash_bytes: &[u8; 32] = sighash.as_ref(); + let sig_recv = secp.sign_schnorr(hash_bytes, &recv_kp); + let sig_send = secp.sign_schnorr(hash_bytes, &sender_kp); + + // 8) 填充 witness 并广播 + let mut wit = Vec::new(); + wit.push(sig_recv.as_ref().to_vec()); + wit.push(sig_send.as_ref().to_vec()); + // —— 把 TapScript 脚本本身压进去 —— + // 先将 Confined, …> 强制成 &[u8],再 to_vec() + let script_slice: &[u8] = leaf_script.script.as_ref(); + wit.push(script_slice.to_vec()); + // ControlBlock 序列化 -> Vec + let mut cb_ser = Vec::new(); + let writer: StreamWriter<&mut Vec> = StreamWriter::new::<1024>(&mut cb_ser); + control_block.strict_write(writer).unwrap(); + wit.push(cb_ser); + + tx.input[0].witness = Witness::from_slice(&wit); + + let raw_tx = serialize(&tx); + let txid = rpc.send_raw_transaction(&raw_tx[..]).unwrap(); + println!("Broadcast txid = {}", txid); +} \ No newline at end of file diff --git a/derive/examples/fra_demo2.rs b/derive/examples/fra_demo2.rs new file mode 100644 index 0000000..d5117bf --- /dev/null +++ b/derive/examples/fra_demo2.rs @@ -0,0 +1,237 @@ +// derive/examples/fra_demo.rs + +use std::{thread::sleep, time::Duration}; + +use bitcoincore_rpc::{Auth, Client, RpcApi}; +use bitcoincore_rpc::bitcoin::{ + Address, Amount, Network, + OutPoint, Transaction, TxIn, TxOut, + ScriptBuf, Sequence, Witness, + absolute::LockTime, transaction::Version, + consensus::encode::serialize, + taproot::{TaprootBuilder, TaprootSpendInfo, LeafVersion, TapLeafHash}, + sighash::{SighashCache, Prevouts, TapSighashType}, + PrivateKey, + secp256k1::{ + Secp256k1 as BitcoinSecp, + SecretKey as BitcoinSecretKey, + Keypair as BitcoinKeypair, + XOnlyPublicKey as BitcoinXOnlyPublicKey, + Message as BitcoinMessage, + }, +}; +use bitcoincore_rpc::json::AddressType; + +use derive::fra::{FraAction, build_fra_control_blocks}; +use bc::{InternalPk, OutputPk, XOnlyPk}; +use secp256k1::{ + Secp256k1 as DeriveSecp, + SecretKey as DeriveSecretKey, + Keypair as DeriveKeypair, +}; + +use rand::thread_rng; +use amplify::num::u7; + +fn main() -> Result<(), Box> { + // 0) 连接 regtest RPC,并加载“legacy_true”钱包 + let rpc = Client::new( + "http://127.0.0.1:18443/wallet/legacy_true", + Auth::UserPass("foo".into(), "bar".into()), + )?; + + // 确保私钥已导入 + rpc.import_private_key(&PrivateKey::from_wif("cVkVW14o6zBGyhaV2xqMsGEqYijB6jzsK5EkNcmzBPJejGmcBrMQ")?, None, None)?; + rpc.import_private_key(&PrivateKey::from_wif("cPMvbJRTmycMYU3pQ3dTfCwzVEtyVqpEeV3LWNaT1pzHhax2FKZF")?, None, None)?; + + // (Optional) 挖 101 块,生成成熟 UTXO + println!(">> Generating 101 blocks for coinbase maturity..."); + let coinbase_addr = rpc.get_new_address(None, Some(AddressType::Legacy))?.require_network(Network::Regtest)?; + rpc.generate_to_address(101, &coinbase_addr)?; + sleep(Duration::from_secs(3)); + println!(" Done. Funds are now available."); + + // 检查钱包余额 + let balance = rpc.get_balance(None, None)?; + println!("Wallet balance: {} BTC", balance.to_btc()); + + // --------------------------------------------------- + // STEP1: FUNDING — 铸造一个 FRA Taproot UTXO + // --------------------------------------------------- + let fund_utxo = rpc.list_unspent(None, None, None, None, None)? + .into_iter() + .find(|utxo| utxo.amount.to_sat() >= 100_000) // 确保 UTXO 金额足够 + .expect("没有找到金额足够的 UTXO(需 >= 0.001 BTC)"); + let _fund_outpoint = OutPoint { txid: fund_utxo.txid, vout: fund_utxo.vout }; + println!("Fund UTXO amount: {} BTC", fund_utxo.amount.to_btc()); + + // 1) DERIVE:生成 Internal KeyPair(FRA 控制块用) + let derive_secp = DeriveSecp::new(); + let mut rng = thread_rng(); + let derive_internal_kp = DeriveKeypair::new(&derive_secp, &mut rng); + let (derive_ix, _) = derive_internal_kp.x_only_public_key(); + let internal_pk = InternalPk::from(derive_ix); + + // 2) 解析发送者/接收者 WIF 私钥(bitcoin crate) + let sender_secret: BitcoinSecretKey = PrivateKey::from_wif( + "cVkVW14o6zBGyhaV2xqMsGEqYijB6jzsK5EkNcmzBPJejGmcBrMQ" + )?.inner; + let recv_secret: BitcoinSecretKey = PrivateKey::from_wif( + "cPMvbJRTmycMYU3pQ3dTfCwzVEtyVqpEeV3LWNaT1pzHhax2FKZF" + )?.inner; + + // 3) bitcoin-secret → derive-secret → derive KeyPair → OutputPk + let derive_sender_sk = DeriveSecretKey::from_slice(&sender_secret.secret_bytes())?; + let derive_recv_sk = DeriveSecretKey::from_slice(&recv_secret.secret_bytes())?; + let derive_sender_kp = DeriveKeypair::from_secret_key(&derive_secp, &derive_sender_sk); + let derive_recv_kp = DeriveKeypair::from_secret_key(&derive_secp, &derive_recv_sk); + let (sx, _) = derive_sender_kp.x_only_public_key(); + let (rx, _) = derive_recv_kp.x_only_public_key(); + let sender_xonly_pk = XOnlyPk::from(sx); + let receiver_xonly_pk = XOnlyPk::from(rx); + + println!("Sender PK bytes length: {}", sender_xonly_pk.to_byte_array().len()); + println!("Receiver PK bytes length: {}", receiver_xonly_pk.to_byte_array().len()); + // 4) 构造 FRA Transfer Leaf + let action = FraAction::Transfer { + asset_id: [0u8; 32], + amount: 1000, + receiver: receiver_xonly_pk, + sender: sender_xonly_pk, + }; + let depth = u7::try_from(0).unwrap(); + let proofs = build_fra_control_blocks(internal_pk.clone(), vec![(action, depth)]); + let (control_block, leaf_script) = &proofs[0]; + + // --------------------------------------------------- + // --------------------------------------------------- + // STEP1.5: 构建 Taproot scriptPubKey + // --------------------------------------------------- + let bitcoin_secp = BitcoinSecp::new(); + let bitcoin_ix = BitcoinXOnlyPublicKey::from_slice(&derive_ix.serialize())?; + + // 取出 leaf 脚本字节 + let sb = ScriptBuf::from(AsRef::<[u8]>::as_ref(&leaf_script.script).to_vec()); // 不再手动添加 OP_DROP + + let tap_info: TaprootSpendInfo = TaprootBuilder::new() + .add_leaf(depth.into(), sb.clone())? + .finalize(&bitcoin_secp, bitcoin_ix) + .expect("Taproot finalize failed"); + let fra_addr = Address::p2tr( + &bitcoin_secp, + bitcoin_ix, + tap_info.merkle_root(), + Network::Regtest, + ); + let fra_spk = fra_addr.script_pubkey(); + + // 5) 广播 Funding TX via 钱包 RPC,自动签名 + let send_val = fund_utxo.amount.to_sat().saturating_sub(10_000); // 降低费用预留到 0.0001 BTC + println!("Sending value: {} satoshi", send_val); + if send_val < 546 { + return Err("Transaction amount too small: must be at least 546 satoshi".into()); + } + let fid = rpc.send_to_address( + &fra_addr, + Amount::from_sat(send_val), + None, // label + None, // comment + None, // comment_to + None, // replaceable + None, // conf_target + None, // estimate_mode + )?; + println!(">> STEP1: Funding txid = {}", fid); + // 立即挖一块确认 + let confirm_addr = rpc.get_new_address(None, Some(AddressType::Legacy))?.require_network(Network::Regtest)?; + rpc.generate_to_address(1, &confirm_addr)?; + sleep(Duration::from_secs(3)); + + // --------------------------------------------------- + // STEP2: SPENDING — 花费 FRA UTXO + // --------------------------------------------------- + println!(">> STEP2: Finding FRA UTXO from funding tx {}", fid); + let funding_tx = rpc.get_raw_transaction(&fid, None)?; + let (fra_vout, fra_txout) = funding_tx.output.iter().enumerate() + .find(|(_vout, txout)| txout.script_pubkey == fra_spk) + .expect("在 funding transaction 中没找到与 fra_spk 匹配的输出"); + let fra_outpoint = OutPoint { txid: fid, vout: fra_vout as u32 }; + let fra_amount = fra_txout.value; + println!(" Found FRA UTXO at {}:{} with value {} BTC", fid, fra_vout, fra_amount.to_btc()); + + let left = fra_amount.to_sat().saturating_sub(10_000); // 降低费用预留 + if left < 546 { + return Err("Spend transaction amount too small: must be at least 546 satoshi".into()); + } + let spend_tx = Transaction { + version: Version(2), + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: fra_outpoint.clone(), + script_sig: ScriptBuf::new(), + sequence: Sequence(0xFFFF_FFFF), + witness: Witness::new(), + }], + output: vec![TxOut { + value: Amount::from_sat(left), + script_pubkey: rpc.get_new_address(None, Some(AddressType::Legacy))?.require_network(Network::Regtest)?.script_pubkey(), + }], + }; + + // 6) 计算 Taproot‑FRA 花费 sighash + let mut cache = SighashCache::new(&spend_tx); + let tapleaf_hash = TapLeafHash::from_script(&sb, LeafVersion::TapScript); + let sighash = cache.taproot_script_spend_signature_hash( + 0, + &Prevouts::All(&[fra_txout.clone()]), + tapleaf_hash, + TapSighashType::Default, + )?; + let msg = BitcoinMessage::from_digest_slice(sighash.as_ref())?; + println!("Sighash: {:?}", AsRef::<[u8]>::as_ref(&sighash)); + + // 7) 双 Schnorr 签名 + let btc_sender_kp = BitcoinKeypair::from_secret_key(&bitcoin_secp, &sender_secret); + let btc_recv_kp = BitcoinKeypair::from_secret_key(&bitcoin_secp, &recv_secret); + + let sig_sender = bitcoin_secp.sign_schnorr(&msg, &btc_sender_kp); + let sig_receiver = bitcoin_secp.sign_schnorr(&msg, &btc_recv_kp); // 确保这里是接收方的签名 + // 添加以下验证代码 + let is_sender_sig_valid = bitcoin_secp.verify_schnorr(&sig_sender, &msg, &btc_sender_kp.x_only_public_key().0); + let is_receiver_sig_valid = bitcoin_secp.verify_schnorr(&sig_receiver, &msg, &btc_recv_kp.x_only_public_key().0); + + println!("Sender signature internal verification: {}", is_sender_sig_valid.is_ok()); + println!("Receiver signature internal verification: {}", is_receiver_sig_valid.is_ok()); + + let sig_sender_bytes = sig_sender.as_ref().to_vec(); + let sig_receiver_bytes = sig_receiver.as_ref().to_vec(); + + println!("Signature receiver length: {}", sig_receiver_bytes.len()); // 应该显示 64 + println!("Signature sender length: {}", sig_sender_bytes.len()); // 应该显示 64 + + // 8) 使用 TaprootSpendInfo 生成正确的 ControlBlock + let control_block_bytes = tap_info.control_block(&(sb.clone(), LeafVersion::TapScript)) + .expect("Failed to get control block") + .serialize(); + println!("ControlBlock bytes length: {}", control_block_bytes.len()); + println!("Script bytes: {:?}", sb.to_bytes()); + println!("ControlBlock bytes: {:?}", control_block_bytes); + + // 9) 填 witness 并广播 Spend TX + let mut final_tx = spend_tx.clone(); + final_tx.input[0].witness = Witness::from_slice(&[ + sig_receiver_bytes, // <--- !!!关键修正:接收方签名必须在第一个位置 (栈顶) + sig_sender_bytes, // 发送方签名 (在接收方签名之后消耗) + sb.to_bytes(), // 完整的脚本 + control_block_bytes, // control block + ]); + println!("Final Witness elements:"); + for (i, elem) in final_tx.input[0].witness.iter().enumerate() { + println!(" Witness[{}] (len {}): {:?}", i, elem.len(), elem); + } + let raw_spend = serialize(&final_tx); + let sid = rpc.send_raw_transaction(&raw_spend[..])?; + println!("Spend txid = {}", sid); + + Ok(()) +} \ No newline at end of file diff --git a/derive/examples/simple_test.rs b/derive/examples/simple_test.rs new file mode 100644 index 0000000..18a7392 --- /dev/null +++ b/derive/examples/simple_test.rs @@ -0,0 +1,94 @@ +// 文件: derive/examples/simple_test.rs + +use bitcoincore_rpc::{Auth, Client, RpcApi}; +use bitcoincore_rpc::bitcoin::{ + self, blockdata::opcodes, Address, Amount, Network, OutPoint, ScriptBuf, + Sequence, Transaction, TxIn, TxOut, Witness, absolute::LockTime, + + transaction::Version, + secp256k1::{Secp256k1, Message, Keypair, SecretKey, PublicKey}, + taproot::{TaprootBuilder, LeafVersion, TapLeafHash}, + sighash::{SighashCache, Prevouts, TapSighashType}, +}; +use bitcoincore_rpc::json::AddressType; +use std::str::FromStr; +use amplify::hex; // 使用 amplify 的 hex +use derive::base58::encode; + +fn main() -> Result<(), Box> { + // 1. 设置 + let rpc = Client::new( + "http://127.0.0.1:18443/wallet/legacy_true", + Auth::UserPass("foo".into(), "bar".into()), + )?; + let legacy_addr = rpc.get_new_address(None, Some(AddressType::Legacy))? + .require_network(Network::Regtest)?; + if rpc.get_balance(None, Some(true))? < Amount::from_btc(1.0)? { + rpc.generate_to_address(101, &legacy_addr)?; + } + + println!("\n--- [simple_test.rs] Golden Standard ---"); + + // 2. 密钥和脚本 + let secp = Secp256k1::new(); + let internal_keypair = Keypair::from_secret_key(&secp, &secp.generate_keypair(&mut rand::thread_rng()).0); + let internal_public_key: PublicKey = internal_keypair.public_key(); + let (internal_xonly_pk, _) = internal_public_key.x_only_public_key(); + println!("1. Internal Key: {}", encode(&internal_xonly_pk.serialize())); + + let script_seckey = SecretKey::from_str("1111111111111111111111111111111111111111111111111111111111111111")?; + let script_keypair = Keypair::from_secret_key(&secp, &script_seckey); + let script_public_key: PublicKey = script_keypair.public_key(); + let (script_xonly_pk, _) = script_public_key.x_only_public_key(); + + let script = ScriptBuf::builder() + .push_x_only_key(&script_xonly_pk) + .push_opcode(opcodes::all::OP_CHECKSIG) + .into_script(); + println!("2. Script (hex): {}", encode(script.as_bytes())); + + let leaf_hash = TapLeafHash::from_script(&script, LeafVersion::TapScript); + println!("3. Leaf Hash: {}", leaf_hash); + + // 3. 创建 Taproot 地址 + let tap_builder = TaprootBuilder::new().add_leaf(0, script.clone())?; + let tap_info = tap_builder.finalize(&secp, internal_xonly_pk) + .expect("Failed to finalize Taproot builder"); + println!("4. Merkle Root: {}", tap_info.merkle_root().unwrap()); + let address = Address::p2tr(&secp, internal_xonly_pk, tap_info.merkle_root(), Network::Regtest); + println!("5. Tweaked Output Key: {}", encode(&address.script_pubkey().as_bytes()[2..].to_vec())); + + // ... 后续代码不变 ... + let txid = rpc.send_to_address(&address, Amount::from_sat(50_000), None, None, None, None, None, None)?; + rpc.generate_to_address(1, &legacy_addr)?; + + let funding_tx = rpc.get_raw_transaction(&txid, None)?; + let vout = funding_tx.output.iter().position(|o| o.script_pubkey == address.script_pubkey()).unwrap() as u32; + let previous_output = OutPoint { txid, vout }; + let prevout_to_spend = funding_tx.output[vout as usize].clone(); + + let dest_addr = rpc.get_new_address(Some("dest"), Some(AddressType::Bech32))?.require_network(Network::Regtest)?; + let mut spend_tx = Transaction { + version: Version(2), + lock_time: LockTime::ZERO, + input: vec![TxIn { previous_output, script_sig: ScriptBuf::new(), sequence: Sequence::MAX, witness: Witness::new() }], + output: vec![TxOut { value: Amount::from_sat(40_000), script_pubkey: dest_addr.script_pubkey() }], + }; + + let mut sighash_cache = SighashCache::new(&mut spend_tx); + let sighash = sighash_cache.taproot_script_spend_signature_hash(0, &Prevouts::All(&[prevout_to_spend]), leaf_hash, TapSighashType::Default)?; + println!("6. Sighash: {}", sighash); + + let msg = Message::from_digest_slice(sighash.as_ref())?; + let signature = secp.sign_schnorr(&msg, &script_keypair); + + let control_block = tap_info.control_block(&(script.clone(), LeafVersion::TapScript)).unwrap(); + println!("7. Control Block (hex): {}", encode(&control_block.serialize())); + let witness = Witness::from(vec![signature.as_ref().to_vec(), script.to_bytes(), control_block.serialize()]); + spend_tx.input[0].witness = witness; + + let spend_txid = rpc.send_raw_transaction(&spend_tx)?; + println!("\n🎉 Success! Spend txid: {}", spend_txid); + + Ok(()) +} \ No newline at end of file diff --git a/derive/my_changes.diff b/derive/my_changes.diff new file mode 100644 index 0000000..0d8fe82 --- /dev/null +++ b/derive/my_changes.diff @@ -0,0 +1,620 @@ +diff --git a/Cargo.lock b/Cargo.lock +index b019098..1e3e105 100644 +--- a/Cargo.lock ++++ b/Cargo.lock +@@ -121,12 +121,29 @@ version = "0.9.1" + source = "registry+https://github.com/rust-lang/crates.io-index" + checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" + ++[[package]] ++name = "bitcoin" ++version = "0.29.2" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "0694ea59225b0c5f3cb405ff3f670e4828358ed26aec49dc352f730f0cb1a8a3" ++dependencies = [ ++ "bech32", ++ "bitcoin_hashes 0.11.0", ++ "secp256k1 0.24.3", ++] ++ + [[package]] + name = "bitcoin-io" + version = "0.1.3" + source = "registry+https://github.com/rust-lang/crates.io-index" + checksum = "0b47c4ab7a93edb0c7198c5535ed9b52b63095f4e9b45279c6736cec4b856baf" + ++[[package]] ++name = "bitcoin_hashes" ++version = "0.11.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "90064b8dee6815a6470d60bad07bbbaee885c0e12d04177138fa3291a01b7bc4" ++ + [[package]] + name = "bitcoin_hashes" + version = "0.14.0" +@@ -155,15 +172,14 @@ dependencies = [ + [[package]] + name = "bp-consensus" + version = "0.12.0" +-source = "registry+https://github.com/rust-lang/crates.io-index" +-checksum = "22bbb56809c40565d6085b4211ee2d25a7b5e84848960678ff748e0a8707552f" ++source = "git+https://github.com/BP-WG/bp-core#34f958bedaa3ff245932492f936cfcb0bcbe14a6" + dependencies = [ + "amplify", + "chrono", + "commit_verify", + "getrandom 0.2.16", + "getrandom 0.3.3", +- "secp256k1", ++ "secp256k1 0.30.0", + "serde", + "strict_encoding", + "wasm-bindgen", +@@ -172,8 +188,7 @@ dependencies = [ + [[package]] + name = "bp-core" + version = "0.12.0" +-source = "registry+https://github.com/rust-lang/crates.io-index" +-checksum = "7a28cb4d675dd49dddd705a29d868d3e3b8f217639fd6f9cbfcbf435236eef77" ++source = "git+https://github.com/BP-WG/bp-core#34f958bedaa3ff245932492f936cfcb0bcbe14a6" + dependencies = [ + "bp-consensus", + "bp-dbc", +@@ -189,8 +204,7 @@ dependencies = [ + [[package]] + name = "bp-dbc" + version = "0.12.0" +-source = "registry+https://github.com/rust-lang/crates.io-index" +-checksum = "8aa1f2d9e00a4f2b107d2df25e0d804cfb87a175a37ee503b982b5784e892a6d" ++source = "git+https://github.com/BP-WG/bp-core#34f958bedaa3ff245932492f936cfcb0bcbe14a6" + dependencies = [ + "amplify", + "base85", +@@ -198,7 +212,7 @@ dependencies = [ + "commit_verify", + "getrandom 0.2.16", + "getrandom 0.3.3", +- "secp256k1", ++ "secp256k1 0.30.0", + "serde", + "strict_encoding", + "wasm-bindgen", +@@ -209,11 +223,13 @@ name = "bp-derive" + version = "0.12.0-rc.3" + dependencies = [ + "amplify", ++ "bitcoin", + "bp-consensus", + "bp-invoice", + "commit_verify", + "hmac", + "indexmap", ++ "secp256k1 0.30.0", + "serde", + "sha2", + ] +@@ -233,8 +249,7 @@ dependencies = [ + [[package]] + name = "bp-seals" + version = "0.12.0" +-source = "registry+https://github.com/rust-lang/crates.io-index" +-checksum = "565ffa069d6425b01630c68dbf1088a88786bd4b794ebf1f37a1d6edbbc29817" ++source = "git+https://github.com/BP-WG/bp-core#34f958bedaa3ff245932492f936cfcb0bcbe14a6" + dependencies = [ + "amplify", + "bp-consensus", +@@ -262,7 +277,7 @@ dependencies = [ + "getrandom 0.3.3", + "psbt", + "rand 0.9.1", +- "secp256k1", ++ "secp256k1 0.30.0", + "serde", + "wasm-bindgen", + "wasm-bindgen-test", +@@ -682,18 +697,37 @@ dependencies = [ + "winapi-util", + ] + ++[[package]] ++name = "secp256k1" ++version = "0.24.3" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "6b1629c9c557ef9b293568b338dddfc8208c98a18c59d722a9d53f859d9c9b62" ++dependencies = [ ++ "bitcoin_hashes 0.11.0", ++ "secp256k1-sys 0.6.1", ++] ++ + [[package]] + name = "secp256k1" + version = "0.30.0" + source = "registry+https://github.com/rust-lang/crates.io-index" + checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" + dependencies = [ +- "bitcoin_hashes", ++ "bitcoin_hashes 0.14.0", + "rand 0.8.5", +- "secp256k1-sys", ++ "secp256k1-sys 0.10.1", + "serde", + ] + ++[[package]] ++name = "secp256k1-sys" ++version = "0.6.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "83080e2c2fc1006e625be82e5d1eb6a43b7fd9578b617fcc55814daf286bba4b" ++dependencies = [ ++ "cc", ++] ++ + [[package]] + name = "secp256k1-sys" + version = "0.10.1" +@@ -1184,13 +1218,3 @@ dependencies = [ + "quote", + "syn 2.0.101", + ] +- +-[[patch.unused]] +-name = "bp-consensus" +-version = "0.12.0-rc.2" +-source = "git+https://github.com/BP-WG/bp-core#43a25d025691626c01794a49b2a177db4fa5bceb" +- +-[[patch.unused]] +-name = "bp-core" +-version = "0.12.0-rc.2" +-source = "git+https://github.com/BP-WG/bp-core#43a25d025691626c01794a49b2a177db4fa5bceb" +diff --git a/Cargo.toml b/Cargo.toml +index fe7b1d8..a03027a 100644 +--- a/Cargo.toml ++++ b/Cargo.toml +@@ -16,7 +16,7 @@ license = "Apache-2.0" + [workspace.dependencies] + amplify = "4.9.0" + bech32 = "0.9.1" +-secp256k1 = "0.30.0" # 0.31 breaks WASM ++secp256k1 = { version = "0.30.0" , features = ["rand"]} # 0.31 breaks WASM + strict_encoding = "2.9.1" + commit_verify = "0.12.0" + bp-consensus = "0.12.0" +@@ -74,5 +74,5 @@ getrandom2 = { package = "getrandom", version = "0.2", features = ["js"] } + wasm-bindgen-test = "0.3" + + [patch.crates-io] +-bp-consensus = { git = "https://github.com/BP-WG/bp-core" } +-bp-core = { git = "https://github.com/BP-WG/bp-core" } ++bp-consensus = { git = "https://github.com/BP-WG/bp-core", version = "0.12.0"} ++bp-core = { git = "https://github.com/BP-WG/bp-core", version = "0.12.0" } +diff --git a/derive/Cargo.toml b/derive/Cargo.toml +index ec00b5c..311a594 100644 +--- a/derive/Cargo.toml ++++ b/derive/Cargo.toml +@@ -24,6 +24,8 @@ sha2 = "0.10.8" + hmac = "0.12.1" + indexmap = { workspace = true } + serde = { workspace = true, optional = true } ++secp256k1 = { version = "0.30.0", features = ["rand"] } ++bitcoin = "0.29" + + [features] + default = [] +diff --git a/derive/src/taptree.rs b/derive/src/taptree.rs +index d5c7880..81a20be 100644 +--- a/derive/src/taptree.rs ++++ b/derive/src/taptree.rs +@@ -26,8 +26,8 @@ use std::{slice, vec}; + + use amplify::num::u7; + use bc::{ +- ControlBlock, InternalPk, LeafScript, OutputPk, Parity, TapLeafHash, TapMerklePath, +- TapNodeHash, TapScript, ++ ControlBlock, InternalPk, LeafScript, OutputPk, Parity, ++ TapLeafHash, TapMerklePath, TapNodeHash, TapScript, TapBranchHash, + }; + use commit_verify::merkle::MerkleBuoy; + +@@ -58,7 +58,7 @@ pub struct UnfinalizedTree(pub u7); + #[derive(Clone, Eq, PartialEq, Debug, Default)] + pub struct TapTreeBuilder { + leaves: Vec>, +- buoy: MerkleBuoy, ++ buoy: MerkleBuoy, + finalized: bool, + } + +@@ -66,7 +66,7 @@ impl TapTreeBuilder { + pub fn new() -> Self { + Self { + leaves: none!(), +- buoy: default!(), ++ buoy: default!(), + finalized: false, + } + } +@@ -74,7 +74,7 @@ impl TapTreeBuilder { + pub fn with_capacity(capacity: usize) -> Self { + Self { + leaves: Vec::with_capacity(capacity), +- buoy: zero!(), ++ buoy: zero!(), + finalized: false, + } + } +@@ -134,18 +134,87 @@ impl<'a, L> IntoIterator for &'a TapTree { + impl TapTree { + pub fn with_single_leaf(leaf: impl Into) -> TapTree { + Self(vec![LeafInfo { +- depth: u7::ZERO, ++ depth: u7::ZERO, + script: leaf.into(), + }]) + } + + pub fn merkle_root(&self) -> TapNodeHash { +- if self.0.len() == 1 { +- TapLeafHash::with_leaf_script(&self.0[0].script).into() +- } else { +- todo!("#10 implement TapTree::merkle_root for trees with more than one leaf") ++ let mut stack: Vec<(u7, TapNodeHash)> = Vec::new(); ++ ++ for leaf in &self.0 { ++ let leaf_hash: TapNodeHash = ++ TapLeafHash::with_leaf_script(&leaf.script).into(); ++ let depth = leaf.depth; ++ stack.push((depth, leaf_hash)); ++ ++ while stack.len() >= 2 { ++ let len = stack.len(); ++ let (d1, _) = stack[len - 1]; ++ let (d2, _) = stack[len - 2]; ++ if d1 != d2 { break; } ++ ++ let (_, right) = stack.pop().unwrap(); ++ let (_, left ) = stack.pop().unwrap(); ++ let parent_depth = d1 - u7::ONE; ++ let parent_hash: TapNodeHash = ++ TapBranchHash::with_nodes(left, right).into(); ++ stack.push((parent_depth, parent_hash)); ++ } ++ } ++ ++ debug_assert!( ++ stack.len() == 1 && stack[0].0 == u7::ZERO, ++ "invalid tap tree: unbalanced leaves" ++ ); ++ stack[0].1 ++ } ++ ++ /// Returns the script path of leaf `index` (only sibling branch hashes are included) ++ pub fn merkle_path(&self, index: usize) -> TapMerklePath { ++ // Save (depth, node_hash, path_vec, is_target_leaf) on the stack ++ let mut stack: Vec<(u7, TapNodeHash, Vec, bool)> = Vec::new(); ++ ++ for (i, leaf) in self.0.iter().enumerate() { ++ let leaf_hash: TapNodeHash = TapLeafHash::with_leaf_script(&leaf.script).into(); ++ let is_target = i == index; ++ stack.push((leaf.depth, leaf_hash, Vec::new(), is_target)); ++ ++ // As long as the top two items of the stack have the same depth, they are merged ++ // —— Here both length and depth are checked ++ while stack.len() >= 2 ++ && stack[stack.len() - 1].0 == stack[stack.len() - 2].0 ++ { ++ // Note the order of pop: right first, then left ++ let (_dr, hr, mut path_r, target_r) = stack.pop().unwrap(); ++ let (_dl, hl, mut path_l, target_l) = stack.pop().unwrap(); ++ ++ // Use hl, hr to calculate branch and parent node hashes ++ let branch_hash = TapBranchHash::with_nodes(hl, hr); ++ let parent_hash: TapNodeHash = branch_hash.clone().into(); ++ let parent_depth = _dr - u7::ONE; // _dr == depth of children ++ ++ // Push the branch hash onto the "path" that contains the target leaf ++ if target_l { ++ path_l.push(branch_hash.clone()); ++ } ++ if target_r { ++ path_r.push(branch_hash.clone()); ++ } ++ ++ let parent_target = target_l || target_r; ++ let parent_path = if target_l { path_l } else { path_r }; ++ ++ stack.push((parent_depth, parent_hash, parent_path, parent_target)); ++ } + } ++ ++ // At this point, only the root node remains on the top of the stack ++ debug_assert!(stack.len() == 1, "unbalanced tap tree"); ++ let (_d, _h, path, _t) = stack.pop().unwrap(); ++ TapMerklePath::try_from(path).expect("path length within [0..128]") + } ++ + } + + impl TapTree { +@@ -167,7 +236,7 @@ impl TapTree { + TapTree( + self.into_iter() + .map(|leaf| LeafInfo { +- depth: leaf.depth, ++ depth: leaf.depth, + script: f(leaf.script), + }) + .collect(), +@@ -178,8 +247,8 @@ impl TapTree { + impl Display for TapTree { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let mut buoy = MerkleBuoy::::default(); +- + let mut depth = u7::ZERO; ++ + for leaf in &self.0 { + for _ in depth.into_u8()..leaf.depth.into_u8() { + f.write_char('{')?; +@@ -194,11 +263,96 @@ impl Display for TapTree { + } + debug_assert_ne!(buoy.level(), u7::ZERO); + } ++ + debug_assert_eq!(buoy.level(), u7::ZERO); + Ok(()) + } + } + ++#[cfg(test)] ++mod taptree_tests { ++ use super::*; // TapTree, merkle_root, merkle_path ++ use amplify::num::u7; ++ use std::convert::TryFrom; ++ use bc::{TapBranchHash, TapLeafHash, TapNodeHash, TapMerklePath, TapScript, TapCode}; ++ ++ /// Construct a LeafInfo: Use TapScript + TapCode ++ fn make_leaf(depth: u7, ops: &[TapCode]) -> LeafInfo { ++ let mut ts = TapScript::new(); ++ for &op in ops { ++ ts.push_opcode(op); ++ } ++ LeafInfo::tap_script(depth, ts) ++ } ++ ++ #[test] ++ fn single_leaf_merkle() { ++ // Test with PushNum1 ++ let leaf = make_leaf(u7::ZERO, &[TapCode::PushNum1]); ++ let tree = TapTree(vec![leaf.clone()]); ++ ++ let expected = TapNodeHash::from(TapLeafHash::with_leaf_script(&leaf.script)); ++ assert_eq!(tree.merkle_root(), expected); ++ ++ let empty = TapMerklePath::try_from(vec![]).unwrap(); ++ assert_eq!(tree.merkle_path(0), empty); ++ } ++ ++ #[test] ++ fn two_leaves_merkle_and_path() { ++ let depth = u7::ONE; ++ // The first leaf uses PushNum1, the second leaf uses PushNum2 ++ let l0 = make_leaf(depth, &[TapCode::PushNum1]); ++ let l1 = make_leaf(depth, &[TapCode::PushNum2]); ++ let tree = TapTree(vec![l0.clone(), l1.clone()]); ++ ++ let h0: TapNodeHash = TapLeafHash::with_leaf_script(&l0.script).into(); ++ let h1: TapNodeHash = TapLeafHash::with_leaf_script(&l1.script).into(); ++ let branch = TapBranchHash::with_nodes(h0, h1); ++ let expected_root: TapNodeHash = branch.clone().into(); ++ assert_eq!(tree.merkle_root(), expected_root); ++ ++ let p0 = TapMerklePath::try_from(vec![branch.clone()]).unwrap(); ++ // The sibling path of leaf 0 ++ let p1 = TapMerklePath::try_from(vec![branch]).unwrap(); ++ // The sibling path of leaf 1 ++ assert_eq!(tree.merkle_path(0), p0); ++ assert_eq!(tree.merkle_path(1), p1); ++ } ++ ++ #[test] ++ fn unbalanced_tree_merkle_and_path() { ++ // Three-leaf imbalance:depth=[2,2,1] ++ let d2 = u7::try_from(2u8).unwrap(); ++ let d1 = u7::ONE; ++ // 前两叶都用 PushNum1,第三叶用 PushNum2 ++ let l0 = make_leaf(d2, &[TapCode::PushNum1]); ++ let l1 = make_leaf(d2, &[TapCode::PushNum1]); ++ let l2 = make_leaf(d1, &[TapCode::PushNum2]); ++ let tree = TapTree(vec![l0.clone(), l1.clone(), l2.clone()]); ++ ++ let h0: TapNodeHash = TapLeafHash::with_leaf_script(&l0.script).into(); ++ let h1: TapNodeHash = TapLeafHash::with_leaf_script(&l1.script).into(); ++ let h2: TapNodeHash = TapLeafHash::with_leaf_script(&l2.script).into(); ++ let branch1 = TapBranchHash::with_nodes(h0, h1); ++ let node1: TapNodeHash = branch1.clone().into(); ++ let branch2 = TapBranchHash::with_nodes(node1, h2); ++ ++ let expected_root: TapNodeHash = branch2.clone().into(); ++ assert_eq!(tree.merkle_root(), expected_root); ++ ++ let p01 = TapMerklePath::try_from(vec![branch1.clone(), branch2.clone()]).unwrap(); ++ let p2 = TapMerklePath::try_from(vec![branch2]).unwrap(); ++ ++ assert_eq!(tree.merkle_path(0), p01); ++ assert_eq!(tree.merkle_path(1), p01); ++ assert_eq!(tree.merkle_path(2), p2); ++ } ++} ++ ++ ++ ++ + #[derive(Clone, Eq, PartialEq, Hash, Debug)] + #[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase"))] + pub struct LeafInfo { +@@ -224,41 +378,47 @@ pub struct ControlBlockFactory { + merkle_root: TapNodeHash, + + #[getter(skip)] +- merkle_path: TapMerklePath, ++ merkle_paths: Vec, + #[getter(skip)] +- remaining_leaves: Vec, ++ remaining: Vec>, + } + + impl ControlBlockFactory { + #[inline] +- pub fn with(internal_pk: InternalPk, tap_tree: TapTree) -> Self { ++ pub fn with(internal_pk: InternalPk, tap_tree: TapTree) -> Self { + let merkle_root = tap_tree.merkle_root(); + let (output_pk, parity) = internal_pk.to_output_pk(Some(merkle_root)); ++ let remaining_leaves = tap_tree.clone().into_vec(); ++ let merkle_paths = (0 .. remaining_leaves.len()) ++ .map(|i| tap_tree.merkle_path(i)) ++ .collect(); + ControlBlockFactory { + internal_pk, + output_pk, + parity, + merkle_root, +- merkle_path: empty!(), +- remaining_leaves: tap_tree.into_vec(), ++ merkle_paths, ++ remaining: remaining_leaves, + } + } + + #[inline] +- pub fn into_remaining_leaves(self) -> Vec { self.remaining_leaves } ++ pub fn into_remaining_leaves(self) -> Vec { self.remaining } + } + + impl Iterator for ControlBlockFactory { + type Item = (ControlBlock, LeafScript); +- + fn next(&mut self) -> Option { +- let leaf = self.remaining_leaves.pop()?; ++ // Pop leaf and its path together ++ let leaf = self.remaining.pop()?; ++ let path = self.merkle_paths.pop()?; + let leaf_script = leaf.script; ++ // Build control block with the correct path + let control_block = ControlBlock::with( + leaf_script.version, + self.internal_pk, + self.parity, +- self.merkle_path.clone(), ++ path, + ); + Some((control_block, leaf_script)) + } +@@ -287,3 +447,109 @@ impl TapDerivation { + } + } + } ++ ++ ++ ++#[cfg(test)] ++mod control_block_factory_tests { ++ use super::*; // ControlBlockFactory ++ use amplify::num::u7; ++ use std::convert::TryFrom; ++ use crate::taptree::TapTree; ++ use bc::{InternalPk, LeafVer, ScriptBytes, TapBranchHash, TapLeafHash, TapNodeHash}; ++ ++ /// Fixed X-only pubkey (32×0x02) ++ fn dummy_internal_pk() -> InternalPk { ++ InternalPk::from_byte_array([0x02u8; 32]).unwrap() ++ } ++ ++ #[test] ++ fn factory_preserves_paths_and_versions() { ++ let depth = u7::ONE; ++ let leaves: Vec> = vec![ ++ LeafInfo { ++ script: LeafScript::new( ++ LeafVer::from_consensus_u8(0xc0).unwrap(), ++ ScriptBytes::try_from(vec![10]).unwrap(), ++ ), ++ depth, ++ }, ++ LeafInfo { ++ script: LeafScript::new( ++ LeafVer::from_consensus_u8(0xc0).unwrap(), ++ ScriptBytes::try_from(vec![20]).unwrap(), ++ ), ++ depth, ++ }, ++ ]; ++ let clone_leaves = leaves.clone(); ++ ++ // Constructing factory and collecting ++ let items: Vec<_> = ++ ControlBlockFactory::with(dummy_internal_pk(), TapTree(leaves)).collect(); ++ assert_eq!(items.len(), clone_leaves.len()); ++ ++ // Hand-Calculated Root Hash ++ let h0 = TapLeafHash::with_leaf_script(&clone_leaves[0].script).into(); ++ let h1 = TapLeafHash::with_leaf_script(&clone_leaves[1].script).into(); ++ let branch = TapBranchHash::with_nodes(h0, h1); ++ let expected_root: TapNodeHash = branch.clone().into(); ++ let tree = TapTree(clone_leaves.clone()); ++ assert_eq!(tree.merkle_root(), expected_root); ++ ++ // Compare the scripts & paths of each ControlBlock ++ for (idx, (cb, ls)) in items.into_iter().enumerate() { ++ assert_eq!(ls.version, tree.0[idx].script.version); ++ let expected_path = tree.merkle_path(idx); ++ assert_eq!(cb.merkle_branch, expected_path); ++ } ++ } ++} ++#[cfg(test)] ++mod negative_tests { ++ use super::*; ++ use amplify::num::u7; ++ use std::convert::TryFrom; ++ use bc::{LeafScript, LeafVer, ScriptBytes}; ++ ++ #[test] ++ #[should_panic(expected = "unbalanced tap tree")] ++ fn merkle_path_empty_tree_panics() { ++ // If the merkle path is called directly on an empty tree, it will panic "unbalanced tap tree" because there is no root node. ++ let empty: TapTree = TapTree(vec![]); ++ let _ = empty.merkle_path(0); ++ } ++ ++ #[test] ++ fn tree_from_no_leaves_err() { ++ let err = TapTree::from_leaves(std::iter::empty::>()); ++ assert!(matches!(err, Err(InvalidTree::Unfinalized(_)))); ++ } ++ ++ #[test] ++ fn leaf_depth_overflow_err() { ++ assert!(u7::try_from(128u8).is_err()); ++ } ++ ++ #[test] ++ fn duplicate_leaves_nonzero_depth_ok() { ++ // The same script has a depth of 1 and is repeated twice without underflow. ++ let depth = u7::ONE; ++ let script = LeafScript::new( ++ LeafVer::from_consensus_u8(0xc0).unwrap(), ++ ScriptBytes::try_from(vec![]).unwrap(), ++ ); ++ let leaf = LeafInfo { depth, script }; ++ let tree = TapTree(vec![leaf.clone(), leaf.clone()]); ++ ++ // The root hash should be the result of merging two identical leaf hashes. ++ let h = TapLeafHash::with_leaf_script(&leaf.script).into(); ++ let expected_root: TapNodeHash = TapBranchHash::with_nodes(h, h).into(); ++ assert_eq!(tree.merkle_root(), expected_root); ++ ++ // The corresponding two paths have only this one branch ++ let expected_path = TapMerklePath::try_from(vec![TapBranchHash::with_nodes(h, h)]).unwrap(); ++ assert_eq!(tree.merkle_path(0), expected_path); ++ assert_eq!(tree.merkle_path(1), expected_path); ++ } ++} diff --git a/derive/src/fra.rs b/derive/src/fra.rs new file mode 100644 index 0000000..3306e52 --- /dev/null +++ b/derive/src/fra.rs @@ -0,0 +1,360 @@ +use amplify::num::u7; +use bc::{ + TapScript, TapCode, ControlBlock, LeafScript, XOnlyPk, + InternalPk, OutputPk}; + +use crate::taptree::{TapTree, LeafInfo, ControlBlockFactory}; + +/// —— FRA 操作对应的 OP_SUCCESSxx 常量 —— +const OP_FRA_TRANSFER: TapCode = TapCode::Success80; +const OP_FRA_MINT: TapCode = TapCode::Success98; +const OP_FRA_BURN: TapCode = TapCode::Success126; +const OP_FRA_ROLLBACK: TapCode = TapCode::Success127; +const OP_FRA_REDEEM: TapCode = TapCode::Success128; +const OP_FRA_SPLIT: TapCode = TapCode::Success129; +const OP_FRA_MERGE: TapCode = TapCode::Success131; +const OP_FRA_FREEZE: TapCode = TapCode::Success132; +const OP_FRA_UNFREEZE: TapCode = TapCode::Success133; +const OP_FRA_GRANT_ROLE: TapCode = TapCode::Success134; +const OP_FRA_REVOKE_ROLE: TapCode = TapCode::Success137; +const OP_FRA_UPGRADE: TapCode = TapCode::Success138; +const OP_FRA_METADATA_UPDATE: TapCode = TapCode::Success141; + +/// Helper: 将字节块编码到脚本缓冲 +fn push_bytes(buf: &mut Vec, data: &[u8]) { + let len = data.len(); + if len == 0 { + buf.push(TapCode::PushBytes0 as u8); + } else if len <= 75 { + let opcode = (TapCode::PushBytes1 as u8).wrapping_add((len - 1) as u8); + buf.push(opcode); + } else if len < 0x100 { + buf.push(TapCode::PushData1 as u8); + buf.push(len as u8); + } else if len < 0x10000 { + buf.push(TapCode::PushData2 as u8); + buf.extend_from_slice(&(len as u16).to_le_bytes()); + } else { + buf.push(TapCode::PushData4 as u8); + buf.extend_from_slice(&(len as u32).to_le_bytes()); + } + buf.extend_from_slice(data); +} + +/// Helper: 将整数编码为最小脚本数字或字节块推送 +fn push_int(buf: &mut Vec, value: u64) { + match value { + 0 => buf.push(TapCode::PushBytes0 as u8), + 1..=16 => { + let code = match value { + 1 => TapCode::PushNum1, + 2 => TapCode::PushNum2, + 3 => TapCode::PushNum3, + 4 => TapCode::PushNum4, + 5 => TapCode::PushNum5, + 6 => TapCode::PushNum6, + 7 => TapCode::PushNum7, + 8 => TapCode::PushNum8, + 9 => TapCode::PushNum9, + 10 => TapCode::PushNum10, + 11 => TapCode::PushNum11, + 12 => TapCode::PushNum12, + 13 => TapCode::PushNum13, + 14 => TapCode::PushNum14, + 15 => TapCode::PushNum15, + 16 => TapCode::PushNum16, + _ => unreachable!(), + }; + buf.push(code as u8); + } + _ => { + let mut v = value; + let mut bytes = Vec::new(); + while v > 0 { + bytes.push((v & 0xFF) as u8); + v >>= 8; + } + if bytes.last().map_or(false, |b| b & 0x80 != 0) { + bytes.push(0); + } + push_bytes(buf, &bytes); + } + } +} + +/// 支持的 FRA 操作类型 +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum FraAction { + /// 转账:双重签名,先接收方同意再发送方授权 + Transfer { + asset_id: [u8; 32], // 资产标识 + amount: u64, // 转账数量 + receiver: XOnlyPk, // 接收方公钥 + sender: XOnlyPk, // 发送方公钥 + }, + /// 增发:单方授权,只有增发者能执行 + Mint { + asset_id: [u8; 32], // 资产标识 + amount: u64, // 增发数量 + receiver: OutputPk, // 接收方公钥 + minter: XOnlyPk, // 增发者公钥 + }, + /// 销毁(燃烧) + Burn { + asset_id: [u8; 32], // 资产标识 + amount: u64, // 燃烧数量 + owner: XOnlyPk, // 授权者公钥 + }, + /// 回退:持有者主动返还给发行者 + Rollback { + asset_id: [u8; 32], // 资产标识 + amount: u64, // 回退数量 + owner: XOnlyPk, // 资产持有者公钥 + minter: XOnlyPk, // 发行者(归还目标)公钥 + }, + /// 赎回:返还底层资产或销毁代币 + Redeem { + asset_id: [u8; 32], // 资产标识 + amount: u64, // 赎回数量 + owner: XOnlyPk, // 授权者公钥 + // 可扩展:time_lock: Option, // 时间锁或其他条件 + }, + /// 拆分:将一笔 UTXO 拆成多笔 + Split { + asset_id: [u8; 32], // 资产标识 + orig_amount: u64, // 原始总量 + outputs: Vec<(u64, OutputPk)>, // 拆分后数量 + 接收方公钥 + owner: XOnlyPk, // 授权者公钥 + }, + /// 合并:将多笔 UTXO 合并为一笔 + Merge { + asset_id: [u8; 32], // 资产标识 + inputs: Vec, // 待合并的数量列表 + recipient: OutputPk, // 合并后接收方公钥 + owner: XOnlyPk, // 授权者公钥 + }, + /// 冻结资产 + Freeze { + asset_id: [u8; 32], // 资产标识 + authority: XOnlyPk, // 冻结权限公钥 + // 可扩展:freeze_until: Option, + }, + /// 解冻资产 + Unfreeze { + asset_id: [u8; 32], // 资产标识 + authority: XOnlyPk, // 解冻权限公钥 + }, + /// 授予角色 + GrantRole { + asset_id: [u8; 32], // 资产标识或 UTXO 唯一 ID + role: Vec, // 角色标识 + target: XOnlyPk, // 被授予方公钥 + admin: XOnlyPk, // 管理员公钥(签名者) + }, + /// 撤销角色 + RevokeRole { + asset_id: [u8; 32], + role: Vec, + target: XOnlyPk, + admin: XOnlyPk, + }, + /// 升级脚本版本 + Upgrade { + asset_id: [u8; 32], // 资产标识或 UTXO ID + new_version: [u8; 32], // 新脚本版本哈希 + authority: XOnlyPk, // 升级权限公钥 + }, + /// 更新元数据 + MetadataUpdate { + asset_id: [u8; 32], // 资产标识或 UTXO ID + metadata: Vec, // 新元数据二进制/JSON + authority: XOnlyPk, // 授权者公钥 + }, +} + +/// 构造对应操作的 TapScript +pub fn build_fra_script(action: FraAction) -> TapScript { + let mut buf = Vec::new(); + match action { + // —— 转账动作 —— + FraAction::Transfer { asset_id: _, amount: _, receiver, sender } => { + // 初始堆栈: [sig_receiver], [sig_sender] + + // --- 验证接收方签名 --- + push_bytes(&mut buf, &receiver.to_byte_array()); // 压入接收方公钥 + buf.push(TapCode::Swap as u8); // 交换 -> [sig_receiver], [receiver_pk] + buf.push(TapCode::CheckSigVerify as u8); // 验证并消耗 + + // --- 验证发送方签名 --- + push_bytes(&mut buf, &sender.to_byte_array()); // 压入发送方公钥 + buf.push(TapCode::Swap as u8); // 交换 -> [sig_sender], [sender_pk] + buf.push(TapCode::CheckSig as u8); // 验证 + } + // —— 增发动作 —— + FraAction::Mint { asset_id, amount, receiver, minter } => { + // 1) AssetID + push_bytes(&mut buf, &asset_id); + // 2) 增发数量 + push_int(&mut buf, amount); + // 3) 接收方公钥 + push_bytes(&mut buf, &receiver.to_byte_array()); + // 4) 增发者公钥 + push_bytes(&mut buf, &minter.to_byte_array()); + buf.push(TapCode::Swap as u8); // <--- [关键修正] 修正堆栈顺序 + // 5) 验签 + buf.push(TapCode::CheckSigVerify as u8); + } + // 销毁:授权者签名后销毁 + FraAction::Burn { asset_id, amount, owner } => { + push_bytes(&mut buf, &asset_id); + push_int(&mut buf, amount); + push_bytes(&mut buf, &owner.to_byte_array()); + buf.push(TapCode::Swap as u8); // + buf.push(TapCode::CheckSigVerify as u8); + } + // 回退:持有者发起, 回退给发行者 + FraAction::Rollback { asset_id, amount, owner, minter } => { + push_bytes(&mut buf, &asset_id); + push_int(&mut buf, amount); + push_bytes(&mut buf, &owner.to_byte_array()); + buf.push(TapCode::Swap as u8); + buf.push(TapCode::CheckSigVerify as u8); + push_bytes(&mut buf, &minter.to_byte_array()); + } + // 赎回:持有者签名并执行赎回逻辑 + FraAction::Redeem { asset_id, amount, owner } => { + push_bytes(&mut buf, &asset_id); + push_int(&mut buf, amount); + push_bytes(&mut buf, &owner.to_byte_array()); + buf.push(TapCode::Swap as u8); + buf.push(TapCode::CheckSigVerify as u8); + } + // 拆分:校验总量一致并签名 + FraAction::Split { asset_id, orig_amount, outputs, owner } => { + push_bytes(&mut buf, &asset_id); + push_int(&mut buf, orig_amount); + // push new amounts + for (amt, _) in &outputs { + push_int(&mut buf, *amt); + } + // sum all new amounts + for _ in 0..outputs.len().saturating_sub(1) { + buf.push(TapCode::Add as u8); + } + // 验证 Orig == sum(new) + buf.push(TapCode::EqualVerify as u8); + // 持有者签名验证 + push_bytes(&mut buf, &owner.to_byte_array()); + buf.push(TapCode::Swap as u8); + buf.push(TapCode::CheckSigVerify as u8); + } + // 合并:校验合并后数量并签名 + FraAction::Merge { asset_id, inputs, recipient, owner } => { + push_bytes(&mut buf, &asset_id); + for amt in &inputs { + push_int(&mut buf, *amt); + } + for _ in 0..inputs.len().saturating_sub(1) { + buf.push(TapCode::Add as u8); + } + // push expected merged amount + let merged_amt: u64 = inputs.iter().sum(); + push_int(&mut buf, merged_amt); + buf.push(TapCode::EqualVerify as u8); + // 持有者签名验证 + push_bytes(&mut buf, &owner.to_byte_array()); + buf.push(TapCode::Swap as u8); + buf.push(TapCode::CheckSigVerify as u8); + // 输出接收方公钥,用于后续输出分配 + push_bytes(&mut buf, &recipient.to_byte_array()); + } + // 冻结:授权者签名验证 + FraAction::Freeze { asset_id, authority } => { + push_bytes(&mut buf, &asset_id); + push_bytes(&mut buf, &authority.to_byte_array()); + buf.push(TapCode::Swap as u8); + buf.push(TapCode::CheckSigVerify as u8); + } + // 解冻:授权者签名验证 + FraAction::Unfreeze { asset_id, authority } => { + push_bytes(&mut buf, &asset_id); + push_bytes(&mut buf, &authority.to_byte_array()); + buf.push(TapCode::Swap as u8); + buf.push(TapCode::CheckSigVerify as u8); + } + // 授予角色:校验管理员签名 + FraAction::GrantRole { asset_id, role, target, admin } => { + push_bytes(&mut buf, &asset_id); + push_bytes(&mut buf, &role); + push_bytes(&mut buf, &target.to_byte_array()); + push_bytes(&mut buf, &admin.to_byte_array()); + buf.push(TapCode::Swap as u8); + buf.push(TapCode::CheckSigVerify as u8); + } + // 撤销角色:校验管理员签名 + FraAction::RevokeRole { asset_id, role, target, admin } => { + push_bytes(&mut buf, &asset_id); + push_bytes(&mut buf, &role); + push_bytes(&mut buf, &target.to_byte_array()); + push_bytes(&mut buf, &admin.to_byte_array()); + buf.push(TapCode::Swap as u8); + buf.push(TapCode::CheckSigVerify as u8); + } + // 升级:校验授权签名 + FraAction::Upgrade { asset_id, new_version, authority } => { + push_bytes(&mut buf, &asset_id); + push_bytes(&mut buf, &new_version); + push_bytes(&mut buf, &authority.to_byte_array()); + buf.push(TapCode::Swap as u8); + buf.push(TapCode::CheckSigVerify as u8); + } + // 元数据更新:校验授权签名 + FraAction::MetadataUpdate { asset_id, metadata, authority } => { + push_bytes(&mut buf, &asset_id); + push_bytes(&mut buf, &metadata); + push_bytes(&mut buf, &authority.to_byte_array()); + buf.push(TapCode::Swap as u8); + buf.push(TapCode::CheckSigVerify as u8); + } + } + println!("Generated raw script bytes: {:?}", buf); // 添加这一行 + TapScript::from_checked(buf) +} + +/// 将 TapScript 包装成叶子信息 +/// 把一个 FRA 动作和它在 Merkle 树里的深度,打包成 LeafInfo +pub fn fra_leaf_info(action: FraAction, depth: u7) -> LeafInfo { + // 1) 用 build_fra_script 根据动作构造对应的 TapScript + let ts = build_fra_script(action); + // 2) 把 TapScript 包装成 LeafScript 并带上深度 + LeafInfo::tap_script(depth, ts) +} + + +/// 将一系列 FRA 动作打包成 Taproot 解锁证明 (ControlBlock + LeafScript) +/// +/// 输入: +/// - `internal_pk`: Taproot 内部公钥,用于 tweaked 输出 key +/// - `actions`: Vec<(FraAction, depth)>,每个元素包含一个 FRA 动作和 Merkle 树深度 +/// +/// 输出: +/// Vec<(ControlBlock, LeafScript)>,对应每个动作的 ControlBlock 及其脚本叶子,可直接放入交易 `witness`。 +pub fn build_fra_control_blocks( + internal_pk: InternalPk, + actions: Vec<(FraAction, u7)>, +) -> Vec<(ControlBlock, LeafScript)> { + // 1. 把每个 (action, depth) 包装成 LeafInfo + let leaf_infos = actions + .into_iter() + .map(|(action, depth)| fra_leaf_info(action, depth)) + .collect::>(); + + // 2. 用所有叶子构造一棵 Merkle 树 + let tap_tree = TapTree::from_leaves(leaf_infos) + .expect("FRA script tree build failed"); + + // 3. 用 ControlBlockFactory 结合 internal_pk 生成每个叶子的 ControlBlock + ControlBlockFactory::with(internal_pk, tap_tree) + .collect() +} + diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 006f33b..9f026f9 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -32,6 +32,7 @@ mod xkey; mod derive; pub mod taptree; mod sign; +pub mod fra; pub use bc::*; pub use derive::{ diff --git a/derive/src/taptree.rs b/derive/src/taptree.rs index 81a20be..ccb0ba4 100644 --- a/derive/src/taptree.rs +++ b/derive/src/taptree.rs @@ -25,9 +25,11 @@ use std::ops::Deref; use std::{slice, vec}; use amplify::num::u7; +use crate::amplify::ByteArray; + use bc::{ - ControlBlock, InternalPk, LeafScript, OutputPk, Parity, - TapLeafHash, TapMerklePath, TapNodeHash, TapScript, TapBranchHash, + ControlBlock, InternalPk, LeafScript, OutputPk, Parity, TapBranchHash, TapLeafHash, + TapMerklePath, TapNodeHash, TapScript, }; use commit_verify::merkle::MerkleBuoy; @@ -58,7 +60,7 @@ pub struct UnfinalizedTree(pub u7); #[derive(Clone, Eq, PartialEq, Debug, Default)] pub struct TapTreeBuilder { leaves: Vec>, - buoy: MerkleBuoy, + buoy: MerkleBuoy, finalized: bool, } @@ -66,7 +68,7 @@ impl TapTreeBuilder { pub fn new() -> Self { Self { leaves: none!(), - buoy: default!(), + buoy: default!(), finalized: false, } } @@ -74,7 +76,7 @@ impl TapTreeBuilder { pub fn with_capacity(capacity: usize) -> Self { Self { leaves: Vec::with_capacity(capacity), - buoy: zero!(), + buoy: zero!(), finalized: false, } } @@ -109,7 +111,11 @@ impl TapTreeBuilder { /// Non-empty taproot script tree. #[derive(Clone, Eq, PartialEq, Hash, Debug, Default)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(transparent))] +#[cfg_attr( + feature = "serde", + derive(Serialize, Deserialize), + serde(transparent) +)] pub struct TapTree(Vec>); impl Deref for TapTree { @@ -134,7 +140,7 @@ impl<'a, L> IntoIterator for &'a TapTree { impl TapTree { pub fn with_single_leaf(leaf: impl Into) -> TapTree { Self(vec![LeafInfo { - depth: u7::ZERO, + depth: u7::ZERO, script: leaf.into(), }]) } @@ -143,8 +149,7 @@ impl TapTree { let mut stack: Vec<(u7, TapNodeHash)> = Vec::new(); for leaf in &self.0 { - let leaf_hash: TapNodeHash = - TapLeafHash::with_leaf_script(&leaf.script).into(); + let leaf_hash: TapNodeHash = TapLeafHash::with_leaf_script(&leaf.script).into(); let depth = leaf.depth; stack.push((depth, leaf_hash)); @@ -152,13 +157,19 @@ impl TapTree { let len = stack.len(); let (d1, _) = stack[len - 1]; let (d2, _) = stack[len - 2]; - if d1 != d2 { break; } + if d1 != d2 { + break; + } let (_, right) = stack.pop().unwrap(); - let (_, left ) = stack.pop().unwrap(); + let (_, left) = stack.pop().unwrap(); let parent_depth = d1 - u7::ONE; - let parent_hash: TapNodeHash = - TapBranchHash::with_nodes(left, right).into(); + let parent_hash = if left.to_byte_array() < right.to_byte_array() { + TapBranchHash::with_nodes(left, right).into() + } else { + TapBranchHash::with_nodes(right, left).into() + }; + stack.push((parent_depth, parent_hash)); } } @@ -172,34 +183,27 @@ impl TapTree { /// Returns the script path of leaf `index` (only sibling branch hashes are included) pub fn merkle_path(&self, index: usize) -> TapMerklePath { - // Save (depth, node_hash, path_vec, is_target_leaf) on the stack - let mut stack: Vec<(u7, TapNodeHash, Vec, bool)> = Vec::new(); + // [BUG 修复] 栈中存储的路径向量类型从 Vec 改为 Vec + let mut stack: Vec<(u7, TapNodeHash, Vec, bool)> = Vec::new(); for (i, leaf) in self.0.iter().enumerate() { let leaf_hash: TapNodeHash = TapLeafHash::with_leaf_script(&leaf.script).into(); let is_target = i == index; stack.push((leaf.depth, leaf_hash, Vec::new(), is_target)); - // As long as the top two items of the stack have the same depth, they are merged - // —— Here both length and depth are checked - while stack.len() >= 2 - && stack[stack.len() - 1].0 == stack[stack.len() - 2].0 - { - // Note the order of pop: right first, then left - let (_dr, hr, mut path_r, target_r) = stack.pop().unwrap(); - let (_dl, hl, mut path_l, target_l) = stack.pop().unwrap(); - - // Use hl, hr to calculate branch and parent node hashes - let branch_hash = TapBranchHash::with_nodes(hl, hr); - let parent_hash: TapNodeHash = branch_hash.clone().into(); - let parent_depth = _dr - u7::ONE; // _dr == depth of children - - // Push the branch hash onto the "path" that contains the target leaf + while stack.len() >= 2 && stack[stack.len() - 1].0 == stack[stack.len() - 2].0 { + let (depth, hr, mut path_r, target_r) = stack.pop().unwrap(); + let (_, hl, mut path_l, target_l) = stack.pop().unwrap(); + + let parent_hash: TapNodeHash = TapBranchHash::with_nodes(hl, hr).into(); + let parent_depth = depth - u7::ONE; + + // [BUG 修复] 将兄弟节点的哈希 (TapNodeHash) 存入路径,而不是父节点的分支哈希 if target_l { - path_l.push(branch_hash.clone()); + path_l.push(hr); } if target_r { - path_r.push(branch_hash.clone()); + path_r.push(hl); } let parent_target = target_l || target_r; @@ -209,12 +213,11 @@ impl TapTree { } } - // At this point, only the root node remains on the top of the stack debug_assert!(stack.len() == 1, "unbalanced tap tree"); let (_d, _h, path, _t) = stack.pop().unwrap(); + // 现在 path 是 Vec,可以成功创建 TapMerklePath TapMerklePath::try_from(path).expect("path length within [0..128]") } - } impl TapTree { diff --git a/derive/taptree_blame.txt b/derive/taptree_blame.txt new file mode 100644 index 0000000..5f71375 --- /dev/null +++ b/derive/taptree_blame.txt @@ -0,0 +1,546 @@ +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 10) // +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 11) // Licensed under the Apache License, Version 2.0 (the "License"); +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 12) // you may not use this file except in compliance with the License. +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 13) // You may obtain a copy of the License at +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 14) // +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 15) // http://www.apache.org/licenses/LICENSE-2.0 +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 16) // +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 17) // Unless required by applicable law or agreed to in writing, software +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 18) // distributed under the License is distributed on an "AS IS" BASIS, +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 19) // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 20) // See the License for the specific language governing permissions and +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 21) // limitations under the License. +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 22) +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 23) use std::fmt::{self, Display, Formatter, Write}; +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 24) use std::ops::Deref; +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 25) use std::{slice, vec}; +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 26) +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 27) use amplify::num::u7; +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 28) use bc::{ +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 29) ControlBlock, InternalPk, LeafScript, OutputPk, Parity, +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 30) TapLeafHash, TapMerklePath, TapNodeHash, TapScript, TapBranchHash, +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 31) }; +c14bae37 derive/src/taptree.rs (Dr Maxim Orlovsky 2023-12-17 16:04:47 +0100 32) use commit_verify::merkle::MerkleBuoy; +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 33) +37288799 derive/src/taptree.rs (Dr Maxim Orlovsky 2024-06-29 16:41:17 +0200 34) use crate::{KeyOrigin, Terminal, XkeyOrigin}; +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 35) +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 36) #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Display, Error, From)] +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 37) pub enum InvalidTree { +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 38) #[from] +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 39) #[display(doc_comments)] +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 40) Unfinalized(UnfinalizedTree), +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 41) +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 42) #[from(FinalizedTree)] +eb9ba13f derive/src/taptree.rs (Dr Maxim Orlovsky 2024-08-18 10:30:21 +0200 43) #[display("tap tree contains too many script leaves which doesn't fit a single Merkle tree")] +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 44) MountainRange, +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 45) } +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 46) +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 47) #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Display, Error)] +eb9ba13f derive/src/taptree.rs (Dr Maxim Orlovsky 2024-08-18 10:30:21 +0200 48) #[display("can't add more leaves to an already finalized tap tree")] +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 49) pub struct FinalizedTree; +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 50) +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 51) #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Display, Error)] +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 52) #[display( +eb9ba13f derive/src/taptree.rs (Dr Maxim Orlovsky 2024-08-18 10:30:21 +0200 53) "unfinalized tap tree containing leaves at level {0} which can't commit into a single Merkle \ +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 54) root" +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 55) )] +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 56) pub struct UnfinalizedTree(pub u7); +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 57) +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 58) #[derive(Clone, Eq, PartialEq, Debug, Default)] +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 59) pub struct TapTreeBuilder { +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 60) leaves: Vec>, +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 61) buoy: MerkleBuoy, +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 62) finalized: bool, +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 63) } +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 64) +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 65) impl TapTreeBuilder { +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 66) pub fn new() -> Self { +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 67) Self { +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 68) leaves: none!(), +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 69) buoy: default!(), +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 70) finalized: false, +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 71) } +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 72) } +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 73) +c14bae37 derive/src/taptree.rs (Dr Maxim Orlovsky 2023-12-17 16:04:47 +0100 74) pub fn with_capacity(capacity: usize) -> Self { +c14bae37 derive/src/taptree.rs (Dr Maxim Orlovsky 2023-12-17 16:04:47 +0100 75) Self { +eb9ba13f derive/src/taptree.rs (Dr Maxim Orlovsky 2024-08-18 10:30:21 +0200 76) leaves: Vec::with_capacity(capacity), +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 77) buoy: zero!(), +c14bae37 derive/src/taptree.rs (Dr Maxim Orlovsky 2023-12-17 16:04:47 +0100 78) finalized: false, +c14bae37 derive/src/taptree.rs (Dr Maxim Orlovsky 2023-12-17 16:04:47 +0100 79) } +c14bae37 derive/src/taptree.rs (Dr Maxim Orlovsky 2023-12-17 16:04:47 +0100 80) } +c14bae37 derive/src/taptree.rs (Dr Maxim Orlovsky 2023-12-17 16:04:47 +0100 81) +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 82) pub fn is_finalized(&self) -> bool { self.finalized } +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 83) +29a3c8d1 derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-31 08:30:35 +0200 84) pub fn with_leaf(mut self, leaf: LeafInfo) -> Result { +29a3c8d1 derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-31 08:30:35 +0200 85) self.push_leaf(leaf)?; +29a3c8d1 derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-31 08:30:35 +0200 86) Ok(self) +29a3c8d1 derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-31 08:30:35 +0200 87) } +29a3c8d1 derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-31 08:30:35 +0200 88) +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 89) pub fn push_leaf(&mut self, leaf: LeafInfo) -> Result { +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 90) if self.finalized { +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 91) return Err(FinalizedTree); +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 92) } +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 93) let depth = leaf.depth; +eb9ba13f derive/src/taptree.rs (Dr Maxim Orlovsky 2024-08-18 10:30:21 +0200 94) self.leaves.push(leaf); +c31646bd std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 19:00:05 +0200 95) self.buoy.push(depth); +c31646bd std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 19:00:05 +0200 96) if self.buoy.level() == u7::ZERO { +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 97) self.finalized = true +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 98) } +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 99) Ok(self.finalized) +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 100) } +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 101) +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 102) pub fn finish(self) -> Result, UnfinalizedTree> { +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 103) if !self.finalized { +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 104) return Err(UnfinalizedTree(self.buoy.level())); +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 105) } +eb9ba13f derive/src/taptree.rs (Dr Maxim Orlovsky 2024-08-18 10:30:21 +0200 106) Ok(TapTree(self.leaves)) +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 107) } +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 108) } +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 109) +1c363025 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-08 23:00:05 +0200 110) /// Non-empty taproot script tree. +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 111) #[derive(Clone, Eq, PartialEq, Hash, Debug, Default)] +3596af55 derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-20 14:48:03 +0200 112) #[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(transparent))] +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 113) pub struct TapTree(Vec>); +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 114) +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 115) impl Deref for TapTree { +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 116) type Target = Vec>; +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 117) fn deref(&self) -> &Self::Target { &self.0 } +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 118) } +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 119) +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 120) impl IntoIterator for TapTree { +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 121) type Item = LeafInfo; +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 122) type IntoIter = vec::IntoIter>; +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 123) +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 124) fn into_iter(self) -> Self::IntoIter { self.0.into_iter() } +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 125) } +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 126) +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 127) impl<'a, L> IntoIterator for &'a TapTree { +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 128) type Item = &'a LeafInfo; +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 129) type IntoIter = slice::Iter<'a, LeafInfo>; +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 130) +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 131) fn into_iter(self) -> Self::IntoIter { self.0.iter() } +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 132) } +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 133) +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 134) impl TapTree { +c14bae37 derive/src/taptree.rs (Dr Maxim Orlovsky 2023-12-17 16:04:47 +0100 135) pub fn with_single_leaf(leaf: impl Into) -> TapTree { +c14bae37 derive/src/taptree.rs (Dr Maxim Orlovsky 2023-12-17 16:04:47 +0100 136) Self(vec![LeafInfo { +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 137) depth: u7::ZERO, +c14bae37 derive/src/taptree.rs (Dr Maxim Orlovsky 2023-12-17 16:04:47 +0100 138) script: leaf.into(), +c14bae37 derive/src/taptree.rs (Dr Maxim Orlovsky 2023-12-17 16:04:47 +0100 139) }]) +c14bae37 derive/src/taptree.rs (Dr Maxim Orlovsky 2023-12-17 16:04:47 +0100 140) } +c14bae37 derive/src/taptree.rs (Dr Maxim Orlovsky 2023-12-17 16:04:47 +0100 141) +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 142) pub fn merkle_root(&self) -> TapNodeHash { +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 143) let mut stack: Vec<(u7, TapNodeHash)> = Vec::new(); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 144) +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 145) for leaf in &self.0 { +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 146) let leaf_hash: TapNodeHash = +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 147) TapLeafHash::with_leaf_script(&leaf.script).into(); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 148) let depth = leaf.depth; +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 149) stack.push((depth, leaf_hash)); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 150) +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 151) while stack.len() >= 2 { +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 152) let len = stack.len(); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 153) let (d1, _) = stack[len - 1]; +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 154) let (d2, _) = stack[len - 2]; +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 155) if d1 != d2 { break; } +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 156) +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 157) let (_, right) = stack.pop().unwrap(); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 158) let (_, left ) = stack.pop().unwrap(); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 159) let parent_depth = d1 - u7::ONE; +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 160) let parent_hash: TapNodeHash = +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 161) TapBranchHash::with_nodes(left, right).into(); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 162) stack.push((parent_depth, parent_hash)); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 163) } +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 164) } +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 165) +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 166) debug_assert!( +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 167) stack.len() == 1 && stack[0].0 == u7::ZERO, +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 168) "invalid tap tree: unbalanced leaves" +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 169) ); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 170) stack[0].1 +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 171) } +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 172) +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 173) /// Returns the script path of leaf `index` (only sibling branch hashes are included) +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 174) pub fn merkle_path(&self, index: usize) -> TapMerklePath { +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 175) // Save (depth, node_hash, path_vec, is_target_leaf) on the stack +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 176) let mut stack: Vec<(u7, TapNodeHash, Vec, bool)> = Vec::new(); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 177) +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 178) for (i, leaf) in self.0.iter().enumerate() { +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 179) let leaf_hash: TapNodeHash = TapLeafHash::with_leaf_script(&leaf.script).into(); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 180) let is_target = i == index; +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 181) stack.push((leaf.depth, leaf_hash, Vec::new(), is_target)); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 182) +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 183) // As long as the top two items of the stack have the same depth, they are merged +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 184) // —— Here both length and depth are checked +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 185) while stack.len() >= 2 +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 186) && stack[stack.len() - 1].0 == stack[stack.len() - 2].0 +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 187) { +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 188) // Note the order of pop: right first, then left +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 189) let (_dr, hr, mut path_r, target_r) = stack.pop().unwrap(); +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 190) let (_dl, hl, mut path_l, target_l) = stack.pop().unwrap(); +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 191) +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 192) // Use hl, hr to calculate branch and parent node hashes +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 193) let branch_hash = TapBranchHash::with_nodes(hl, hr); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 194) let parent_hash: TapNodeHash = branch_hash.clone().into(); +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 195) let parent_depth = _dr - u7::ONE; // _dr == depth of children +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 196) +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 197) // Push the branch hash onto the "path" that contains the target leaf +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 198) if target_l { +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 199) path_l.push(branch_hash.clone()); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 200) } +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 201) if target_r { +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 202) path_r.push(branch_hash.clone()); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 203) } +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 204) +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 205) let parent_target = target_l || target_r; +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 206) let parent_path = if target_l { path_l } else { path_r }; +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 207) +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 208) stack.push((parent_depth, parent_hash, parent_path, parent_target)); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 209) } +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 210) } +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 211) +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 212) // At this point, only the root node remains on the top of the stack +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 213) debug_assert!(stack.len() == 1, "unbalanced tap tree"); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 214) let (_d, _h, path, _t) = stack.pop().unwrap(); +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 215) TapMerklePath::try_from(path).expect("path length within [0..128]") +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 216) } +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 217) +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 218) } +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 219) +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 220) impl TapTree { +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 221) pub fn from_leaves(leaves: impl IntoIterator>) -> Result { +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 222) let mut builder = TapTreeBuilder::::new(); +eb9ba13f derive/src/taptree.rs (Dr Maxim Orlovsky 2024-08-18 10:30:21 +0200 223) for leaf in leaves { +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 224) builder.push_leaf(leaf)?; +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 225) } +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 226) builder.finish().map_err(InvalidTree::from) +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 227) } +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 228) +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 229) pub fn from_builder(builder: TapTreeBuilder) -> Result { +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 230) builder.finish() +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 231) } +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 232) +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 233) pub fn into_vec(self) -> Vec> { self.0 } +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 234) +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 235) pub fn map(self, f: impl Fn(L) -> M) -> TapTree { +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 236) TapTree( +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 237) self.into_iter() +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 238) .map(|leaf| LeafInfo { +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 239) depth: leaf.depth, +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 240) script: f(leaf.script), +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 241) }) +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 242) .collect(), +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 243) ) +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 244) } +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 245) } +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 246) +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 247) impl Display for TapTree { +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 248) fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 249) let mut buoy = MerkleBuoy::::default(); +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 250) let mut depth = u7::ZERO; +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 251) +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 252) for leaf in &self.0 { +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 253) for _ in depth.into_u8()..leaf.depth.into_u8() { +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 254) f.write_char('{')?; +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 255) } +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 256) buoy.push(leaf.depth); +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 257) if depth == leaf.depth { +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 258) f.write_char(',')?; +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 259) } +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 260) depth = leaf.depth; +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 261) for _ in buoy.level().into_u8()..depth.into_u8() { +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 262) f.write_char('}')?; +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 263) } +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 264) debug_assert_ne!(buoy.level(), u7::ZERO); +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 265) } +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 266) +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 267) debug_assert_eq!(buoy.level(), u7::ZERO); +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 268) Ok(()) +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 269) } +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 270) } +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 271) +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 272) #[cfg(test)] +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 273) mod taptree_tests { +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 274) use super::*; // TapTree, merkle_root, merkle_path +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 275) use amplify::num::u7; +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 276) use std::convert::TryFrom; +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 277) use bc::{TapBranchHash, TapLeafHash, TapNodeHash, TapMerklePath, TapScript, TapCode}; +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 278) +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 279) /// Construct a LeafInfo: Use TapScript + TapCode +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 280) fn make_leaf(depth: u7, ops: &[TapCode]) -> LeafInfo { +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 281) let mut ts = TapScript::new(); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 282) for &op in ops { +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 283) ts.push_opcode(op); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 284) } +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 285) LeafInfo::tap_script(depth, ts) +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 286) } +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 287) +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 288) #[test] +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 289) fn single_leaf_merkle() { +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 290) // Test with PushNum1 +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 291) let leaf = make_leaf(u7::ZERO, &[TapCode::PushNum1]); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 292) let tree = TapTree(vec![leaf.clone()]); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 293) +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 294) let expected = TapNodeHash::from(TapLeafHash::with_leaf_script(&leaf.script)); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 295) assert_eq!(tree.merkle_root(), expected); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 296) +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 297) let empty = TapMerklePath::try_from(vec![]).unwrap(); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 298) assert_eq!(tree.merkle_path(0), empty); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 299) } +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 300) +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 301) #[test] +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 302) fn two_leaves_merkle_and_path() { +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 303) let depth = u7::ONE; +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 304) // The first leaf uses PushNum1, the second leaf uses PushNum2 +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 305) let l0 = make_leaf(depth, &[TapCode::PushNum1]); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 306) let l1 = make_leaf(depth, &[TapCode::PushNum2]); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 307) let tree = TapTree(vec![l0.clone(), l1.clone()]); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 308) +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 309) let h0: TapNodeHash = TapLeafHash::with_leaf_script(&l0.script).into(); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 310) let h1: TapNodeHash = TapLeafHash::with_leaf_script(&l1.script).into(); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 311) let branch = TapBranchHash::with_nodes(h0, h1); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 312) let expected_root: TapNodeHash = branch.clone().into(); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 313) assert_eq!(tree.merkle_root(), expected_root); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 314) +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 315) let p0 = TapMerklePath::try_from(vec![branch.clone()]).unwrap(); +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 316) // The sibling path of leaf 0 +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 317) let p1 = TapMerklePath::try_from(vec![branch]).unwrap(); +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 318) // The sibling path of leaf 1 +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 319) assert_eq!(tree.merkle_path(0), p0); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 320) assert_eq!(tree.merkle_path(1), p1); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 321) } +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 322) +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 323) #[test] +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 324) fn unbalanced_tree_merkle_and_path() { +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 325) // Three-leaf imbalance:depth=[2,2,1] +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 326) let d2 = u7::try_from(2u8).unwrap(); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 327) let d1 = u7::ONE; +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 328) // 前两叶都用 PushNum1,第三叶用 PushNum2 +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 329) let l0 = make_leaf(d2, &[TapCode::PushNum1]); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 330) let l1 = make_leaf(d2, &[TapCode::PushNum1]); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 331) let l2 = make_leaf(d1, &[TapCode::PushNum2]); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 332) let tree = TapTree(vec![l0.clone(), l1.clone(), l2.clone()]); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 333) +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 334) let h0: TapNodeHash = TapLeafHash::with_leaf_script(&l0.script).into(); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 335) let h1: TapNodeHash = TapLeafHash::with_leaf_script(&l1.script).into(); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 336) let h2: TapNodeHash = TapLeafHash::with_leaf_script(&l2.script).into(); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 337) let branch1 = TapBranchHash::with_nodes(h0, h1); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 338) let node1: TapNodeHash = branch1.clone().into(); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 339) let branch2 = TapBranchHash::with_nodes(node1, h2); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 340) +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 341) let expected_root: TapNodeHash = branch2.clone().into(); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 342) assert_eq!(tree.merkle_root(), expected_root); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 343) +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 344) let p01 = TapMerklePath::try_from(vec![branch1.clone(), branch2.clone()]).unwrap(); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 345) let p2 = TapMerklePath::try_from(vec![branch2]).unwrap(); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 346) +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 347) assert_eq!(tree.merkle_path(0), p01); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 348) assert_eq!(tree.merkle_path(1), p01); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 349) assert_eq!(tree.merkle_path(2), p2); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 350) } +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 351) } +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 352) +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 353) +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 354) +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 355) +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 356) #[derive(Clone, Eq, PartialEq, Hash, Debug)] +3596af55 derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-20 14:48:03 +0200 357) #[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase"))] +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 358) pub struct LeafInfo { +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 359) pub depth: u7, +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 360) pub script: L, +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 361) } +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 362) +dfa39d7c derive/src/taptree.rs (Dr Maxim Orlovsky 2025-05-30 20:18:22 +0200 363) impl LeafInfo { +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 364) pub fn tap_script(depth: u7, script: TapScript) -> Self { +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 365) LeafInfo { +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 366) depth, +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 367) script: LeafScript::from_tap_script(script), +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 368) } +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 369) } +f2ff7c93 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 13:48:50 +0200 370) } +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 371) +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 372) #[derive(Getters, Clone, Eq, PartialEq, Debug)] +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 373) #[getter(as_copy)] +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 374) pub struct ControlBlockFactory { +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 375) internal_pk: InternalPk, +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 376) output_pk: OutputPk, +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 377) parity: Parity, +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 378) merkle_root: TapNodeHash, +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 379) +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 380) #[getter(skip)] +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 381) merkle_paths: Vec, +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 382) #[getter(skip)] +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 383) remaining: Vec>, +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 384) } +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 385) +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 386) impl ControlBlockFactory { +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 387) #[inline] +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 388) pub fn with(internal_pk: InternalPk, tap_tree: TapTree) -> Self { +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 389) let merkle_root = tap_tree.merkle_root(); +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 390) let (output_pk, parity) = internal_pk.to_output_pk(Some(merkle_root)); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 391) let remaining_leaves = tap_tree.clone().into_vec(); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 392) let merkle_paths = (0 .. remaining_leaves.len()) +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 393) .map(|i| tap_tree.merkle_path(i)) +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 394) .collect(); +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 395) ControlBlockFactory { +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 396) internal_pk, +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 397) output_pk, +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 398) parity, +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 399) merkle_root, +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 400) merkle_paths, +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 401) remaining: remaining_leaves, +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 402) } +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 403) } +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 404) +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 405) #[inline] +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 406) pub fn into_remaining_leaves(self) -> Vec { self.remaining } +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 407) } +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 408) +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 409) impl Iterator for ControlBlockFactory { +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 410) type Item = (ControlBlock, LeafScript); +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 411) fn next(&mut self) -> Option { +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 412) // Pop leaf and its path together +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 413) let leaf = self.remaining.pop()?; +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 414) let path = self.merkle_paths.pop()?; +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 415) let leaf_script = leaf.script; +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 416) // Build control block with the correct path +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 417) let control_block = ControlBlock::with( +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 418) leaf_script.version, +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 419) self.internal_pk, +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 420) self.parity, +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 421) path, +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 422) ); +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 423) Some((control_block, leaf_script)) +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 424) } +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 425) } +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 426) +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 427) /// A compact size unsigned integer representing the number of leaf hashes, followed by a list +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 428) /// of leaf hashes, followed by the 4 byte master key fingerprint concatenated with the +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 429) /// derivation path of the public key. The derivation path is represented as 32-bit little +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 430) /// endian unsigned integer indexes concatenated with each other. Public keys are those needed +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 431) /// to spend this output. The leaf hashes are of the leaves which involve this public key. The +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 432) /// internal key does not have leaf hashes, so can be indicated with a hashes len of 0. +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 433) /// Finalizers should remove this field after `PSBT_IN_FINAL_SCRIPTWITNESS` is constructed. +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 434) #[derive(Clone, Eq, PartialEq, Hash, Debug)] +9a8056f9 derive/src/taptree.rs (Dr Maxim Orlovsky 2024-12-09 13:48:03 +0100 435) #[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase"))] +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 436) pub struct TapDerivation { +c31646bd std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 19:00:05 +0200 437) pub leaf_hashes: Vec, +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 438) pub origin: KeyOrigin, +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 439) } +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 440) +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 441) impl TapDerivation { +37288799 derive/src/taptree.rs (Dr Maxim Orlovsky 2024-06-29 16:41:17 +0200 442) pub fn with_internal_pk(xpub_origin: XkeyOrigin, terminal: Terminal) -> Self { +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 443) let origin = KeyOrigin::with(xpub_origin, terminal); +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 444) TapDerivation { +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 445) leaf_hashes: empty!(), +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 446) origin, +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 447) } +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 448) } +178fb649 std/src/taptree.rs (Dr Maxim Orlovsky 2023-10-06 17:40:14 +0200 449) } +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 450) +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 451) +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 452) +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 453) #[cfg(test)] +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 454) mod control_block_factory_tests { +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 455) use super::*; // ControlBlockFactory +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 456) use amplify::num::u7; +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 457) use std::convert::TryFrom; +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 458) use crate::taptree::TapTree; +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 459) use bc::{InternalPk, LeafVer, ScriptBytes, TapBranchHash, TapLeafHash, TapNodeHash}; +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 460) +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 461) /// Fixed X-only pubkey (32×0x02) +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 462) fn dummy_internal_pk() -> InternalPk { +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 463) InternalPk::from_byte_array([0x02u8; 32]).unwrap() +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 464) } +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 465) +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 466) #[test] +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 467) fn factory_preserves_paths_and_versions() { +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 468) let depth = u7::ONE; +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 469) let leaves: Vec> = vec![ +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 470) LeafInfo { +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 471) script: LeafScript::new( +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 472) LeafVer::from_consensus_u8(0xc0).unwrap(), +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 473) ScriptBytes::try_from(vec![10]).unwrap(), +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 474) ), +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 475) depth, +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 476) }, +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 477) LeafInfo { +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 478) script: LeafScript::new( +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 479) LeafVer::from_consensus_u8(0xc0).unwrap(), +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 480) ScriptBytes::try_from(vec![20]).unwrap(), +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 481) ), +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 482) depth, +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 483) }, +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 484) ]; +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 485) let clone_leaves = leaves.clone(); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 486) +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 487) // Constructing factory and collecting +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 488) let items: Vec<_> = +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 489) ControlBlockFactory::with(dummy_internal_pk(), TapTree(leaves)).collect(); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 490) assert_eq!(items.len(), clone_leaves.len()); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 491) +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 492) // Hand-Calculated Root Hash +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 493) let h0 = TapLeafHash::with_leaf_script(&clone_leaves[0].script).into(); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 494) let h1 = TapLeafHash::with_leaf_script(&clone_leaves[1].script).into(); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 495) let branch = TapBranchHash::with_nodes(h0, h1); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 496) let expected_root: TapNodeHash = branch.clone().into(); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 497) let tree = TapTree(clone_leaves.clone()); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 498) assert_eq!(tree.merkle_root(), expected_root); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 499) +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 500) // Compare the scripts & paths of each ControlBlock +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 501) for (idx, (cb, ls)) in items.into_iter().enumerate() { +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 502) assert_eq!(ls.version, tree.0[idx].script.version); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 503) let expected_path = tree.merkle_path(idx); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 504) assert_eq!(cb.merkle_branch, expected_path); +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 505) } +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 506) } +d347558a derive/src/taptree.rs (Thomzin 2025-07-18 16:46:06 +0800 507) } +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 508) #[cfg(test)] +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 509) mod negative_tests { +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 510) use super::*; +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 511) use amplify::num::u7; +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 512) use std::convert::TryFrom; +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 513) use bc::{LeafScript, LeafVer, ScriptBytes}; +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 514) +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 515) #[test] +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 516) #[should_panic(expected = "unbalanced tap tree")] +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 517) fn merkle_path_empty_tree_panics() { +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 518) // If the merkle path is called directly on an empty tree, it will panic "unbalanced tap tree" because there is no root node. +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 519) let empty: TapTree = TapTree(vec![]); +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 520) let _ = empty.merkle_path(0); +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 521) } +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 522) +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 523) #[test] +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 524) fn tree_from_no_leaves_err() { +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 525) let err = TapTree::from_leaves(std::iter::empty::>()); +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 526) assert!(matches!(err, Err(InvalidTree::Unfinalized(_)))); +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 527) } +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 528) +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 529) #[test] +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 530) fn leaf_depth_overflow_err() { +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 531) assert!(u7::try_from(128u8).is_err()); +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 532) } +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 533) +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 534) #[test] +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 535) fn duplicate_leaves_nonzero_depth_ok() { +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 536) // The same script has a depth of 1 and is repeated twice without underflow. +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 537) let depth = u7::ONE; +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 538) let script = LeafScript::new( +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 539) LeafVer::from_consensus_u8(0xc0).unwrap(), +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 540) ScriptBytes::try_from(vec![]).unwrap(), +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 541) ); +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 542) let leaf = LeafInfo { depth, script }; +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 543) let tree = TapTree(vec![leaf.clone(), leaf.clone()]); +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 544) +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 545) // The root hash should be the result of merging two identical leaf hashes. +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 546) let h = TapLeafHash::with_leaf_script(&leaf.script).into(); +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 547) let expected_root: TapNodeHash = TapBranchHash::with_nodes(h, h).into(); +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 548) assert_eq!(tree.merkle_root(), expected_root); +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 549) +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 550) // The corresponding two paths have only this one branch +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 551) let expected_path = TapMerklePath::try_from(vec![TapBranchHash::with_nodes(h, h)]).unwrap(); +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 552) assert_eq!(tree.merkle_path(0), expected_path); +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 553) assert_eq!(tree.merkle_path(1), expected_path); +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 554) } +b4cd07b9 derive/src/taptree.rs (Thomzin 2025-07-20 14:34:32 +0800 555) } From 36e03cd6c5d905c0c3ae69b36350622e165bdf1f Mon Sep 17 00:00:00 2001 From: Thomzin Date: Fri, 8 Aug 2025 14:04:48 +0800 Subject: [PATCH 4/7] more demos --- derive/Cargo.toml | 2 +- derive/examples/fra_demo.rs | 2 +- derive/examples/fra_demo2.rs | 14 ++- derive/examples/fra_demo3.rs | 190 +++++++++++++++++++++++++++++++ derive/examples/fra_demo4.rs | 210 +++++++++++++++++++++++++++++++++++ derive/src/fra.rs | 1 + 6 files changed, 412 insertions(+), 7 deletions(-) create mode 100644 derive/examples/fra_demo3.rs create mode 100644 derive/examples/fra_demo4.rs diff --git a/derive/Cargo.toml b/derive/Cargo.toml index 0dfb935..03fa3ec 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -29,7 +29,7 @@ bitcoin = "0.32" strict_encoding = { workspace = true} bitcoincore-rpc = "0.19" rand = "0.8.5" # RPC 调用 - +bitcoin_hashes = "0.14" [features] default = [] all = [] diff --git a/derive/examples/fra_demo.rs b/derive/examples/fra_demo.rs index cf1c021..d59654a 100644 --- a/derive/examples/fra_demo.rs +++ b/derive/examples/fra_demo.rs @@ -24,7 +24,7 @@ use bitcoincore_rpc::bitcoin::secp256k1 as bitcoin_secp; // ... (所有帮助函数保持不变) ... fn to_bc_outpoint(rpc_outpoint: OutPoint) -> bc::Outpoint { bc::Outpoint::new( - bc::Txid::from_byte_array(rpc_outpoint.txid.to_byte_array()), + bc::Txid::from_byte_array(Hash::hash(&rpc_outpoint.txid[..]).to_byte_array()), bc::Vout::from_u32(rpc_outpoint.vout) ) } diff --git a/derive/examples/fra_demo2.rs b/derive/examples/fra_demo2.rs index d5117bf..4298aa5 100644 --- a/derive/examples/fra_demo2.rs +++ b/derive/examples/fra_demo2.rs @@ -1,7 +1,6 @@ // derive/examples/fra_demo.rs use std::{thread::sleep, time::Duration}; - use bitcoincore_rpc::{Auth, Client, RpcApi}; use bitcoincore_rpc::bitcoin::{ Address, Amount, Network, @@ -23,6 +22,7 @@ use bitcoincore_rpc::bitcoin::{ use bitcoincore_rpc::json::AddressType; use derive::fra::{FraAction, build_fra_control_blocks}; +use derive::base58::encode; use bc::{InternalPk, OutputPk, XOnlyPk}; use secp256k1::{ Secp256k1 as DeriveSecp, @@ -47,7 +47,7 @@ fn main() -> Result<(), Box> { // (Optional) 挖 101 块,生成成熟 UTXO println!(">> Generating 101 blocks for coinbase maturity..."); let coinbase_addr = rpc.get_new_address(None, Some(AddressType::Legacy))?.require_network(Network::Regtest)?; - rpc.generate_to_address(101, &coinbase_addr)?; + rpc.generate_to_address(210, &coinbase_addr)?; sleep(Duration::from_secs(3)); println!(" Done. Funds are now available."); @@ -113,10 +113,13 @@ fn main() -> Result<(), Box> { // 取出 leaf 脚本字节 let sb = ScriptBuf::from(AsRef::<[u8]>::as_ref(&leaf_script.script).to_vec()); // 不再手动添加 OP_DROP - let tap_info: TaprootSpendInfo = TaprootBuilder::new() - .add_leaf(depth.into(), sb.clone())? + + let tap_info = TaprootBuilder::new() + // 若出错,直接 panic 并打印 e 的 Debug 信息 + .add_leaf(depth.into(), sb.clone()) + .expect("TaprootBuilder::add_leaf 失败") .finalize(&bitcoin_secp, bitcoin_ix) - .expect("Taproot finalize failed"); + .expect("TaprootBuilder::finalize 失败"); let fra_addr = Address::p2tr( &bitcoin_secp, bitcoin_ix, @@ -230,6 +233,7 @@ fn main() -> Result<(), Box> { println!(" Witness[{}] (len {}): {:?}", i, elem.len(), elem); } let raw_spend = serialize(&final_tx); + println!("Raw Spend Tx: {:?}", encode(&raw_spend)); let sid = rpc.send_raw_transaction(&raw_spend[..])?; println!("Spend txid = {}", sid); diff --git a/derive/examples/fra_demo3.rs b/derive/examples/fra_demo3.rs new file mode 100644 index 0000000..199dfe8 --- /dev/null +++ b/derive/examples/fra_demo3.rs @@ -0,0 +1,190 @@ + +use std::{thread::sleep, time::Duration}; +use bitcoincore_rpc::{Auth, Client, RpcApi}; +use bitcoin::{ + Address, Amount, Network, OutPoint, Transaction, TxIn, TxOut, ScriptBuf, Sequence, Witness, + absolute::LockTime, transaction::Version, consensus::encode::serialize, + taproot::{TaprootBuilder, TaprootSpendInfo, LeafVersion, TapLeafHash}, + sighash::{SighashCache, Prevouts, TapSighashType}, + PrivateKey, secp256k1::{Secp256k1, SecretKey, Keypair, XOnlyPublicKey, Message}, +}; +use bitcoincore_rpc::json::AddressType; +use rand::thread_rng; +use derive::base58::{encode, decode}; + +#[allow(unused_imports)] +fn main() -> Result<(), Box> { + // 0) Connect to regtest RPC and load "legacy_true" wallet + let rpc = Client::new( + "http://127.0.0.1:18443/wallet/legacy_true", + Auth::UserPass("foo".into(), "bar".into()), + )?; + // Import private keys + let sender_wif = "cVkVW14o6zBGyhaV2xqMsGEqYijB6jzsK5EkNcmzBPJejGmcBrMQ"; + let receiver_wif = "cPMvbJRTmycMYU3pQ3dTfCwzVEtyVqpEeV3LWNaT1pzHhax2FKZF"; + rpc.import_private_key(&PrivateKey::from_wif(sender_wif)?, None, None)?; + rpc.import_private_key(&PrivateKey::from_wif(receiver_wif)?, None, None)?; + // Generate 101 blocks for coinbase maturity + println!(">> Generating 101 blocks for coinbase maturity..."); + let coinbase_addr = rpc.get_new_address(None, Some(AddressType::Legacy))?.require_network(Network::Regtest)?; + rpc.generate_to_address(210, &coinbase_addr)?; + sleep(Duration::from_secs(3)); + println!(" Done. Funds are now available."); + // Check wallet balance + let balance = rpc.get_balance(None, None)?; + println!("Wallet balance: {} BTC", balance.to_btc()); + + // --------------------------------------------------- + // STEP1: FUNDING — Create a FRA Taproot UTXO + // --------------------------------------------------- + let fund_utxo = rpc.list_unspent(None, None, None, None, None)? + .into_iter() + .find(|utxo| utxo.amount.to_sat() >= 100_000) + .expect("No UTXO with sufficient funds (>= 0.001 BTC)"); + println!("Fund UTXO amount: {} BTC", fund_utxo.amount.to_btc()); + + // 1) Generate Internal KeyPair + let secp = Secp256k1::new(); + let internal_kp = Keypair::new(&secp, &mut thread_rng()); + let (internal_xonly, _) = internal_kp.x_only_public_key(); + println!("Internal PK: {:?}", encode(&internal_xonly.serialize())); + + // 2) Parse sender/receiver private keys + let sender_secret = PrivateKey::from_wif(sender_wif)?.inner; + let receiver_secret = PrivateKey::from_wif(receiver_wif)?.inner; + let sender_kp = Keypair::from_secret_key(&secp, &sender_secret); + let receiver_kp = Keypair::from_secret_key(&secp, &receiver_secret); + let (sender_xonly, _) = sender_kp.x_only_public_key(); + let (receiver_xonly, _) = receiver_kp.x_only_public_key(); + let sender_pk_bytes = sender_xonly.serialize(); + let receiver_pk_bytes = receiver_xonly.serialize(); + println!("Sender PK: {:?}", encode(&sender_pk_bytes)); + println!("Receiver PK: {:?}", encode(&receiver_pk_bytes)); + // Verify public keys + let expected_sender_pk = "d8254e7443d48c701e10dc7ae8e8e429c71f4d07100d3fcc4d374f103759764e"; + let expected_receiver_pk = "a969d4a73fdc45987eb2ec968026045cd8050750956ff0ccca38f5ee1c8032cc"; + if encode(&sender_pk_bytes) != expected_sender_pk { + println!("Sender PK mismatch: expected {}, got {}", expected_sender_pk, encode(&sender_pk_bytes)); + return Err("Sender public key does not match expected value".into()); + } + if encode(&receiver_pk_bytes) != expected_receiver_pk { + println!("Receiver PK mismatch: expected {}, got {}", expected_receiver_pk, encode(&receiver_pk_bytes)); + return Err("Receiver public key does not match expected value".into()); + } + + // 3) Construct FRA script using bitcoin::ScriptBuf + let mut script_bytes = vec![]; + script_bytes.push(0x20); // OP_PUSHDATA1 + script_bytes.extend_from_slice(&sender_pk_bytes); + script_bytes.push(0x7c); // OP_SWAP + script_bytes.push(0xad); // OP_CHECKSIGVERIFY + script_bytes.push(0x20); // OP_PUSHDATA1 + script_bytes.extend_from_slice(&receiver_pk_bytes); + script_bytes.push(0x7c); // OP_SWAP + script_bytes.push(0xac); // OP_CHECKSIG + let tap_script = ScriptBuf::from(script_bytes); + println!("Generated Script Bytes: {:?}", encode(&tap_script.to_bytes())); + // Verify script + let expected_script = "20d8254e7443d48c701e10dc7ae8e8e429c71f4d07100d3fcc4d374f103759764e7cad20a969d4a73fdc45987eb2ec968026045cd8050750956ff0ccca38f5ee1c8032cc7cac"; + assert_eq!(encode(&tap_script.to_bytes()), expected_script); + + // 4) Build Taproot scriptPubKey + let tap_info = TaprootBuilder::new() + .add_leaf(0, tap_script.clone())? + .finalize(&secp, internal_xonly) + .map_err(|e| format!("Taproot finalize failed: {:?}", e))?; + let fra_addr = Address::p2tr(&secp, internal_xonly, tap_info.merkle_root(), Network::Regtest); + let fra_spk = fra_addr.script_pubkey(); + println!("FRA Address: {}", fra_addr); + println!("Taproot Merkle Root: {:?}", tap_info.merkle_root()); + + // 5) Broadcast Funding TX + let send_val = fund_utxo.amount.to_sat().saturating_sub(10_000); + if send_val < 546 { + return Err("Transaction amount too small: must be at least 546 satoshi".into()); + } + let fid = rpc.send_to_address(&fra_addr, Amount::from_sat(send_val), None, None, None, None, None, None)?; + println!(">> STEP1: Funding txid = {}", fid); + rpc.generate_to_address(1, &coinbase_addr)?; + sleep(Duration::from_secs(3)); + + // --------------------------------------------------- + // STEP2: SPENDING — Spend the FRA UTXO + // --------------------------------------------------- + println!(">> STEP2: Finding FRA UTXO from funding tx {}", fid); + let funding_tx = rpc.get_raw_transaction(&fid, None)?; + let (fra_vout, fra_txout) = funding_tx.output.iter().enumerate() + .find(|(_vout, txout)| txout.script_pubkey == fra_spk) + .expect("No output matching fra_spk in funding transaction"); + let fra_outpoint = OutPoint { txid: fid, vout: fra_vout as u32 }; + let fra_amount = fra_txout.value; + println!(" Found FRA UTXO at {}:{} with value {} BTC", fid, fra_vout, fra_amount.to_btc()); + + let left = fra_amount.to_sat().saturating_sub(10_000); + if left < 546 { + return Err("Spend transaction amount too small: must be at least 546 satoshi".into()); + } + let spend_tx = Transaction { + version: Version(2), + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: fra_outpoint.clone(), + script_sig: ScriptBuf::new(), + sequence: Sequence(0xFFFF_FFFF), + witness: Witness::new(), + }], + output: vec![TxOut { + value: Amount::from_sat(left), + script_pubkey: rpc.get_new_address(None, Some(AddressType::Legacy))?.require_network(Network::Regtest)?.script_pubkey(), + }], + }; + + // 6) Calculate Taproot Sighash + let mut cache = SighashCache::new(&spend_tx); + let tapleaf_hash = TapLeafHash::from_script(&tap_script, LeafVersion::TapScript); + let sighash = cache.taproot_script_spend_signature_hash( + 0, + &Prevouts::All(&[fra_txout.clone()]), + tapleaf_hash, + TapSighashType::Default, + )?; + let msg = Message::from_digest_slice(sighash.as_ref())?; + println!("Sighash: {:?}", encode(sighash.as_ref())); + + // 7) Generate Schnorr signatures + let sig_sender = secp.sign_schnorr(&msg, &sender_kp); + let sig_receiver = secp.sign_schnorr(&msg, &receiver_kp); + let is_sender_sig_valid = secp.verify_schnorr(&sig_sender, &msg, &sender_xonly); + let is_receiver_sig_valid = secp.verify_schnorr(&sig_receiver, &msg, &receiver_xonly); + println!("Sender signature valid: {}", is_sender_sig_valid.is_ok()); + println!("Receiver signature valid: {}", is_receiver_sig_valid.is_ok()); + let sig_sender_bytes = sig_sender.as_ref().to_vec(); + let sig_receiver_bytes = sig_receiver.as_ref().to_vec(); + println!("Sig Sender: {:?}", encode(&sig_sender_bytes)); + println!("Sig Receiver: {:?}", encode(&sig_receiver_bytes)); + + // 8) Generate Control Block + let control_block_bytes = tap_info.control_block(&(tap_script.clone(), LeafVersion::TapScript)) + .expect("Failed to get control block") + .serialize(); + println!("Control Block: {:?}", encode(&control_block_bytes)); + + // 9) Assemble Witness and broadcast + let mut final_tx = spend_tx.clone(); + final_tx.input[0].witness = Witness::from_slice(&[ + sig_sender_bytes, // Corresponds to sender_pk (OP_CHECKSIGVERIFY) + sig_receiver_bytes, // Corresponds to receiver_pk (OP_CHECKSIG) + tap_script.to_bytes(), + control_block_bytes, + ]); + println!("Final Witness elements:"); + for (i, elem) in final_tx.input[0].witness.iter().enumerate() { + println!(" Witness[{}] (len {}): {:?}", i, elem.len(), encode(elem)); + } + + let raw_spend = serialize(&final_tx); + println!("Raw Spend Tx: {:?}", encode(&raw_spend)); + let sid = rpc.send_raw_transaction(&raw_spend)?; + println!("Spend txid = {}", sid); + Ok(()) +} diff --git a/derive/examples/fra_demo4.rs b/derive/examples/fra_demo4.rs new file mode 100644 index 0000000..fc83ad7 --- /dev/null +++ b/derive/examples/fra_demo4.rs @@ -0,0 +1,210 @@ +// fra_demo4_method_a.rs +use std::{thread::sleep, time::Duration, str::FromStr}; + +use bitcoincore_rpc::{Auth, Client, RpcApi}; +use bitcoincore_rpc::bitcoin::{ + Address as RpcAddress, + Amount, + Network, + OutPoint, + secp256k1::{Secp256k1, Keypair, SecretKey, XOnlyPublicKey, Message}, +}; +use bitcoincore_rpc::json::AddressType; +use bitcoin_hashes::Hash; + +// bp-std / bp-core 相关类型(保持主导地位) +use derive::{ + fra::{FraAction, build_fra_control_blocks}, + XOnlyPk, +}; +use bc::{ + self, + ConsensusEncode, + ScriptPubkey, + SighashCache, + Witness, +}; +use amplify::{Wrapper, ByteArray}; + +/// helper: rpc OutPoint -> bc::Outpoint +fn to_bc_outpoint(rpc_out: OutPoint) -> bc::Outpoint { + // 【修改】bitcoincore-rpc 的 Txid 需要先 as_hash() 再 to_byte_array() + bc::Outpoint::new( + bc::Txid::from_byte_array(rpc_out.txid.to_byte_array()), + bc::Vout::from_u32(rpc_out.vout), + ) +} + +/// helper: rpc TxOut -> bc::TxOut +fn to_bc_txout(rpc_txout: bitcoincore_rpc::bitcoin::TxOut) -> bc::TxOut { + bc::TxOut { + value: bc::Sats::from(rpc_txout.value.to_sat()), + script_pubkey: ScriptPubkey::from_inner( + bc::ScriptBytes::try_from(rpc_txout.script_pubkey.to_bytes()).unwrap() + ), + } +} + +fn main() -> Result<(), Box> { + // 0) RPC + 钱包 + let rpc = Client::new( + "http://127.0.0.1:18443/wallet/legacy_true", + Auth::UserPass("foo".into(), "bar".into()), + )?; + // 确保用于 demo 的两个私钥已导入钱包(只是为了方便广播 funding tx) + rpc.import_private_key(&bitcoincore_rpc::bitcoin::PrivateKey::from_wif( + "cVkVW14o6zBGyhaV2xqMsGEqYijB6jzsK5EkNcmzBPJejGmcBrMQ" + )?, None, None)?; + rpc.import_private_key(&bitcoincore_rpc::bitcoin::PrivateKey::from_wif( + "cPMvbJRTmycMYU3pQ3dTfCwzVEtyVqpEeV3LWNaT1pzHhax2FKZF" + )?, None, None)?; + + // 1) 挖 101 确保 coinbase 成熟 + let coinbase_addr = rpc.get_new_address(None, Some(AddressType::Legacy))? + .require_network(Network::Regtest)?; + rpc.generate_to_address(101, &coinbase_addr)?; + sleep(Duration::from_secs(3)); + + // 2) 选 UTXO + let balance = rpc.get_balance(None, None)?; + println!("Wallet balance: {} BTC", balance.to_btc()); + let fund_utxo = rpc.list_unspent(None, None, None, None, None)? + .into_iter() + .find(|u| u.amount.to_sat() >= 100_000) + .expect("没有足够的 UTXO (>= 0.001 BTC)"); + + // 3) 生成 internal keypair(用于 Taproot internal key) + let secp = Secp256k1::new(); + let internal_kp = Keypair::new(&secp, &mut rand::thread_rng()); + // 【注意】这里保留 bitcoin::secp256k1::XOnlyPublicKey 以便传给 rust-bitcoin::Address::p2tr + let internal_xonly_key: XOnlyPublicKey = internal_kp.public_key().x_only_public_key().0; + + // 同时为了 bp-core 使用,将其转换为 bp-core 的 XOnlyPk -> InternalPk + let internal_x_bytes = internal_xonly_key.serialize(); + let internal_xonly = XOnlyPk::from_byte_array(internal_x_bytes).expect("bad internal xonly"); + let internal_pk = bc::InternalPk::from_unchecked(internal_xonly); + + // 4) 解析 sender / receiver 私钥,并建 keypairs(用于签名) + let sender_priv = bitcoincore_rpc::bitcoin::PrivateKey::from_wif( + "cVkVW14o6zBGyhaV2xqMsGEqYijB6jzsK5EkNcmzBPJejGmcBrMQ" + )?; + let recv_priv = bitcoincore_rpc::bitcoin::PrivateKey::from_wif( + "cPMvbJRTmycMYU3pQ3dTfCwzVEtyVqpEeV3LWNaT1pzHhax2FKZF" + )?; + let sender_sk = SecretKey::from_slice(&sender_priv.inner.secret_bytes())?; + let recv_sk = SecretKey::from_slice(&recv_priv.inner.secret_bytes())?; + let sender_kp = Keypair::from_secret_key(&secp, &sender_sk); + let recv_kp = Keypair::from_secret_key(&secp, &recv_sk); + + // 构造 bp-core XOnlyPk(用于脚本中的公钥字节) + let sender_xonly_key = sender_kp.public_key().x_only_public_key().0; + let recv_xonly_key = recv_kp.public_key().x_only_public_key().0; + let sender_xonly = XOnlyPk::from_byte_array(sender_xonly_key.serialize()).expect("bad sender"); + let recv_xonly = XOnlyPk::from_byte_array(recv_xonly_key.serialize()).expect("bad recv"); + + // 5) 构造 FRA Transfer leaf(bp-std) + let action = FraAction::Transfer { + asset_id: [0u8; 32], + amount: 1000, + receiver: recv_xonly, + sender: sender_xonly, + }; + let depth: amplify::num::u7 = 0u8.try_into().unwrap(); + + // 6) 使用 bp-std 构建 ControlBlock + LeafScript + let proofs = build_fra_control_blocks(internal_pk.clone(), vec![(action, depth)]); + let (control_block, leaf_script) = &proofs[0]; + + println!("⛑️ ControlBlock bytes: {:?}", control_block.consensus_serialize()); + println!("⛑️ LeafScript bytes: {:?}", leaf_script.script.as_inner()); + + // 7) 计算 leaf hash (bp-core TapLeafHash) + let leaf_hash = leaf_script.tap_leaf_hash(); // bp-core 类型 + + // --------------------------- + // 【关键:Method A 的单一转换点】 + // 在此把 bp-core 的 TapLeafHash(bytes) -> bitcoin::TapNodeHash(rust-bitcoin) + // 仅此一次的字节级转换,然后传给 Address::p2tr。 + // 这样 rust-bitcoin 会自己计算 tweak(internal_xonly + merkle_root), + // 并生成与 bp-core 相同的 tweaked output key。 + // --------------------------- + let inner_arr = leaf_hash.into_inner(); // 得到 amplify::Array + let inner_bytes = inner_arr.to_byte_array(); // -> [u8; 32] + let merkle_root = bitcoin::TapNodeHash::from_slice(&inner_bytes) + .expect("Invalid tap node hash"); + + + // 8) 用 rust-bitcoin 的 Address::p2tr 生成带脚本路径的 P2TR 地址 + // 传入 internal_xonly_key 和 merkle_root(上一步转好的) + let fra_addr = bitcoincore_rpc::bitcoin::Address::p2tr( + &secp, + internal_xonly_key, + Some(merkle_root), + Network::Regtest, + ); + let fra_spk = fra_addr.script_pubkey(); + println!("FRA Taproot 地址: {}", fra_addr); + + // 9) 广播 Funding TX(钱包自动签名) + let rpc_addr = RpcAddress::from_str(&fra_addr.to_string())?.assume_checked(); + let fid = rpc.send_to_address( + &rpc_addr, + Amount::from_sat(fund_utxo.amount.to_sat().saturating_sub(10_000)), + None, None, None, None, None, None, + )?; + // 确认 funding + rpc.generate_to_address(1, &coinbase_addr)?; + sleep(Duration::from_secs(3)); + + // 10) 找到 funding 输出并构造要花费的交易(用 bp-core 的 Tx) + let funding_tx = rpc.get_raw_transaction(&fid, None)?; + let (idx, found_vout) = funding_tx.output.iter().enumerate() + .find(|(_, o)| o.script_pubkey.to_bytes() == fra_spk.to_bytes()) + .expect("FRA UTXO not found"); + let fra_outpoint = OutPoint { txid: fid, vout: idx as u32 }; + + let dest_addr = rpc.get_new_address(None, Some(AddressType::Legacy))?.assume_checked(); + let mut spend_tx = bc::Tx { + version: bc::TxVer::V2, + lock_time: bc::LockTime::ZERO, + inputs: bc::VarIntArray::from_iter_checked([bc::TxIn { + prev_output: to_bc_outpoint(fra_outpoint.clone()), + sig_script: bc::SigScript::new(), + sequence: bc::SeqNo::from_consensus_u32(0xFFFF_FFFF), + witness: bc::Witness::new(), + }]), + outputs: bc::VarIntArray::from_iter_checked([bc::TxOut { + value: bc::Sats::from(found_vout.value.to_sat().saturating_sub(10_000)), + script_pubkey: ScriptPubkey::from_inner( + bc::ScriptBytes::try_from(dest_addr.script_pubkey().to_bytes()).unwrap() + ), + }]), + }; + + // 11) 计算 sighash(仍然用 bp-core 的 SighashCache) + let prevout_bc = to_bc_txout(found_vout.clone()); + let mut cache = SighashCache::new(&mut spend_tx, vec![prevout_bc])?; + let sighash = cache.tap_sighash_script(0, leaf_hash, None)?; // bp-core TapSighash + let sighash_bytes: [u8; 32] = sighash.into(); // 转成 32 字节数组 + let msg = Message::from_digest_slice(&sighash_bytes).expect("32 bytes"); + + + // 12) signer:双方用 schnorr 签名(message + keypair) + let sig_sender = secp.sign_schnorr(&msg, &sender_kp); + let sig_receiver = secp.sign_schnorr(&msg, &recv_kp); + + // 13) 组装 witness(遵循脚本:接收方签名在前 -> 发送方签名 -> script -> control_block) + spend_tx.inputs[0].witness = Witness::from_consensus_stack(vec![ + sig_receiver.as_ref().to_vec(), + sig_sender.as_ref().to_vec(), + leaf_script.script.as_inner().to_vec(), + control_block.consensus_serialize(), + ]); + + // 14) 广播(raw) + let raw = spend_tx.consensus_serialize(); + let sid = rpc.send_raw_transaction(&raw)?; + println!("🎉 Spend TXID = {}", sid); + + Ok(()) +} diff --git a/derive/src/fra.rs b/derive/src/fra.rs index 3306e52..85e0ed0 100644 --- a/derive/src/fra.rs +++ b/derive/src/fra.rs @@ -189,6 +189,7 @@ pub fn build_fra_script(action: FraAction) -> TapScript { push_bytes(&mut buf, &sender.to_byte_array()); // 压入发送方公钥 buf.push(TapCode::Swap as u8); // 交换 -> [sig_sender], [sender_pk] buf.push(TapCode::CheckSig as u8); // 验证 + } // —— 增发动作 —— FraAction::Mint { asset_id, amount, receiver, minter } => { From 13e792eaf425b404b560856ac804cb17a7137255 Mon Sep 17 00:00:00 2001 From: Thomzin Date: Tue, 12 Aug 2025 22:51:40 +0800 Subject: [PATCH 5/7] testing --- derive/examples/fra_demo4.rs | 36 +++-- derive/examples/fra_demo5.rs | 194 +++++++++++++++++++++++++++ derive/src/fra.rs | 250 ++++++++++++++--------------------- 3 files changed, 319 insertions(+), 161 deletions(-) create mode 100644 derive/examples/fra_demo5.rs diff --git a/derive/examples/fra_demo4.rs b/derive/examples/fra_demo4.rs index fc83ad7..2a6e9ba 100644 --- a/derive/examples/fra_demo4.rs +++ b/derive/examples/fra_demo4.rs @@ -11,12 +11,14 @@ use bitcoincore_rpc::bitcoin::{ }; use bitcoincore_rpc::json::AddressType; use bitcoin_hashes::Hash; +use amplify::hex::ToHex; // bp-std / bp-core 相关类型(保持主导地位) use derive::{ fra::{FraAction, build_fra_control_blocks}, XOnlyPk, }; +use bitcoin::consensus::encode; use bc::{ self, ConsensusEncode, @@ -120,7 +122,7 @@ fn main() -> Result<(), Box> { // 7) 计算 leaf hash (bp-core TapLeafHash) let leaf_hash = leaf_script.tap_leaf_hash(); // bp-core 类型 - + println!("Leaf Hash: {:?}", leaf_hash.to_byte_array().to_hex()); // --------------------------- // 【关键:Method A 的单一转换点】 // 在此把 bp-core 的 TapLeafHash(bytes) -> bitcoin::TapNodeHash(rust-bitcoin) @@ -184,26 +186,42 @@ fn main() -> Result<(), Box> { // 11) 计算 sighash(仍然用 bp-core 的 SighashCache) let prevout_bc = to_bc_txout(found_vout.clone()); let mut cache = SighashCache::new(&mut spend_tx, vec![prevout_bc])?; - let sighash = cache.tap_sighash_script(0, leaf_hash, None)?; // bp-core TapSighash - let sighash_bytes: [u8; 32] = sighash.into(); // 转成 32 字节数组 + + // 直接调用我们刚刚修复的函数 + let sighash = cache.tap_sighash_script(0, leaf_hash, None)?; + let sighash_bytes: [u8; 32] = sighash.into(); + + println!("Sighash: {}", sighash_bytes.to_hex()); + let msg = Message::from_digest_slice(&sighash_bytes).expect("32 bytes"); // 12) signer:双方用 schnorr 签名(message + keypair) let sig_sender = secp.sign_schnorr(&msg, &sender_kp); let sig_receiver = secp.sign_schnorr(&msg, &recv_kp); - - // 13) 组装 witness(遵循脚本:接收方签名在前 -> 发送方签名 -> script -> control_block) + println!("sig_sender length: {}", sig_sender.as_ref().len()); + println!("sig_receiver length: {}", sig_receiver.as_ref().len()); + + // 13) 组装 witness (必须严格遵循脚本的预期顺序!) + // 脚本逻辑 (来自 fra.rs): + // OP_SWAP OP_CHECKSIGVERIFY OP_SWAP OP_CHECKSIG + // + // 预期的 Witness (从栈顶 -> 栈底): + // - sender_sig + // - receiver_sig spend_tx.inputs[0].witness = Witness::from_consensus_stack(vec![ - sig_receiver.as_ref().to_vec(), - sig_sender.as_ref().to_vec(), + sig_sender.as_ref().to_vec(), // 对应 OP_CHECKSIG + sig_receiver.as_ref().to_vec(), // 对应 OP_CHECKSIGVERIFY leaf_script.script.as_inner().to_vec(), control_block.consensus_serialize(), ]); // 14) 广播(raw) - let raw = spend_tx.consensus_serialize(); - let sid = rpc.send_raw_transaction(&raw)?; + let raw_bytes = spend_tx.consensus_serialize(); + // 将字节转换为十六进制字符串 + let raw_hex = raw_bytes.to_hex(); + println!("RAW_TX_HEX: {}", raw_hex); + let sid = rpc.send_raw_transaction(&*raw_hex)?; println!("🎉 Spend TXID = {}", sid); Ok(()) diff --git a/derive/examples/fra_demo5.rs b/derive/examples/fra_demo5.rs new file mode 100644 index 0000000..448d105 --- /dev/null +++ b/derive/examples/fra_demo5.rs @@ -0,0 +1,194 @@ +// fra_demo5_final_solution_v3.rs + +use std::str::FromStr; // 保留以备 Address::from_str 使用 + +// --- 修正后的 rust-bitcoin 导入 --- +use bitcoin::{ + self, + consensus::encode, + hashes::Hash, + secp256k1::SecretKey, + key::{Keypair, Secp256k1}, + absolute::LockTime, + network::Network, + sighash::{self, Prevouts, SighashCache, TapSighash}, + taproot::{self, LeafVersion, TaprootBuilder}, + Address, Amount, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Witness, +}; + +// --- RPC 库保持不变 --- +use bitcoincore_rpc::{Auth, Client, RpcApi}; +use bitcoincore_rpc::json::AddressType; + +// --- 仍然需要的 bp-core/std 类型 --- +use derive::{ + fra::{build_fra_script, FraAction}, + XOnlyPk, +}; + +fn main() -> Result<(), Box> { + // =================================================================== + // 步骤 0-2: RPC 设置和 UTXO 准备 + // =================================================================== + let rpc = Client::new( + "http://127.0.0.1:18443/wallet/legacy_true", + Auth::UserPass("foo".into(), "bar".into()), + )?; + rpc.import_private_key( + &bitcoin::PrivateKey::from_wif("cVkVW14o6zBGyhaV2xqMsGEqYijB6jzsK5EkNcmzBPJejGmcBrMQ")?, + None, + None, + )?; + rpc.import_private_key( + &bitcoin::PrivateKey::from_wif("cPMvbJRTmycMYU3pQ3dTfCwzVEtyVqpEeV3LWNaT1pzHhax2FKZF")?, + None, + None, + )?; + // --- 关键修正: Address 类型 --- + let coinbase_addr_unchecked = rpc.get_new_address(None, Some(AddressType::Legacy))?; + let coinbase_addr = coinbase_addr_unchecked.require_network(Network::Regtest)?; + rpc.generate_to_address(101, &coinbase_addr)?; // 现在类型匹配 + let balance = rpc.get_balance(None, None)?; + println!("Wallet balance: {} BTC", balance); + let fund_utxo = rpc + .list_unspent(None, None, None, None, None)? + .into_iter() + .find(|u| u.amount.to_sat() >= 100_000) + .expect("没有足够的 UTXO (>= 0.001 BTC)"); + + // =================================================================== + // 步骤 3-4: 密钥生成 + // =================================================================== + let secp = Secp256k1::new(); + let internal_kp = Keypair::new(&secp, &mut rand::thread_rng()); + let internal_pk = internal_kp.x_only_public_key().0; + + let sender_sk = SecretKey::from_slice( + &bitcoin::PrivateKey::from_wif("cVkVW14o6zBGyhaV2xqMsGEqYijB6jzsK5EkNcmzBPJejGmcBrMQ")? + .inner + .secret_bytes(), + )?; + let sender_kp = Keypair::from_secret_key(&secp, &sender_sk); + let sender_pk = sender_kp.x_only_public_key().0; + + let recv_sk = SecretKey::from_slice( + &bitcoin::PrivateKey::from_wif("cPMvbJRTmycMYU3pQ3dTfCwzVEtyVqpEeV3LWNaT1pzHhax2FKZF")? + .inner + .secret_bytes(), + )?; + let recv_kp = Keypair::from_secret_key(&secp, &recv_sk); + let recv_pk = recv_kp.x_only_public_key().0; + + // =================================================================== + // 步骤 5-8: 地址生成 + // =================================================================== + // --- 关键修正: XOnlyPk 构造 --- + let action = FraAction::Transfer { + asset_id: [0u8; 32], + amount: 1000, + receiver: XOnlyPk::from_byte_array(recv_pk.serialize()).unwrap(), + sender: XOnlyPk::from_byte_array(sender_pk.serialize()).unwrap(), + }; + // --- 关键修正: .into_inner() 已被废弃, 使用 .release() --- + let leaf_script_bytes = build_fra_script(action).as_inner().to_vec(); + let script = ScriptBuf::from(leaf_script_bytes); + println!("Leaf Script ({} bytes): {}", script.len(), script.to_hex_string()); + + let builder = TaprootBuilder::new().add_leaf(0, script.clone()).unwrap(); + let spend_info = builder.finalize(&secp, internal_pk).unwrap(); + let fra_addr = Address::p2tr(&secp, internal_pk, spend_info.merkle_root(), Network::Regtest); + println!("FRA Taproot 地址: {}", fra_addr); + + // =================================================================== + // 步骤 9-10: 交易注资 + // =================================================================== + // --- 关键修正: RPC 地址类型 --- + let rpc_address = Address::from_str(&fra_addr.to_string())?.assume_checked(); + let funding_txid = rpc.send_to_address( + &rpc_address, + Amount::from_sat(fund_utxo.amount.to_sat() - 10_000), + None, None, None, None, None, None, + )?; + rpc.generate_to_address(1, &coinbase_addr)?; // 现在类型匹配 + println!("Funding TXID: {}", funding_txid); + let funding_tx_raw = rpc.get_raw_transaction(&funding_txid, None)?; + let (vout, prevout_value) = funding_tx_raw + .output + .iter() + .enumerate() + .find(|(_, o)| o.script_pubkey == fra_addr.script_pubkey()) + .map(|(i, o)| (i as u32, o.value)) + .expect("FRA UTXO not found in funding tx"); + let fra_outpoint = OutPoint { + txid: funding_txid, + vout, + }; + + // =================================================================== + // 步骤 11: 构建花费交易 + // =================================================================== + let dest_addr_unchecked = rpc.get_new_address(None, Some(AddressType::Legacy))?; + let dest_addr = dest_addr_unchecked.require_network(Network::Regtest)?; + let mut spend_tx = Transaction { + version: bitcoin::transaction::Version(2), + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: fra_outpoint, + script_sig: ScriptBuf::new(), + sequence: Sequence::MAX, + witness: Witness::new(), + }], + output: vec![TxOut { + value: prevout_value - Amount::from_sat(10_000), + script_pubkey: dest_addr.script_pubkey(), + }], + }; + let prevouts = vec![TxOut { + value: prevout_value, + script_pubkey: fra_addr.script_pubkey(), + }]; + + // =================================================================== + // 步骤 12: 计算 Sighash (使用修正后的 API) + // =================================================================== + let mut sighasher = SighashCache::new(&spend_tx); + let leaf_hash = taproot::TapLeafHash::from_script(&script, LeafVersion::TapScript); + let sighash: TapSighash = sighasher + .taproot_script_spend_signature_hash( + 0, + &Prevouts::All(&prevouts), + leaf_hash, + sighash::TapSighashType::Default, + )?; + + let msg = bitcoin::secp256k1::Message::from(sighash); + println!("Sighash (rust-bitcoin): {}", sighash.to_string()); // .to_hex_string() -> .to_string() + + // =================================================================== + // 步骤 13: 签名并构建 Witness + // =================================================================== + let sig_sender = secp.sign_schnorr(&msg, &sender_kp); + let sig_receiver = secp.sign_schnorr(&msg, &recv_kp); + + let control_block = spend_info + .control_block(&(script.clone(), LeafVersion::TapScript)) + .unwrap(); + + let mut witness = Witness::new(); + witness.push(sig_sender.as_ref()); + witness.push(sig_receiver.as_ref()); + witness.push(script); + witness.push(control_block.serialize()); + spend_tx.input[0].witness = witness; + + // =================================================================== + // 步骤 14: 广播交易 + // =================================================================== + let tx_hex = encode::serialize_hex(&spend_tx); + println!("Final TX Hex: {}", tx_hex); + + let final_txid = rpc.send_raw_transaction(&*tx_hex)?; // Pass String by value + println!("\n🎉🎉🎉 交易成功广播! TXID = {} 🎉🎉🎉", final_txid); + + Ok(()) +} \ No newline at end of file diff --git a/derive/src/fra.rs b/derive/src/fra.rs index 85e0ed0..9884a81 100644 --- a/derive/src/fra.rs +++ b/derive/src/fra.rs @@ -1,24 +1,12 @@ + use amplify::num::u7; use bc::{ TapScript, TapCode, ControlBlock, LeafScript, XOnlyPk, - InternalPk, OutputPk}; - + InternalPk, OutputPk +}; use crate::taptree::{TapTree, LeafInfo, ControlBlockFactory}; -/// —— FRA 操作对应的 OP_SUCCESSxx 常量 —— -const OP_FRA_TRANSFER: TapCode = TapCode::Success80; -const OP_FRA_MINT: TapCode = TapCode::Success98; -const OP_FRA_BURN: TapCode = TapCode::Success126; -const OP_FRA_ROLLBACK: TapCode = TapCode::Success127; -const OP_FRA_REDEEM: TapCode = TapCode::Success128; -const OP_FRA_SPLIT: TapCode = TapCode::Success129; -const OP_FRA_MERGE: TapCode = TapCode::Success131; -const OP_FRA_FREEZE: TapCode = TapCode::Success132; -const OP_FRA_UNFREEZE: TapCode = TapCode::Success133; -const OP_FRA_GRANT_ROLE: TapCode = TapCode::Success134; -const OP_FRA_REVOKE_ROLE: TapCode = TapCode::Success137; -const OP_FRA_UPGRADE: TapCode = TapCode::Success138; -const OP_FRA_METADATA_UPDATE: TapCode = TapCode::Success141; +// --- Helper 函数 --- /// Helper: 将字节块编码到脚本缓冲 fn push_bytes(buf: &mut Vec, data: &[u8]) { @@ -26,8 +14,7 @@ fn push_bytes(buf: &mut Vec, data: &[u8]) { if len == 0 { buf.push(TapCode::PushBytes0 as u8); } else if len <= 75 { - let opcode = (TapCode::PushBytes1 as u8).wrapping_add((len - 1) as u8); - buf.push(opcode); + buf.push((TapCode::PushBytes1 as u8).wrapping_add((len - 1) as u8)); } else if len < 0x100 { buf.push(TapCode::PushData1 as u8); buf.push(len as u8); @@ -43,43 +30,22 @@ fn push_bytes(buf: &mut Vec, data: &[u8]) { /// Helper: 将整数编码为最小脚本数字或字节块推送 fn push_int(buf: &mut Vec, value: u64) { - match value { - 0 => buf.push(TapCode::PushBytes0 as u8), - 1..=16 => { - let code = match value { - 1 => TapCode::PushNum1, - 2 => TapCode::PushNum2, - 3 => TapCode::PushNum3, - 4 => TapCode::PushNum4, - 5 => TapCode::PushNum5, - 6 => TapCode::PushNum6, - 7 => TapCode::PushNum7, - 8 => TapCode::PushNum8, - 9 => TapCode::PushNum9, - 10 => TapCode::PushNum10, - 11 => TapCode::PushNum11, - 12 => TapCode::PushNum12, - 13 => TapCode::PushNum13, - 14 => TapCode::PushNum14, - 15 => TapCode::PushNum15, - 16 => TapCode::PushNum16, - _ => unreachable!(), - }; - buf.push(code as u8); - } - _ => { - let mut v = value; - let mut bytes = Vec::new(); - while v > 0 { - bytes.push((v & 0xFF) as u8); - v >>= 8; - } - if bytes.last().map_or(false, |b| b & 0x80 != 0) { - bytes.push(0); - } - push_bytes(buf, &bytes); - } + if value == 0 { + buf.push(TapCode::PushBytes0 as u8); + return; + } + if (1..=16).contains(&value) { + buf.push((TapCode::PushNum1 as u8) + (value - 1) as u8); + return; + } + let mut bytes = value.to_le_bytes().to_vec(); + while bytes.last() == Some(&0) { + bytes.pop(); } + if bytes.last().map_or(false, |&b| b & 0x80 != 0) { + bytes.push(0); + } + push_bytes(buf, &bytes); } /// 支持的 FRA 操作类型 @@ -89,67 +55,65 @@ pub enum FraAction { Transfer { asset_id: [u8; 32], // 资产标识 amount: u64, // 转账数量 - receiver: XOnlyPk, // 接收方公钥 - sender: XOnlyPk, // 发送方公钥 + receiver: XOnlyPk, // 接收方公钥 + sender: XOnlyPk, // 发送方公钥 }, /// 增发:单方授权,只有增发者能执行 Mint { - asset_id: [u8; 32], // 资产标识 - amount: u64, // 增发数量 - receiver: OutputPk, // 接收方公钥 - minter: XOnlyPk, // 增发者公钥 + asset_id: [u8; 32], + amount: u64, + receiver: OutputPk, + minter: XOnlyPk, }, - /// 销毁(燃烧) + /// 销毁 Burn { - asset_id: [u8; 32], // 资产标识 - amount: u64, // 燃烧数量 - owner: XOnlyPk, // 授权者公钥 + asset_id: [u8; 32], + amount: u64, + owner: XOnlyPk, }, /// 回退:持有者主动返还给发行者 Rollback { - asset_id: [u8; 32], // 资产标识 - amount: u64, // 回退数量 - owner: XOnlyPk, // 资产持有者公钥 - minter: XOnlyPk, // 发行者(归还目标)公钥 + asset_id: [u8; 32], + amount: u64, + owner: XOnlyPk, + minter: XOnlyPk, }, /// 赎回:返还底层资产或销毁代币 Redeem { - asset_id: [u8; 32], // 资产标识 - amount: u64, // 赎回数量 - owner: XOnlyPk, // 授权者公钥 - // 可扩展:time_lock: Option, // 时间锁或其他条件 + asset_id: [u8; 32], + amount: u64, + owner: XOnlyPk, }, /// 拆分:将一笔 UTXO 拆成多笔 Split { - asset_id: [u8; 32], // 资产标识 - orig_amount: u64, // 原始总量 - outputs: Vec<(u64, OutputPk)>, // 拆分后数量 + 接收方公钥 - owner: XOnlyPk, // 授权者公钥 + asset_id: [u8; 32], + orig_amount: u64, + outputs: Vec<(u64, OutputPk)>, + owner: XOnlyPk, }, /// 合并:将多笔 UTXO 合并为一笔 Merge { - asset_id: [u8; 32], // 资产标识 - inputs: Vec, // 待合并的数量列表 - recipient: OutputPk, // 合并后接收方公钥 - owner: XOnlyPk, // 授权者公钥 + asset_id: [u8; 32], + inputs: Vec, + recipient: OutputPk, + owner: XOnlyPk, }, /// 冻结资产 Freeze { - asset_id: [u8; 32], // 资产标识 - authority: XOnlyPk, // 冻结权限公钥 - // 可扩展:freeze_until: Option, + asset_id: [u8; 32], + authority: XOnlyPk, }, /// 解冻资产 Unfreeze { - asset_id: [u8; 32], // 资产标识 - authority: XOnlyPk, // 解冻权限公钥 + asset_id: [u8; 32], + authority: XOnlyPk, }, /// 授予角色 GrantRole { - asset_id: [u8; 32], // 资产标识或 UTXO 唯一 ID - role: Vec, // 角色标识 - target: XOnlyPk, // 被授予方公钥 - admin: XOnlyPk, // 管理员公钥(签名者) + asset_id: [u8; 32], + role: Vec, + target: XOnlyPk, + admin: XOnlyPk, }, /// 撤销角色 RevokeRole { @@ -160,15 +124,15 @@ pub enum FraAction { }, /// 升级脚本版本 Upgrade { - asset_id: [u8; 32], // 资产标识或 UTXO ID - new_version: [u8; 32], // 新脚本版本哈希 - authority: XOnlyPk, // 升级权限公钥 + asset_id: [u8; 32], + new_version: [u8; 32], + authority: XOnlyPk, }, /// 更新元数据 MetadataUpdate { - asset_id: [u8; 32], // 资产标识或 UTXO ID - metadata: Vec, // 新元数据二进制/JSON - authority: XOnlyPk, // 授权者公钥 + asset_id: [u8; 32], + metadata: Vec, + authority: XOnlyPk, }, } @@ -176,53 +140,66 @@ pub enum FraAction { pub fn build_fra_script(action: FraAction) -> TapScript { let mut buf = Vec::new(); match action { - // —— 转账动作 —— - FraAction::Transfer { asset_id: _, amount: _, receiver, sender } => { - // 初始堆栈: [sig_receiver], [sig_sender] + // --- 转账动作 (Transfer) --- + // + // 脚本逻辑: + // OP_SWAP OP_CHECKSIGVERIFY OP_SWAP OP_CHECKSIG + // + // 预期 Witness (从栈顶 -> 栈底): + // - sender_sig (发送方签名) + // - receiver_sig (接收方签名) + // + // 执行流程: + // 1. 初始栈: [sender_sig, receiver_sig] + // 2. 推入 receiver_pk -> 栈: [sender_sig, receiver_sig, receiver_pk] + // 3. OP_SWAP -> 栈: [sender_sig, receiver_pk, receiver_sig] + // 4. OP_CHECKSIGVERIFY 消耗 receiver_pk 和 receiver_sig, 验证通过。栈: [sender_sig] + // 5. 推入 sender_pk -> 栈: [sender_sig, sender_pk] + // 6. OP_SWAP -> 栈: [sender_pk, sender_sig] + // 7. OP_CHECKSIG 消耗 sender_pk 和 sender_sig, 验证通过, 最终在栈上留下 TRUE。 + FraAction::Transfer { receiver, sender, .. } => { + // 注意:当前实现仅包含签名逻辑。 + // 未来可在这里添加对 asset_id 和 amount 的承诺校验 (例如使用 OP_EQUALVERIFY)。 // --- 验证接收方签名 --- - push_bytes(&mut buf, &receiver.to_byte_array()); // 压入接收方公钥 - buf.push(TapCode::Swap as u8); // 交换 -> [sig_receiver], [receiver_pk] - buf.push(TapCode::CheckSigVerify as u8); // 验证并消耗 + push_bytes(&mut buf, &receiver.to_byte_array()); + buf.push(TapCode::Swap as u8); + buf.push(TapCode::CheckSigVerify as u8); // --- 验证发送方签名 --- - push_bytes(&mut buf, &sender.to_byte_array()); // 压入发送方公钥 - buf.push(TapCode::Swap as u8); // 交换 -> [sig_sender], [sender_pk] - buf.push(TapCode::CheckSig as u8); // 验证 - + push_bytes(&mut buf, &sender.to_byte_array()); + buf.push(TapCode::Swap as u8); + buf.push(TapCode::CheckSig as u8); } - // —— 增发动作 —— + + // --- 增发、销毁等其他操作的脚本实现 --- + // 以下逻辑基于 "数据承诺 + 单签名" 模式,即先将操作数据压栈, + // 然后提供授权者公钥,用 OP_SWAP 交换栈顶的签名和公钥,最后用 OP_CHECKSIGVERIFY 验证。 + // 这是一种健壮且常见的模式。 + FraAction::Mint { asset_id, amount, receiver, minter } => { - // 1) AssetID push_bytes(&mut buf, &asset_id); - // 2) 增发数量 push_int(&mut buf, amount); - // 3) 接收方公钥 push_bytes(&mut buf, &receiver.to_byte_array()); - // 4) 增发者公钥 push_bytes(&mut buf, &minter.to_byte_array()); - buf.push(TapCode::Swap as u8); // <--- [关键修正] 修正堆栈顺序 - // 5) 验签 + buf.push(TapCode::Swap as u8); buf.push(TapCode::CheckSigVerify as u8); } - // 销毁:授权者签名后销毁 FraAction::Burn { asset_id, amount, owner } => { push_bytes(&mut buf, &asset_id); push_int(&mut buf, amount); push_bytes(&mut buf, &owner.to_byte_array()); - buf.push(TapCode::Swap as u8); // + buf.push(TapCode::Swap as u8); buf.push(TapCode::CheckSigVerify as u8); } - // 回退:持有者发起, 回退给发行者 FraAction::Rollback { asset_id, amount, owner, minter } => { push_bytes(&mut buf, &asset_id); push_int(&mut buf, amount); + push_bytes(&mut buf, &minter.to_byte_array()); // 承诺归还目标 push_bytes(&mut buf, &owner.to_byte_array()); buf.push(TapCode::Swap as u8); buf.push(TapCode::CheckSigVerify as u8); - push_bytes(&mut buf, &minter.to_byte_array()); } - // 赎回:持有者签名并执行赎回逻辑 FraAction::Redeem { asset_id, amount, owner } => { push_bytes(&mut buf, &asset_id); push_int(&mut buf, amount); @@ -230,60 +207,48 @@ pub fn build_fra_script(action: FraAction) -> TapScript { buf.push(TapCode::Swap as u8); buf.push(TapCode::CheckSigVerify as u8); } - // 拆分:校验总量一致并签名 FraAction::Split { asset_id, orig_amount, outputs, owner } => { push_bytes(&mut buf, &asset_id); push_int(&mut buf, orig_amount); - // push new amounts for (amt, _) in &outputs { push_int(&mut buf, *amt); } - // sum all new amounts for _ in 0..outputs.len().saturating_sub(1) { buf.push(TapCode::Add as u8); } - // 验证 Orig == sum(new) buf.push(TapCode::EqualVerify as u8); - // 持有者签名验证 push_bytes(&mut buf, &owner.to_byte_array()); buf.push(TapCode::Swap as u8); buf.push(TapCode::CheckSigVerify as u8); } - // 合并:校验合并后数量并签名 FraAction::Merge { asset_id, inputs, recipient, owner } => { push_bytes(&mut buf, &asset_id); + let merged_amt: u64 = inputs.iter().sum(); for amt in &inputs { push_int(&mut buf, *amt); } for _ in 0..inputs.len().saturating_sub(1) { buf.push(TapCode::Add as u8); } - // push expected merged amount - let merged_amt: u64 = inputs.iter().sum(); push_int(&mut buf, merged_amt); buf.push(TapCode::EqualVerify as u8); - // 持有者签名验证 + push_bytes(&mut buf, &recipient.to_byte_array()); push_bytes(&mut buf, &owner.to_byte_array()); buf.push(TapCode::Swap as u8); buf.push(TapCode::CheckSigVerify as u8); - // 输出接收方公钥,用于后续输出分配 - push_bytes(&mut buf, &recipient.to_byte_array()); } - // 冻结:授权者签名验证 FraAction::Freeze { asset_id, authority } => { push_bytes(&mut buf, &asset_id); push_bytes(&mut buf, &authority.to_byte_array()); buf.push(TapCode::Swap as u8); buf.push(TapCode::CheckSigVerify as u8); } - // 解冻:授权者签名验证 FraAction::Unfreeze { asset_id, authority } => { push_bytes(&mut buf, &asset_id); push_bytes(&mut buf, &authority.to_byte_array()); buf.push(TapCode::Swap as u8); buf.push(TapCode::CheckSigVerify as u8); } - // 授予角色:校验管理员签名 FraAction::GrantRole { asset_id, role, target, admin } => { push_bytes(&mut buf, &asset_id); push_bytes(&mut buf, &role); @@ -292,7 +257,6 @@ pub fn build_fra_script(action: FraAction) -> TapScript { buf.push(TapCode::Swap as u8); buf.push(TapCode::CheckSigVerify as u8); } - // 撤销角色:校验管理员签名 FraAction::RevokeRole { asset_id, role, target, admin } => { push_bytes(&mut buf, &asset_id); push_bytes(&mut buf, &role); @@ -301,7 +265,6 @@ pub fn build_fra_script(action: FraAction) -> TapScript { buf.push(TapCode::Swap as u8); buf.push(TapCode::CheckSigVerify as u8); } - // 升级:校验授权签名 FraAction::Upgrade { asset_id, new_version, authority } => { push_bytes(&mut buf, &asset_id); push_bytes(&mut buf, &new_version); @@ -309,7 +272,6 @@ pub fn build_fra_script(action: FraAction) -> TapScript { buf.push(TapCode::Swap as u8); buf.push(TapCode::CheckSigVerify as u8); } - // 元数据更新:校验授权签名 FraAction::MetadataUpdate { asset_id, metadata, authority } => { push_bytes(&mut buf, &asset_id); push_bytes(&mut buf, &metadata); @@ -318,44 +280,28 @@ pub fn build_fra_script(action: FraAction) -> TapScript { buf.push(TapCode::CheckSigVerify as u8); } } - println!("Generated raw script bytes: {:?}", buf); // 添加这一行 TapScript::from_checked(buf) } /// 将 TapScript 包装成叶子信息 -/// 把一个 FRA 动作和它在 Merkle 树里的深度,打包成 LeafInfo pub fn fra_leaf_info(action: FraAction, depth: u7) -> LeafInfo { - // 1) 用 build_fra_script 根据动作构造对应的 TapScript let ts = build_fra_script(action); - // 2) 把 TapScript 包装成 LeafScript 并带上深度 LeafInfo::tap_script(depth, ts) } - -/// 将一系列 FRA 动作打包成 Taproot 解锁证明 (ControlBlock + LeafScript) -/// -/// 输入: -/// - `internal_pk`: Taproot 内部公钥,用于 tweaked 输出 key -/// - `actions`: Vec<(FraAction, depth)>,每个元素包含一个 FRA 动作和 Merkle 树深度 -/// -/// 输出: -/// Vec<(ControlBlock, LeafScript)>,对应每个动作的 ControlBlock 及其脚本叶子,可直接放入交易 `witness`。 +/// 将一系列 FRA 动作打包成 Taproot 解锁证明 pub fn build_fra_control_blocks( internal_pk: InternalPk, actions: Vec<(FraAction, u7)>, ) -> Vec<(ControlBlock, LeafScript)> { - // 1. 把每个 (action, depth) 包装成 LeafInfo let leaf_infos = actions .into_iter() .map(|(action, depth)| fra_leaf_info(action, depth)) .collect::>(); - // 2. 用所有叶子构造一棵 Merkle 树 let tap_tree = TapTree::from_leaves(leaf_infos) .expect("FRA script tree build failed"); - // 3. 用 ControlBlockFactory 结合 internal_pk 生成每个叶子的 ControlBlock ControlBlockFactory::with(internal_pk, tap_tree) .collect() -} - +} \ No newline at end of file From 9af0107f3b59b03b9cb66d2cf0604c91a1a7d087 Mon Sep 17 00:00:00 2001 From: Thomzin Date: Wed, 13 Aug 2025 14:13:14 +0800 Subject: [PATCH 6/7] fra finished --- derive/examples/fra_demo.rs | 303 +++++++------ derive/examples/fra_demo1.rs | 142 ------- derive/examples/fra_demo2.rs | 241 ----------- derive/examples/fra_demo3.rs | 190 --------- .../{fra_demo4.rs => fra_demo_bpcore.rs} | 72 ++-- .../{fra_demo5.rs => fra_demo_rustbitcoin.rs} | 39 +- derive/examples/simple_test.rs | 4 +- derive/src/fra.rs | 137 +++--- derive/tests/fra_actions.rs | 401 ++++++++++++++++++ 9 files changed, 684 insertions(+), 845 deletions(-) delete mode 100644 derive/examples/fra_demo1.rs delete mode 100644 derive/examples/fra_demo2.rs delete mode 100644 derive/examples/fra_demo3.rs rename derive/examples/{fra_demo4.rs => fra_demo_bpcore.rs} (76%) rename derive/examples/{fra_demo5.rs => fra_demo_rustbitcoin.rs} (85%) create mode 100644 derive/tests/fra_actions.rs diff --git a/derive/examples/fra_demo.rs b/derive/examples/fra_demo.rs index d59654a..e8c6795 100644 --- a/derive/examples/fra_demo.rs +++ b/derive/examples/fra_demo.rs @@ -1,161 +1,190 @@ -use std::{thread::sleep, time::Duration, str::FromStr}; +// fra_demo5_final_solution_v3.rs -use bitcoincore_rpc::{Auth, Client, RpcApi}; -use bitcoincore_rpc::bitcoin::{ - Address as RpcAddress, Amount, Network, OutPoint, Transaction, TxIn, TxOut, - Sequence, Witness, absolute::LockTime, transaction::Version,PrivateKey, +use std::str::FromStr; + +use bitcoin::{ + self, + consensus::encode, hashes::Hash, + secp256k1::SecretKey, + key::{Keypair, Secp256k1}, + absolute::LockTime, + network::Network, + sighash::{self, Prevouts, SighashCache, TapSighash}, + taproot::{self, LeafVersion, TaprootBuilder}, + Address, Amount, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Witness, }; -use bitcoincore_rpc::json::AddressType; - -// --- 使用 bp-std 生态系统内的类型 --- -use derive::{self, fra::{FraAction, build_fra_script, build_fra_control_blocks}, base58::encode, KeyOrigin, XOnlyPk, Xpriv, XpubDerivable}; -use bc::{self, ConsensusEncode, ScriptPubkey, SighashCache, SighashFlag, SighashType, TapSighash, TapMerklePath, TapNodeHash , TapBranchHash}; -use invoice::{Address, AddressNetwork, AddressPayload}; -use secp256k1::{Secp256k1, Message, Keypair, SecretKey, PublicKey}; -use amplify::{Wrapper, ByteArray, hex}; // [修复] 导入 Wrapper, ByteArray 和 hex - - -// `bitcoincore-rpc` 生态系统使用的版本(通过其依赖 `bitcoin`) -use bitcoincore_rpc::bitcoin::secp256k1 as bitcoin_secp; - +use bitcoincore_rpc::{Auth, Client, RpcApi}; +use bitcoincore_rpc::json::AddressType; -// ... (所有帮助函数保持不变) ... -fn to_bc_outpoint(rpc_outpoint: OutPoint) -> bc::Outpoint { - bc::Outpoint::new( - bc::Txid::from_byte_array(Hash::hash(&rpc_outpoint.txid[..]).to_byte_array()), - bc::Vout::from_u32(rpc_outpoint.vout) - ) -} - -fn to_bc_txout(rpc_txout: bitcoincore_rpc::bitcoin::TxOut) -> bc::TxOut { - bc::TxOut { - value: bc::Sats::from(rpc_txout.value.to_sat()), - script_pubkey: ScriptPubkey::from_inner( - bc::ScriptBytes::try_from(rpc_txout.script_pubkey.to_bytes()).unwrap() - ), - } -} - -fn calculate_merkle_root(path: &TapMerklePath, leaf_hash: bc::TapLeafHash) -> TapNodeHash { - let mut current_hash: TapNodeHash = leaf_hash.into(); - for sibling_hash in path.iter() { - current_hash = bc::TapBranchHash::with_nodes(current_hash, (*sibling_hash).into()).into(); - } - current_hash -} - +use derive::{ + fra::{build_fra_script, FraAction}, + XOnlyPk, +}; fn main() -> Result<(), Box> { + // =================================================================== + // 步骤 0-2: RPC 设置和 UTXO 准备 + // =================================================================== let rpc = Client::new( "http://127.0.0.1:18443/wallet/legacy_true", Auth::UserPass("foo".into(), "bar".into()), )?; - rpc.import_private_key(&PrivateKey::from_wif("cVkVW14o6zBGyhaV2xqMsGEqYijB6jzsK5EkNcmzBPJejGmcBrMQ")?, None, None)?; - rpc.import_private_key(&PrivateKey::from_wif("cPMvbJRTmycMYU3pQ3dTfCwzVEtyVqpEeV3LWNaT1pzHhax2FKZF")?, None, None)?; + rpc.import_private_key( + &bitcoin::PrivateKey::from_wif("cVkVW14o6zBGyhaV2xqMsGEqYijB6jzsK5EkNcmzBPJejGmcBrMQ")?, + None, + None, + )?; + rpc.import_private_key( + &bitcoin::PrivateKey::from_wif("cPMvbJRTmycMYU3pQ3dTfCwzVEtyVqpEeV3LWNaT1pzHhax2FKZF")?, + None, + None, + )?; - let coinbase_addr = rpc.get_new_address(None, Some(AddressType::Legacy))?.assume_checked(); + let coinbase_addr_unchecked = rpc.get_new_address(None, Some(AddressType::Legacy))?; + let coinbase_addr = coinbase_addr_unchecked.require_network(Network::Regtest)?; rpc.generate_to_address(101, &coinbase_addr)?; - sleep(Duration::from_secs(3)); - + let balance = rpc.get_balance(None, None)?; + println!("Wallet balance: {} BTC", balance); + let fund_utxo = rpc + .list_unspent(None, None, None, None, None)? + .into_iter() + .find(|u| u.amount.to_sat() >= 100_000) + .expect("没有足够的 UTXO (>= 0.001 BTC)"); + + // =================================================================== + // 步骤 3-4: 密钥生成 + // =================================================================== let secp = Secp256k1::new(); + let internal_kp = Keypair::new(&secp, &mut rand::thread_rng()); + let internal_pk = internal_kp.x_only_public_key().0; + + let sender_sk = SecretKey::from_slice( + &bitcoin::PrivateKey::from_wif("cVkVW14o6zBGyhaV2xqMsGEqYijB6jzsK5EkNcmzBPJejGmcBrMQ")? + .inner + .secret_bytes(), + )?; + let sender_kp = Keypair::from_secret_key(&secp, &sender_sk); + let sender_pk = sender_kp.x_only_public_key().0; + + let recv_sk = SecretKey::from_slice( + &bitcoin::PrivateKey::from_wif("cPMvbJRTmycMYU3pQ3dTfCwzVEtyVqpEeV3LWNaT1pzHhax2FKZF")? + .inner + .secret_bytes(), + )?; + let recv_kp = Keypair::from_secret_key(&secp, &recv_sk); + let recv_pk = recv_kp.x_only_public_key().0; - // 1. 生成内部密钥 - let internal_keypair = Keypair::new(&secp, &mut rand::thread_rng()); - // [最终修复] 显式地从 PublicKey 获取 XOnlyPublicKey,与 simple_test.rs 保持一致 - let internal_public_key = PublicKey::from_keypair(&internal_keypair); - let (internal_pk_xonly, _) = internal_public_key.x_only_public_key(); - let internal_pk = bc::InternalPk::from(XOnlyPk::from(internal_pk_xonly)); - - // 2. 解析发送方/接收方私钥 - let sender_secret_rpc = PrivateKey::from_wif("cVkVW14o6zBGyhaV2xqMsGEqYijB6jzsK5EkNcmzBPJejGmcBrMQ")?.inner; - let sender_secret = SecretKey::from_slice(&sender_secret_rpc.secret_bytes())?; - let sender_keypair = Keypair::from_secret_key(&secp, &sender_secret); - let sender_public_key = PublicKey::from_keypair(&sender_keypair); - let (sender_pk_xonly, _) = sender_public_key.x_only_public_key(); - - let recv_secret_rpc = PrivateKey::from_wif("cPMvbJRTmycMYU3pQ3dTfCwzVEtyVqpEeV3LWNaT1pzHhax2FKZF")?.inner; - let recv_secret = SecretKey::from_slice(&recv_secret_rpc.secret_bytes())?; - let recv_keypair = Keypair::from_secret_key(&secp, &recv_secret); - let receiver_public_key = PublicKey::from_keypair(&recv_keypair); - let (receiver_pk_xonly, _) = receiver_public_key.x_only_public_key(); - - // 3. 构建脚本和叶子 + // =================================================================== + // 步骤 5-8: 地址生成 + // =================================================================== let action = FraAction::Transfer { asset_id: [0u8; 32], amount: 1000, - receiver: XOnlyPk::from(receiver_pk_xonly), - sender: XOnlyPk::from(sender_pk_xonly), + receiver: XOnlyPk::from_byte_array(recv_pk.serialize()).unwrap(), + sender: XOnlyPk::from_byte_array(sender_pk.serialize()).unwrap(), }; - let tap_script = derive::fra::build_fra_script(action); - let leaf_script = bc::LeafScript::from_tap_script(tap_script.clone()); - let leaf_hash = leaf_script.tap_leaf_hash(); - - // 4. 手动构建 Taproot 地址 (对于单一脚本,默克尔根就是叶子哈希) - let (output_pk, output_pk_parity) = internal_pk.to_output_pk(Some(leaf_hash.into())); - let fra_addr = Address::new(AddressPayload::Tr(output_pk), AddressNetwork::Regtest); - let fra_spk = fra_addr.script_pubkey(); - - // 5. 注资交易... - let fund_utxo = rpc.list_unspent(None, None, None, None, None)?.into_iter().find(|u| u.amount.to_sat() >= 100_000).unwrap(); - let rpc_addr = RpcAddress::from_str(&fra_addr.to_string())?.assume_checked(); - let fid = rpc.send_to_address(&rpc_addr, Amount::from_sat(fund_utxo.amount.to_sat() - 10000), None, None, None, None, None, None)?; - rpc.generate_to_address(1, &coinbase_addr)?; - - // 6. 准备花费交易... - let funding_tx = rpc.get_raw_transaction(&fid, None)?; - let (fra_vout, fra_txout) = funding_tx.output.iter().enumerate().find(|(_, o)| o.script_pubkey.to_bytes() == fra_spk.as_slice()).unwrap(); - let fra_outpoint = OutPoint { txid: fid, vout: fra_vout as u32 }; - - let dest_addr = rpc.get_new_address(None, Some(AddressType::Legacy))?.assume_checked(); - let mut spend_tx = bc::Tx { - version: bc::TxVer::V2, - lock_time: bc::LockTime::ZERO, - inputs: bc::VarIntArray::from_iter_checked([bc::TxIn { - prev_output: to_bc_outpoint(fra_outpoint), - sig_script: bc::SigScript::new(), - sequence: bc::SeqNo::from_consensus_u32(0xFFFF_FFFF), - witness: bc::Witness::new(), - }]), - outputs: bc::VarIntArray::from_iter_checked([bc::TxOut { - value: bc::Sats::from(fra_txout.value.to_sat() - 10000), - script_pubkey: ScriptPubkey::from_inner(bc::ScriptBytes::try_from(dest_addr.script_pubkey().to_bytes()).unwrap()), - }]), + + let leaf_script_bytes = build_fra_script(action).as_inner().to_vec(); + let script = ScriptBuf::from(leaf_script_bytes); + println!("Leaf Script ({} bytes): {}", script.len(), script.to_hex_string()); + + let builder = TaprootBuilder::new().add_leaf(0, script.clone()).unwrap(); + let spend_info = builder.finalize(&secp, internal_pk).unwrap(); + let fra_addr = Address::p2tr(&secp, internal_pk, spend_info.merkle_root(), Network::Regtest); + println!("FRA Taproot 地址: {}", fra_addr); + + // =================================================================== + // 步骤 9-10: 交易注资 + // =================================================================== + + let rpc_address = Address::from_str(&fra_addr.to_string())?.assume_checked(); + let funding_txid = rpc.send_to_address( + &rpc_address, + Amount::from_sat(fund_utxo.amount.to_sat() - 10_000), + None, None, None, None, None, None, + )?; + rpc.generate_to_address(1, &coinbase_addr)?; // 现在类型匹配 + println!("Funding TXID: {}", funding_txid); + let funding_tx_raw = rpc.get_raw_transaction(&funding_txid, None)?; + let (vout, prevout_value) = funding_tx_raw + .output + .iter() + .enumerate() + .find(|(_, o)| o.script_pubkey == fra_addr.script_pubkey()) + .map(|(i, o)| (i as u32, o.value)) + .expect("FRA UTXO not found in funding tx"); + let fra_outpoint = OutPoint { + txid: funding_txid, + vout, }; - // 7. 计算 Sighash - let prevout_bc = to_bc_txout(fra_txout.clone()); - let mut cache = SighashCache::new(&mut spend_tx, vec![prevout_bc])?; - let sighash = cache.tap_sighash_script(0, leaf_hash, None)?; - let msg = Message::from(sighash); - - // 8. 签名 - let sig_sender = secp.sign_schnorr(msg.as_ref(), &sender_keypair); - let sig_receiver = secp.sign_schnorr(msg.as_ref(), &recv_keypair); - - // 9. 手动构建 Control Block (对于单一脚本,路径为空) - let merkle_path = TapMerklePath::try_from(Vec::new())?; - let control_block = bc::ControlBlock::with( - leaf_script.version, - internal_pk, - output_pk_parity, - merkle_path, - ); - let control_block_bytes = control_block.consensus_serialize(); - - // 10. 组装 Witness - spend_tx.inputs[0].witness = bc::Witness::from_consensus_stack(vec![ - sig_receiver.as_ref().to_vec(), - sig_sender.as_ref().to_vec(), - tap_script.to_vec(), - control_block_bytes, - ]); - - // 11. 广播 - let raw_spend_tx = spend_tx.consensus_serialize(); - let sid = rpc.send_raw_transaction(&raw_spend_tx)?; - println!("\n🎉 成功! 花费交易已广播: {}", sid); + // =================================================================== + // 步骤 11: 构建花费交易 + // =================================================================== + let dest_addr_unchecked = rpc.get_new_address(None, Some(AddressType::Legacy))?; + let dest_addr = dest_addr_unchecked.require_network(Network::Regtest)?; + let mut spend_tx = Transaction { + version: bitcoin::transaction::Version(2), + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: fra_outpoint, + script_sig: ScriptBuf::new(), + sequence: Sequence::MAX, + witness: Witness::new(), + }], + output: vec![TxOut { + value: prevout_value - Amount::from_sat(10_000), + script_pubkey: dest_addr.script_pubkey(), + }], + }; + let prevouts = vec![TxOut { + value: prevout_value, + script_pubkey: fra_addr.script_pubkey(), + }]; + + // =================================================================== + // 步骤 12: 计算 Sighash (使用修正后的 API) + // =================================================================== + let mut sighasher = SighashCache::new(&spend_tx); + let leaf_hash = taproot::TapLeafHash::from_script(&script, LeafVersion::TapScript); + let sighash: TapSighash = sighasher + .taproot_script_spend_signature_hash( + 0, + &Prevouts::All(&prevouts), + leaf_hash, + sighash::TapSighashType::Default, + )?; + + let msg = bitcoin::secp256k1::Message::from(sighash); + println!("Sighash (rust-bitcoin): {}", sighash.to_string()); // .to_hex_string() -> .to_string() + + // =================================================================== + // 步骤 13: 签名并构建 Witness + // =================================================================== + let sig_sender = secp.sign_schnorr(&msg, &sender_kp); + let sig_receiver = secp.sign_schnorr(&msg, &recv_kp); + + let control_block = spend_info + .control_block(&(script.clone(), LeafVersion::TapScript)) + .unwrap(); + + let mut witness = Witness::new(); + witness.push(sig_sender.as_ref()); + witness.push(sig_receiver.as_ref()); + witness.push(script); + witness.push(control_block.serialize()); + spend_tx.input[0].witness = witness; + + // =================================================================== + // 步骤 14: 广播交易 + // =================================================================== + let tx_hex = encode::serialize_hex(&spend_tx); + println!("Final TX Hex: {}", tx_hex); + + let final_txid = rpc.send_raw_transaction(&*tx_hex)?; // Pass String by value + println!("\n🎉🎉🎉 交易成功广播! TXID = {} 🎉🎉🎉", final_txid); Ok(()) } \ No newline at end of file diff --git a/derive/examples/fra_demo1.rs b/derive/examples/fra_demo1.rs deleted file mode 100644 index 3e2794f..0000000 --- a/derive/examples/fra_demo1.rs +++ /dev/null @@ -1,142 +0,0 @@ -// derive/examples/fra_demo.rs - -use std::str::FromStr; -use strict_encoding::StreamWriter; -use derive::secp256k1::{Secp256k1, Keypair}; -use rand::thread_rng; -use bitcoincore_rpc::{Auth, Client, RpcApi}; - -// 全部从 bitcoincore_rpc::bitcoin 引入,不要再用单独的 bitcoin crate -use bitcoincore_rpc::bitcoin::{ - Transaction, TxIn, TxOut, OutPoint, Address, - ScriptBuf, Sequence, Witness, - absolute::LockTime, Amount, - taproot::{TapLeafHash, LeafVersion}, - sighash::{SighashCache, Prevouts, TapSighashType}, - consensus::encode::serialize, - Network, transaction::Version, Script, -}; -// 用 StrictWriter 来做 TypedWrite -use strict_encoding::StrictEncode; - -// 从你的 derive crate 拿到 FRA 相关类型和工厂 -use derive::fra::{FraAction, build_fra_control_blocks}; -use bc::{InternalPk, OutputPk, XOnlyPk}; - -use amplify::num::u7; - -fn main() { - // 1) RPC setup - let rpc = Client::new( - "http://127.0.0.1:18443", - Auth::UserPass("foo".into(), "bar".into()), - ).unwrap(); - - // 2) 拿一个可花 UTXO - let utxo = rpc - .list_unspent(None, None, None, None, None) - .unwrap() - .into_iter() - .next() - .expect("请先在 regtest 挖矿并生产 UTXO"); - let outpoint = OutPoint { txid: utxo.txid, vout: utxo.vout }; - - // 原 UTXO 的脚本和金额 - let prev_amount = utxo.amount; - let prev_script = utxo.script_pub_key.clone(); - - // 构造 TxIn - let txin = TxIn { - previous_output: outpoint, - script_sig: ScriptBuf::new(), - sequence: Sequence(0xFFFF_FFFF), - witness: Witness::new(), - }; - - // 3) 构造普通的发送输出:从 RPC 拿一个新地址,并校验网络 - let recipient = rpc.get_new_address(None, None).unwrap() - .require_network(Network::Regtest).unwrap(); - let fee = 1_000u64; - let send_sat = utxo.amount.to_sat() - fee; - let txout = TxOut { - value: Amount::from_sat(send_sat), - script_pubkey: recipient.script_pubkey(), - }; - - - let mut tx = Transaction { - version: Version(2), - lock_time: LockTime::ZERO, - input: vec![txin], - output: vec![txout], - }; - - // 4) 用随机密钥生成 InternalPk / OutputPk - let secp = Secp256k1::new(); - let mut rng = thread_rng(); - - // Internal key for Taproot tweak - let internal_kp = Keypair::new(&secp, &mut rng); - let (ix, _) = internal_kp.x_only_public_key(); - // From for XOnlyPk, then From for InternalPk: - let internal_pk = InternalPk::from(XOnlyPk::from(ix)); - - // Sender - let sender_kp = Keypair::new(&secp, &mut rng); - let (sx, _) = sender_kp.x_only_public_key(); - let sender_pk = OutputPk::from(XOnlyPk::from(sx)); - - // Receiver - let recv_kp = Keypair::new(&secp, &mut rng); - let (rx, _) = recv_kp.x_only_public_key(); - let receiver_pk = OutputPk::from(XOnlyPk::from(rx)); - - // 5) 构造 FRA Merkle 证明 - let depth = u7::try_from(0).unwrap(); - let action = FraAction::Transfer { - asset_id: [0u8; 32], - amount: 1_000, - receiver: receiver_pk.clone(), - sender: sender_pk.clone(), - }; - let proofs = build_fra_control_blocks(internal_pk, vec![(action, depth)]); - let (control_block, leaf_script) = &proofs[0]; - - // 6) 计算 sighash - let mut cache = SighashCache::new(&tx); - let tapleaf = TapLeafHash::from_script( - &Script::from_bytes(leaf_script.script.as_slice()), - LeafVersion::TapScript, - ); - let sighash = cache.taproot_script_spend_signature_hash( - 0, - &Prevouts::All(&[TxOut { value: prev_amount, script_pubkey: prev_script }]), - tapleaf, - TapSighashType::Default, - ).unwrap(); - - // 7) Schnorr 签名:直接用 TapSighash 的字节切片 - let hash_bytes: &[u8; 32] = sighash.as_ref(); - let sig_recv = secp.sign_schnorr(hash_bytes, &recv_kp); - let sig_send = secp.sign_schnorr(hash_bytes, &sender_kp); - - // 8) 填充 witness 并广播 - let mut wit = Vec::new(); - wit.push(sig_recv.as_ref().to_vec()); - wit.push(sig_send.as_ref().to_vec()); - // —— 把 TapScript 脚本本身压进去 —— - // 先将 Confined, …> 强制成 &[u8],再 to_vec() - let script_slice: &[u8] = leaf_script.script.as_ref(); - wit.push(script_slice.to_vec()); - // ControlBlock 序列化 -> Vec - let mut cb_ser = Vec::new(); - let writer: StreamWriter<&mut Vec> = StreamWriter::new::<1024>(&mut cb_ser); - control_block.strict_write(writer).unwrap(); - wit.push(cb_ser); - - tx.input[0].witness = Witness::from_slice(&wit); - - let raw_tx = serialize(&tx); - let txid = rpc.send_raw_transaction(&raw_tx[..]).unwrap(); - println!("Broadcast txid = {}", txid); -} \ No newline at end of file diff --git a/derive/examples/fra_demo2.rs b/derive/examples/fra_demo2.rs deleted file mode 100644 index 4298aa5..0000000 --- a/derive/examples/fra_demo2.rs +++ /dev/null @@ -1,241 +0,0 @@ -// derive/examples/fra_demo.rs - -use std::{thread::sleep, time::Duration}; -use bitcoincore_rpc::{Auth, Client, RpcApi}; -use bitcoincore_rpc::bitcoin::{ - Address, Amount, Network, - OutPoint, Transaction, TxIn, TxOut, - ScriptBuf, Sequence, Witness, - absolute::LockTime, transaction::Version, - consensus::encode::serialize, - taproot::{TaprootBuilder, TaprootSpendInfo, LeafVersion, TapLeafHash}, - sighash::{SighashCache, Prevouts, TapSighashType}, - PrivateKey, - secp256k1::{ - Secp256k1 as BitcoinSecp, - SecretKey as BitcoinSecretKey, - Keypair as BitcoinKeypair, - XOnlyPublicKey as BitcoinXOnlyPublicKey, - Message as BitcoinMessage, - }, -}; -use bitcoincore_rpc::json::AddressType; - -use derive::fra::{FraAction, build_fra_control_blocks}; -use derive::base58::encode; -use bc::{InternalPk, OutputPk, XOnlyPk}; -use secp256k1::{ - Secp256k1 as DeriveSecp, - SecretKey as DeriveSecretKey, - Keypair as DeriveKeypair, -}; - -use rand::thread_rng; -use amplify::num::u7; - -fn main() -> Result<(), Box> { - // 0) 连接 regtest RPC,并加载“legacy_true”钱包 - let rpc = Client::new( - "http://127.0.0.1:18443/wallet/legacy_true", - Auth::UserPass("foo".into(), "bar".into()), - )?; - - // 确保私钥已导入 - rpc.import_private_key(&PrivateKey::from_wif("cVkVW14o6zBGyhaV2xqMsGEqYijB6jzsK5EkNcmzBPJejGmcBrMQ")?, None, None)?; - rpc.import_private_key(&PrivateKey::from_wif("cPMvbJRTmycMYU3pQ3dTfCwzVEtyVqpEeV3LWNaT1pzHhax2FKZF")?, None, None)?; - - // (Optional) 挖 101 块,生成成熟 UTXO - println!(">> Generating 101 blocks for coinbase maturity..."); - let coinbase_addr = rpc.get_new_address(None, Some(AddressType::Legacy))?.require_network(Network::Regtest)?; - rpc.generate_to_address(210, &coinbase_addr)?; - sleep(Duration::from_secs(3)); - println!(" Done. Funds are now available."); - - // 检查钱包余额 - let balance = rpc.get_balance(None, None)?; - println!("Wallet balance: {} BTC", balance.to_btc()); - - // --------------------------------------------------- - // STEP1: FUNDING — 铸造一个 FRA Taproot UTXO - // --------------------------------------------------- - let fund_utxo = rpc.list_unspent(None, None, None, None, None)? - .into_iter() - .find(|utxo| utxo.amount.to_sat() >= 100_000) // 确保 UTXO 金额足够 - .expect("没有找到金额足够的 UTXO(需 >= 0.001 BTC)"); - let _fund_outpoint = OutPoint { txid: fund_utxo.txid, vout: fund_utxo.vout }; - println!("Fund UTXO amount: {} BTC", fund_utxo.amount.to_btc()); - - // 1) DERIVE:生成 Internal KeyPair(FRA 控制块用) - let derive_secp = DeriveSecp::new(); - let mut rng = thread_rng(); - let derive_internal_kp = DeriveKeypair::new(&derive_secp, &mut rng); - let (derive_ix, _) = derive_internal_kp.x_only_public_key(); - let internal_pk = InternalPk::from(derive_ix); - - // 2) 解析发送者/接收者 WIF 私钥(bitcoin crate) - let sender_secret: BitcoinSecretKey = PrivateKey::from_wif( - "cVkVW14o6zBGyhaV2xqMsGEqYijB6jzsK5EkNcmzBPJejGmcBrMQ" - )?.inner; - let recv_secret: BitcoinSecretKey = PrivateKey::from_wif( - "cPMvbJRTmycMYU3pQ3dTfCwzVEtyVqpEeV3LWNaT1pzHhax2FKZF" - )?.inner; - - // 3) bitcoin-secret → derive-secret → derive KeyPair → OutputPk - let derive_sender_sk = DeriveSecretKey::from_slice(&sender_secret.secret_bytes())?; - let derive_recv_sk = DeriveSecretKey::from_slice(&recv_secret.secret_bytes())?; - let derive_sender_kp = DeriveKeypair::from_secret_key(&derive_secp, &derive_sender_sk); - let derive_recv_kp = DeriveKeypair::from_secret_key(&derive_secp, &derive_recv_sk); - let (sx, _) = derive_sender_kp.x_only_public_key(); - let (rx, _) = derive_recv_kp.x_only_public_key(); - let sender_xonly_pk = XOnlyPk::from(sx); - let receiver_xonly_pk = XOnlyPk::from(rx); - - println!("Sender PK bytes length: {}", sender_xonly_pk.to_byte_array().len()); - println!("Receiver PK bytes length: {}", receiver_xonly_pk.to_byte_array().len()); - // 4) 构造 FRA Transfer Leaf - let action = FraAction::Transfer { - asset_id: [0u8; 32], - amount: 1000, - receiver: receiver_xonly_pk, - sender: sender_xonly_pk, - }; - let depth = u7::try_from(0).unwrap(); - let proofs = build_fra_control_blocks(internal_pk.clone(), vec![(action, depth)]); - let (control_block, leaf_script) = &proofs[0]; - - // --------------------------------------------------- - // --------------------------------------------------- - // STEP1.5: 构建 Taproot scriptPubKey - // --------------------------------------------------- - let bitcoin_secp = BitcoinSecp::new(); - let bitcoin_ix = BitcoinXOnlyPublicKey::from_slice(&derive_ix.serialize())?; - - // 取出 leaf 脚本字节 - let sb = ScriptBuf::from(AsRef::<[u8]>::as_ref(&leaf_script.script).to_vec()); // 不再手动添加 OP_DROP - - - let tap_info = TaprootBuilder::new() - // 若出错,直接 panic 并打印 e 的 Debug 信息 - .add_leaf(depth.into(), sb.clone()) - .expect("TaprootBuilder::add_leaf 失败") - .finalize(&bitcoin_secp, bitcoin_ix) - .expect("TaprootBuilder::finalize 失败"); - let fra_addr = Address::p2tr( - &bitcoin_secp, - bitcoin_ix, - tap_info.merkle_root(), - Network::Regtest, - ); - let fra_spk = fra_addr.script_pubkey(); - - // 5) 广播 Funding TX via 钱包 RPC,自动签名 - let send_val = fund_utxo.amount.to_sat().saturating_sub(10_000); // 降低费用预留到 0.0001 BTC - println!("Sending value: {} satoshi", send_val); - if send_val < 546 { - return Err("Transaction amount too small: must be at least 546 satoshi".into()); - } - let fid = rpc.send_to_address( - &fra_addr, - Amount::from_sat(send_val), - None, // label - None, // comment - None, // comment_to - None, // replaceable - None, // conf_target - None, // estimate_mode - )?; - println!(">> STEP1: Funding txid = {}", fid); - // 立即挖一块确认 - let confirm_addr = rpc.get_new_address(None, Some(AddressType::Legacy))?.require_network(Network::Regtest)?; - rpc.generate_to_address(1, &confirm_addr)?; - sleep(Duration::from_secs(3)); - - // --------------------------------------------------- - // STEP2: SPENDING — 花费 FRA UTXO - // --------------------------------------------------- - println!(">> STEP2: Finding FRA UTXO from funding tx {}", fid); - let funding_tx = rpc.get_raw_transaction(&fid, None)?; - let (fra_vout, fra_txout) = funding_tx.output.iter().enumerate() - .find(|(_vout, txout)| txout.script_pubkey == fra_spk) - .expect("在 funding transaction 中没找到与 fra_spk 匹配的输出"); - let fra_outpoint = OutPoint { txid: fid, vout: fra_vout as u32 }; - let fra_amount = fra_txout.value; - println!(" Found FRA UTXO at {}:{} with value {} BTC", fid, fra_vout, fra_amount.to_btc()); - - let left = fra_amount.to_sat().saturating_sub(10_000); // 降低费用预留 - if left < 546 { - return Err("Spend transaction amount too small: must be at least 546 satoshi".into()); - } - let spend_tx = Transaction { - version: Version(2), - lock_time: LockTime::ZERO, - input: vec![TxIn { - previous_output: fra_outpoint.clone(), - script_sig: ScriptBuf::new(), - sequence: Sequence(0xFFFF_FFFF), - witness: Witness::new(), - }], - output: vec![TxOut { - value: Amount::from_sat(left), - script_pubkey: rpc.get_new_address(None, Some(AddressType::Legacy))?.require_network(Network::Regtest)?.script_pubkey(), - }], - }; - - // 6) 计算 Taproot‑FRA 花费 sighash - let mut cache = SighashCache::new(&spend_tx); - let tapleaf_hash = TapLeafHash::from_script(&sb, LeafVersion::TapScript); - let sighash = cache.taproot_script_spend_signature_hash( - 0, - &Prevouts::All(&[fra_txout.clone()]), - tapleaf_hash, - TapSighashType::Default, - )?; - let msg = BitcoinMessage::from_digest_slice(sighash.as_ref())?; - println!("Sighash: {:?}", AsRef::<[u8]>::as_ref(&sighash)); - - // 7) 双 Schnorr 签名 - let btc_sender_kp = BitcoinKeypair::from_secret_key(&bitcoin_secp, &sender_secret); - let btc_recv_kp = BitcoinKeypair::from_secret_key(&bitcoin_secp, &recv_secret); - - let sig_sender = bitcoin_secp.sign_schnorr(&msg, &btc_sender_kp); - let sig_receiver = bitcoin_secp.sign_schnorr(&msg, &btc_recv_kp); // 确保这里是接收方的签名 - // 添加以下验证代码 - let is_sender_sig_valid = bitcoin_secp.verify_schnorr(&sig_sender, &msg, &btc_sender_kp.x_only_public_key().0); - let is_receiver_sig_valid = bitcoin_secp.verify_schnorr(&sig_receiver, &msg, &btc_recv_kp.x_only_public_key().0); - - println!("Sender signature internal verification: {}", is_sender_sig_valid.is_ok()); - println!("Receiver signature internal verification: {}", is_receiver_sig_valid.is_ok()); - - let sig_sender_bytes = sig_sender.as_ref().to_vec(); - let sig_receiver_bytes = sig_receiver.as_ref().to_vec(); - - println!("Signature receiver length: {}", sig_receiver_bytes.len()); // 应该显示 64 - println!("Signature sender length: {}", sig_sender_bytes.len()); // 应该显示 64 - - // 8) 使用 TaprootSpendInfo 生成正确的 ControlBlock - let control_block_bytes = tap_info.control_block(&(sb.clone(), LeafVersion::TapScript)) - .expect("Failed to get control block") - .serialize(); - println!("ControlBlock bytes length: {}", control_block_bytes.len()); - println!("Script bytes: {:?}", sb.to_bytes()); - println!("ControlBlock bytes: {:?}", control_block_bytes); - - // 9) 填 witness 并广播 Spend TX - let mut final_tx = spend_tx.clone(); - final_tx.input[0].witness = Witness::from_slice(&[ - sig_receiver_bytes, // <--- !!!关键修正:接收方签名必须在第一个位置 (栈顶) - sig_sender_bytes, // 发送方签名 (在接收方签名之后消耗) - sb.to_bytes(), // 完整的脚本 - control_block_bytes, // control block - ]); - println!("Final Witness elements:"); - for (i, elem) in final_tx.input[0].witness.iter().enumerate() { - println!(" Witness[{}] (len {}): {:?}", i, elem.len(), elem); - } - let raw_spend = serialize(&final_tx); - println!("Raw Spend Tx: {:?}", encode(&raw_spend)); - let sid = rpc.send_raw_transaction(&raw_spend[..])?; - println!("Spend txid = {}", sid); - - Ok(()) -} \ No newline at end of file diff --git a/derive/examples/fra_demo3.rs b/derive/examples/fra_demo3.rs deleted file mode 100644 index 199dfe8..0000000 --- a/derive/examples/fra_demo3.rs +++ /dev/null @@ -1,190 +0,0 @@ - -use std::{thread::sleep, time::Duration}; -use bitcoincore_rpc::{Auth, Client, RpcApi}; -use bitcoin::{ - Address, Amount, Network, OutPoint, Transaction, TxIn, TxOut, ScriptBuf, Sequence, Witness, - absolute::LockTime, transaction::Version, consensus::encode::serialize, - taproot::{TaprootBuilder, TaprootSpendInfo, LeafVersion, TapLeafHash}, - sighash::{SighashCache, Prevouts, TapSighashType}, - PrivateKey, secp256k1::{Secp256k1, SecretKey, Keypair, XOnlyPublicKey, Message}, -}; -use bitcoincore_rpc::json::AddressType; -use rand::thread_rng; -use derive::base58::{encode, decode}; - -#[allow(unused_imports)] -fn main() -> Result<(), Box> { - // 0) Connect to regtest RPC and load "legacy_true" wallet - let rpc = Client::new( - "http://127.0.0.1:18443/wallet/legacy_true", - Auth::UserPass("foo".into(), "bar".into()), - )?; - // Import private keys - let sender_wif = "cVkVW14o6zBGyhaV2xqMsGEqYijB6jzsK5EkNcmzBPJejGmcBrMQ"; - let receiver_wif = "cPMvbJRTmycMYU3pQ3dTfCwzVEtyVqpEeV3LWNaT1pzHhax2FKZF"; - rpc.import_private_key(&PrivateKey::from_wif(sender_wif)?, None, None)?; - rpc.import_private_key(&PrivateKey::from_wif(receiver_wif)?, None, None)?; - // Generate 101 blocks for coinbase maturity - println!(">> Generating 101 blocks for coinbase maturity..."); - let coinbase_addr = rpc.get_new_address(None, Some(AddressType::Legacy))?.require_network(Network::Regtest)?; - rpc.generate_to_address(210, &coinbase_addr)?; - sleep(Duration::from_secs(3)); - println!(" Done. Funds are now available."); - // Check wallet balance - let balance = rpc.get_balance(None, None)?; - println!("Wallet balance: {} BTC", balance.to_btc()); - - // --------------------------------------------------- - // STEP1: FUNDING — Create a FRA Taproot UTXO - // --------------------------------------------------- - let fund_utxo = rpc.list_unspent(None, None, None, None, None)? - .into_iter() - .find(|utxo| utxo.amount.to_sat() >= 100_000) - .expect("No UTXO with sufficient funds (>= 0.001 BTC)"); - println!("Fund UTXO amount: {} BTC", fund_utxo.amount.to_btc()); - - // 1) Generate Internal KeyPair - let secp = Secp256k1::new(); - let internal_kp = Keypair::new(&secp, &mut thread_rng()); - let (internal_xonly, _) = internal_kp.x_only_public_key(); - println!("Internal PK: {:?}", encode(&internal_xonly.serialize())); - - // 2) Parse sender/receiver private keys - let sender_secret = PrivateKey::from_wif(sender_wif)?.inner; - let receiver_secret = PrivateKey::from_wif(receiver_wif)?.inner; - let sender_kp = Keypair::from_secret_key(&secp, &sender_secret); - let receiver_kp = Keypair::from_secret_key(&secp, &receiver_secret); - let (sender_xonly, _) = sender_kp.x_only_public_key(); - let (receiver_xonly, _) = receiver_kp.x_only_public_key(); - let sender_pk_bytes = sender_xonly.serialize(); - let receiver_pk_bytes = receiver_xonly.serialize(); - println!("Sender PK: {:?}", encode(&sender_pk_bytes)); - println!("Receiver PK: {:?}", encode(&receiver_pk_bytes)); - // Verify public keys - let expected_sender_pk = "d8254e7443d48c701e10dc7ae8e8e429c71f4d07100d3fcc4d374f103759764e"; - let expected_receiver_pk = "a969d4a73fdc45987eb2ec968026045cd8050750956ff0ccca38f5ee1c8032cc"; - if encode(&sender_pk_bytes) != expected_sender_pk { - println!("Sender PK mismatch: expected {}, got {}", expected_sender_pk, encode(&sender_pk_bytes)); - return Err("Sender public key does not match expected value".into()); - } - if encode(&receiver_pk_bytes) != expected_receiver_pk { - println!("Receiver PK mismatch: expected {}, got {}", expected_receiver_pk, encode(&receiver_pk_bytes)); - return Err("Receiver public key does not match expected value".into()); - } - - // 3) Construct FRA script using bitcoin::ScriptBuf - let mut script_bytes = vec![]; - script_bytes.push(0x20); // OP_PUSHDATA1 - script_bytes.extend_from_slice(&sender_pk_bytes); - script_bytes.push(0x7c); // OP_SWAP - script_bytes.push(0xad); // OP_CHECKSIGVERIFY - script_bytes.push(0x20); // OP_PUSHDATA1 - script_bytes.extend_from_slice(&receiver_pk_bytes); - script_bytes.push(0x7c); // OP_SWAP - script_bytes.push(0xac); // OP_CHECKSIG - let tap_script = ScriptBuf::from(script_bytes); - println!("Generated Script Bytes: {:?}", encode(&tap_script.to_bytes())); - // Verify script - let expected_script = "20d8254e7443d48c701e10dc7ae8e8e429c71f4d07100d3fcc4d374f103759764e7cad20a969d4a73fdc45987eb2ec968026045cd8050750956ff0ccca38f5ee1c8032cc7cac"; - assert_eq!(encode(&tap_script.to_bytes()), expected_script); - - // 4) Build Taproot scriptPubKey - let tap_info = TaprootBuilder::new() - .add_leaf(0, tap_script.clone())? - .finalize(&secp, internal_xonly) - .map_err(|e| format!("Taproot finalize failed: {:?}", e))?; - let fra_addr = Address::p2tr(&secp, internal_xonly, tap_info.merkle_root(), Network::Regtest); - let fra_spk = fra_addr.script_pubkey(); - println!("FRA Address: {}", fra_addr); - println!("Taproot Merkle Root: {:?}", tap_info.merkle_root()); - - // 5) Broadcast Funding TX - let send_val = fund_utxo.amount.to_sat().saturating_sub(10_000); - if send_val < 546 { - return Err("Transaction amount too small: must be at least 546 satoshi".into()); - } - let fid = rpc.send_to_address(&fra_addr, Amount::from_sat(send_val), None, None, None, None, None, None)?; - println!(">> STEP1: Funding txid = {}", fid); - rpc.generate_to_address(1, &coinbase_addr)?; - sleep(Duration::from_secs(3)); - - // --------------------------------------------------- - // STEP2: SPENDING — Spend the FRA UTXO - // --------------------------------------------------- - println!(">> STEP2: Finding FRA UTXO from funding tx {}", fid); - let funding_tx = rpc.get_raw_transaction(&fid, None)?; - let (fra_vout, fra_txout) = funding_tx.output.iter().enumerate() - .find(|(_vout, txout)| txout.script_pubkey == fra_spk) - .expect("No output matching fra_spk in funding transaction"); - let fra_outpoint = OutPoint { txid: fid, vout: fra_vout as u32 }; - let fra_amount = fra_txout.value; - println!(" Found FRA UTXO at {}:{} with value {} BTC", fid, fra_vout, fra_amount.to_btc()); - - let left = fra_amount.to_sat().saturating_sub(10_000); - if left < 546 { - return Err("Spend transaction amount too small: must be at least 546 satoshi".into()); - } - let spend_tx = Transaction { - version: Version(2), - lock_time: LockTime::ZERO, - input: vec![TxIn { - previous_output: fra_outpoint.clone(), - script_sig: ScriptBuf::new(), - sequence: Sequence(0xFFFF_FFFF), - witness: Witness::new(), - }], - output: vec![TxOut { - value: Amount::from_sat(left), - script_pubkey: rpc.get_new_address(None, Some(AddressType::Legacy))?.require_network(Network::Regtest)?.script_pubkey(), - }], - }; - - // 6) Calculate Taproot Sighash - let mut cache = SighashCache::new(&spend_tx); - let tapleaf_hash = TapLeafHash::from_script(&tap_script, LeafVersion::TapScript); - let sighash = cache.taproot_script_spend_signature_hash( - 0, - &Prevouts::All(&[fra_txout.clone()]), - tapleaf_hash, - TapSighashType::Default, - )?; - let msg = Message::from_digest_slice(sighash.as_ref())?; - println!("Sighash: {:?}", encode(sighash.as_ref())); - - // 7) Generate Schnorr signatures - let sig_sender = secp.sign_schnorr(&msg, &sender_kp); - let sig_receiver = secp.sign_schnorr(&msg, &receiver_kp); - let is_sender_sig_valid = secp.verify_schnorr(&sig_sender, &msg, &sender_xonly); - let is_receiver_sig_valid = secp.verify_schnorr(&sig_receiver, &msg, &receiver_xonly); - println!("Sender signature valid: {}", is_sender_sig_valid.is_ok()); - println!("Receiver signature valid: {}", is_receiver_sig_valid.is_ok()); - let sig_sender_bytes = sig_sender.as_ref().to_vec(); - let sig_receiver_bytes = sig_receiver.as_ref().to_vec(); - println!("Sig Sender: {:?}", encode(&sig_sender_bytes)); - println!("Sig Receiver: {:?}", encode(&sig_receiver_bytes)); - - // 8) Generate Control Block - let control_block_bytes = tap_info.control_block(&(tap_script.clone(), LeafVersion::TapScript)) - .expect("Failed to get control block") - .serialize(); - println!("Control Block: {:?}", encode(&control_block_bytes)); - - // 9) Assemble Witness and broadcast - let mut final_tx = spend_tx.clone(); - final_tx.input[0].witness = Witness::from_slice(&[ - sig_sender_bytes, // Corresponds to sender_pk (OP_CHECKSIGVERIFY) - sig_receiver_bytes, // Corresponds to receiver_pk (OP_CHECKSIG) - tap_script.to_bytes(), - control_block_bytes, - ]); - println!("Final Witness elements:"); - for (i, elem) in final_tx.input[0].witness.iter().enumerate() { - println!(" Witness[{}] (len {}): {:?}", i, elem.len(), encode(elem)); - } - - let raw_spend = serialize(&final_tx); - println!("Raw Spend Tx: {:?}", encode(&raw_spend)); - let sid = rpc.send_raw_transaction(&raw_spend)?; - println!("Spend txid = {}", sid); - Ok(()) -} diff --git a/derive/examples/fra_demo4.rs b/derive/examples/fra_demo_bpcore.rs similarity index 76% rename from derive/examples/fra_demo4.rs rename to derive/examples/fra_demo_bpcore.rs index 2a6e9ba..8e6dd8b 100644 --- a/derive/examples/fra_demo4.rs +++ b/derive/examples/fra_demo_bpcore.rs @@ -1,4 +1,5 @@ -// fra_demo4_method_a.rs +// fra_demo4_method_a_aligned.rs + use std::{thread::sleep, time::Duration, str::FromStr}; use bitcoincore_rpc::{Auth, Client, RpcApi}; @@ -18,7 +19,6 @@ use derive::{ fra::{FraAction, build_fra_control_blocks}, XOnlyPk, }; -use bitcoin::consensus::encode; use bc::{ self, ConsensusEncode, @@ -30,7 +30,6 @@ use amplify::{Wrapper, ByteArray}; /// helper: rpc OutPoint -> bc::Outpoint fn to_bc_outpoint(rpc_out: OutPoint) -> bc::Outpoint { - // 【修改】bitcoincore-rpc 的 Txid 需要先 as_hash() 再 to_byte_array() bc::Outpoint::new( bc::Txid::from_byte_array(rpc_out.txid.to_byte_array()), bc::Vout::from_u32(rpc_out.vout), @@ -53,7 +52,7 @@ fn main() -> Result<(), Box> { "http://127.0.0.1:18443/wallet/legacy_true", Auth::UserPass("foo".into(), "bar".into()), )?; - // 确保用于 demo 的两个私钥已导入钱包(只是为了方便广播 funding tx) + // 导入两个演示用私钥 rpc.import_private_key(&bitcoincore_rpc::bitcoin::PrivateKey::from_wif( "cVkVW14o6zBGyhaV2xqMsGEqYijB6jzsK5EkNcmzBPJejGmcBrMQ" )?, None, None)?; @@ -61,7 +60,7 @@ fn main() -> Result<(), Box> { "cPMvbJRTmycMYU3pQ3dTfCwzVEtyVqpEeV3LWNaT1pzHhax2FKZF" )?, None, None)?; - // 1) 挖 101 确保 coinbase 成熟 + // 1) 挖 101 块确保 coinbase 成熟 let coinbase_addr = rpc.get_new_address(None, Some(AddressType::Legacy))? .require_network(Network::Regtest)?; rpc.generate_to_address(101, &coinbase_addr)?; @@ -78,15 +77,14 @@ fn main() -> Result<(), Box> { // 3) 生成 internal keypair(用于 Taproot internal key) let secp = Secp256k1::new(); let internal_kp = Keypair::new(&secp, &mut rand::thread_rng()); - // 【注意】这里保留 bitcoin::secp256k1::XOnlyPublicKey 以便传给 rust-bitcoin::Address::p2tr let internal_xonly_key: XOnlyPublicKey = internal_kp.public_key().x_only_public_key().0; - // 同时为了 bp-core 使用,将其转换为 bp-core 的 XOnlyPk -> InternalPk + // 转为 bp-core 的 InternalPk let internal_x_bytes = internal_xonly_key.serialize(); let internal_xonly = XOnlyPk::from_byte_array(internal_x_bytes).expect("bad internal xonly"); let internal_pk = bc::InternalPk::from_unchecked(internal_xonly); - // 4) 解析 sender / receiver 私钥,并建 keypairs(用于签名) + // 4) sender / receiver 密钥对 let sender_priv = bitcoincore_rpc::bitcoin::PrivateKey::from_wif( "cVkVW14o6zBGyhaV2xqMsGEqYijB6jzsK5EkNcmzBPJejGmcBrMQ" )?; @@ -98,13 +96,14 @@ fn main() -> Result<(), Box> { let sender_kp = Keypair::from_secret_key(&secp, &sender_sk); let recv_kp = Keypair::from_secret_key(&secp, &recv_sk); - // 构造 bp-core XOnlyPk(用于脚本中的公钥字节) + // 转为 bp-core XOnlyPk(用于脚本公钥字节) let sender_xonly_key = sender_kp.public_key().x_only_public_key().0; let recv_xonly_key = recv_kp.public_key().x_only_public_key().0; let sender_xonly = XOnlyPk::from_byte_array(sender_xonly_key.serialize()).expect("bad sender"); let recv_xonly = XOnlyPk::from_byte_array(recv_xonly_key.serialize()).expect("bad recv"); // 5) 构造 FRA Transfer leaf(bp-std) + // OP_CHECKSIGVERIFY OP_CHECKSIG let action = FraAction::Transfer { asset_id: [0u8; 32], amount: 1000, @@ -113,31 +112,22 @@ fn main() -> Result<(), Box> { }; let depth: amplify::num::u7 = 0u8.try_into().unwrap(); - // 6) 使用 bp-std 构建 ControlBlock + LeafScript + // 6) 用 bp-std 构建 (ControlBlock, LeafScript) let proofs = build_fra_control_blocks(internal_pk.clone(), vec![(action, depth)]); let (control_block, leaf_script) = &proofs[0]; println!("⛑️ ControlBlock bytes: {:?}", control_block.consensus_serialize()); println!("⛑️ LeafScript bytes: {:?}", leaf_script.script.as_inner()); - // 7) 计算 leaf hash (bp-core TapLeafHash) - let leaf_hash = leaf_script.tap_leaf_hash(); // bp-core 类型 + // 7) 计算 leaf hash(bp-core TapLeafHash),并转换为 rust-bitcoin TapNodeHash + let leaf_hash = leaf_script.tap_leaf_hash(); println!("Leaf Hash: {:?}", leaf_hash.to_byte_array().to_hex()); - // --------------------------- - // 【关键:Method A 的单一转换点】 - // 在此把 bp-core 的 TapLeafHash(bytes) -> bitcoin::TapNodeHash(rust-bitcoin) - // 仅此一次的字节级转换,然后传给 Address::p2tr。 - // 这样 rust-bitcoin 会自己计算 tweak(internal_xonly + merkle_root), - // 并生成与 bp-core 相同的 tweaked output key。 - // --------------------------- - let inner_arr = leaf_hash.into_inner(); // 得到 amplify::Array - let inner_bytes = inner_arr.to_byte_array(); // -> [u8; 32] + let inner_arr = leaf_hash.into_inner(); + let inner_bytes = inner_arr.to_byte_array(); let merkle_root = bitcoin::TapNodeHash::from_slice(&inner_bytes) .expect("Invalid tap node hash"); - - // 8) 用 rust-bitcoin 的 Address::p2tr 生成带脚本路径的 P2TR 地址 - // 传入 internal_xonly_key 和 merkle_root(上一步转好的) + // 8) 生成带脚本路径的 P2TR 地址(rust-bitcoin 计算 tweak) let fra_addr = bitcoincore_rpc::bitcoin::Address::p2tr( &secp, internal_xonly_key, @@ -154,11 +144,10 @@ fn main() -> Result<(), Box> { Amount::from_sat(fund_utxo.amount.to_sat().saturating_sub(10_000)), None, None, None, None, None, None, )?; - // 确认 funding rpc.generate_to_address(1, &coinbase_addr)?; sleep(Duration::from_secs(3)); - // 10) 找到 funding 输出并构造要花费的交易(用 bp-core 的 Tx) + // 10) 找到 funding 输出并构造要花费的交易(bp-core Tx) let funding_tx = rpc.get_raw_transaction(&fid, None)?; let (idx, found_vout) = funding_tx.output.iter().enumerate() .find(|(_, o)| o.script_pubkey.to_bytes() == fra_spk.to_bytes()) @@ -183,46 +172,41 @@ fn main() -> Result<(), Box> { }]), }; - // 11) 计算 sighash(仍然用 bp-core 的 SighashCache) + // 11) 计算 sighash(bp-core 的 SighashCache) let prevout_bc = to_bc_txout(found_vout.clone()); let mut cache = SighashCache::new(&mut spend_tx, vec![prevout_bc])?; - - // 直接调用我们刚刚修复的函数 - let sighash = cache.tap_sighash_script(0, leaf_hash, None)?; + let sighash = cache.tap_sighash_script(0, leaf_script.tap_leaf_hash(), None)?; let sighash_bytes: [u8; 32] = sighash.into(); - println!("Sighash: {}", sighash_bytes.to_hex()); let msg = Message::from_digest_slice(&sighash_bytes).expect("32 bytes"); - - // 12) signer:双方用 schnorr 签名(message + keypair) + // 12) 双方 Schnorr 签名 let sig_sender = secp.sign_schnorr(&msg, &sender_kp); let sig_receiver = secp.sign_schnorr(&msg, &recv_kp); println!("sig_sender length: {}", sig_sender.as_ref().len()); println!("sig_receiver length: {}", sig_receiver.as_ref().len()); - // 13) 组装 witness (必须严格遵循脚本的预期顺序!) - // 脚本逻辑 (来自 fra.rs): - // OP_SWAP OP_CHECKSIGVERIFY OP_SWAP OP_CHECKSIG - // - // 预期的 Witness (从栈顶 -> 栈底): - // - sender_sig - // - receiver_sig + // 13) 组装 witness —— 与 fra_demo6.rs 完全一致 + // 脚本逻辑: OP_CHECKSIGVERIFY OP_CHECKSIG + // Witness(从栈顶 -> 栈底): + // - receiver_sig (对应最后一步 OP_CHECKSIG) + // - sender_sig (对应第一步 OP_CHECKSIGVERIFY) + // - leaf_script + // - control_block spend_tx.inputs[0].witness = Witness::from_consensus_stack(vec![ - sig_sender.as_ref().to_vec(), // 对应 OP_CHECKSIG - sig_receiver.as_ref().to_vec(), // 对应 OP_CHECKSIGVERIFY + sig_receiver.as_ref().to_vec(), // 对应 OP_CHECKSIG + sig_sender.as_ref().to_vec(), // 对应 OP_CHECKSIGVERIFY leaf_script.script.as_inner().to_vec(), control_block.consensus_serialize(), ]); // 14) 广播(raw) let raw_bytes = spend_tx.consensus_serialize(); - // 将字节转换为十六进制字符串 let raw_hex = raw_bytes.to_hex(); println!("RAW_TX_HEX: {}", raw_hex); let sid = rpc.send_raw_transaction(&*raw_hex)?; println!("🎉 Spend TXID = {}", sid); Ok(()) -} +} \ No newline at end of file diff --git a/derive/examples/fra_demo5.rs b/derive/examples/fra_demo_rustbitcoin.rs similarity index 85% rename from derive/examples/fra_demo5.rs rename to derive/examples/fra_demo_rustbitcoin.rs index 448d105..bba49a2 100644 --- a/derive/examples/fra_demo5.rs +++ b/derive/examples/fra_demo_rustbitcoin.rs @@ -1,12 +1,10 @@ // fra_demo5_final_solution_v3.rs -use std::str::FromStr; // 保留以备 Address::from_str 使用 +use std::str::FromStr; -// --- 修正后的 rust-bitcoin 导入 --- use bitcoin::{ self, consensus::encode, - hashes::Hash, secp256k1::SecretKey, key::{Keypair, Secp256k1}, absolute::LockTime, @@ -16,11 +14,9 @@ use bitcoin::{ Address, Amount, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Witness, }; -// --- RPC 库保持不变 --- use bitcoincore_rpc::{Auth, Client, RpcApi}; use bitcoincore_rpc::json::AddressType; -// --- 仍然需要的 bp-core/std 类型 --- use derive::{ fra::{build_fra_script, FraAction}, XOnlyPk, @@ -44,10 +40,9 @@ fn main() -> Result<(), Box> { None, None, )?; - // --- 关键修正: Address 类型 --- let coinbase_addr_unchecked = rpc.get_new_address(None, Some(AddressType::Legacy))?; let coinbase_addr = coinbase_addr_unchecked.require_network(Network::Regtest)?; - rpc.generate_to_address(101, &coinbase_addr)?; // 现在类型匹配 + rpc.generate_to_address(101, &coinbase_addr)?; let balance = rpc.get_balance(None, None)?; println!("Wallet balance: {} BTC", balance); let fund_utxo = rpc @@ -80,17 +75,15 @@ fn main() -> Result<(), Box> { let recv_pk = recv_kp.x_only_public_key().0; // =================================================================== - // 步骤 5-8: 地址生成 + // 步骤 5-8: 地址生成 (调用 build_fra_script) // =================================================================== - // --- 关键修正: XOnlyPk 构造 --- let action = FraAction::Transfer { asset_id: [0u8; 32], amount: 1000, receiver: XOnlyPk::from_byte_array(recv_pk.serialize()).unwrap(), sender: XOnlyPk::from_byte_array(sender_pk.serialize()).unwrap(), }; - // --- 关键修正: .into_inner() 已被废弃, 使用 .release() --- - let leaf_script_bytes = build_fra_script(action).as_inner().to_vec(); + let leaf_script_bytes = build_fra_script(action).as_unconfined().to_vec(); let script = ScriptBuf::from(leaf_script_bytes); println!("Leaf Script ({} bytes): {}", script.len(), script.to_hex_string()); @@ -100,16 +93,15 @@ fn main() -> Result<(), Box> { println!("FRA Taproot 地址: {}", fra_addr); // =================================================================== - // 步骤 9-10: 交易注资 + // 步骤 9-11: 交易注资和花费交易骨架构建 // =================================================================== - // --- 关键修正: RPC 地址类型 --- let rpc_address = Address::from_str(&fra_addr.to_string())?.assume_checked(); let funding_txid = rpc.send_to_address( &rpc_address, Amount::from_sat(fund_utxo.amount.to_sat() - 10_000), None, None, None, None, None, None, )?; - rpc.generate_to_address(1, &coinbase_addr)?; // 现在类型匹配 + rpc.generate_to_address(1, &coinbase_addr)?; println!("Funding TXID: {}", funding_txid); let funding_tx_raw = rpc.get_raw_transaction(&funding_txid, None)?; let (vout, prevout_value) = funding_tx_raw @@ -124,9 +116,6 @@ fn main() -> Result<(), Box> { vout, }; - // =================================================================== - // 步骤 11: 构建花费交易 - // =================================================================== let dest_addr_unchecked = rpc.get_new_address(None, Some(AddressType::Legacy))?; let dest_addr = dest_addr_unchecked.require_network(Network::Regtest)?; let mut spend_tx = Transaction { @@ -149,7 +138,7 @@ fn main() -> Result<(), Box> { }]; // =================================================================== - // 步骤 12: 计算 Sighash (使用修正后的 API) + // 步骤 12: 计算 Sighash // =================================================================== let mut sighasher = SighashCache::new(&spend_tx); let leaf_hash = taproot::TapLeafHash::from_script(&script, LeafVersion::TapScript); @@ -162,7 +151,7 @@ fn main() -> Result<(), Box> { )?; let msg = bitcoin::secp256k1::Message::from(sighash); - println!("Sighash (rust-bitcoin): {}", sighash.to_string()); // .to_hex_string() -> .to_string() + println!("Sighash (rust-bitcoin): {}", sighash.to_string()); // =================================================================== // 步骤 13: 签名并构建 Witness @@ -175,8 +164,14 @@ fn main() -> Result<(), Box> { .unwrap(); let mut witness = Witness::new(); - witness.push(sig_sender.as_ref()); - witness.push(sig_receiver.as_ref()); + + // --- Witness 顺序必须与脚本消耗顺序相反 --- + // 脚本: OP_CHECKSIGVERIFY OP_CHECKSIG + // 1. 脚本先验证 sender,所以 sender_sig 必须在栈顶。 + // 2. 为了让 sender_sig 在栈顶,它必须是最后一个被 push 的签名。 + witness.push(sig_receiver.as_ref()); // 先推入 receiver 签名 (对应 OP_CHECKSIG) + witness.push(sig_sender.as_ref()); // 后推入 sender 签名 (对应 OP_CHECKSIGVERIFY) + witness.push(script); witness.push(control_block.serialize()); spend_tx.input[0].witness = witness; @@ -187,7 +182,7 @@ fn main() -> Result<(), Box> { let tx_hex = encode::serialize_hex(&spend_tx); println!("Final TX Hex: {}", tx_hex); - let final_txid = rpc.send_raw_transaction(&*tx_hex)?; // Pass String by value + let final_txid = rpc.send_raw_transaction(&*tx_hex)?; println!("\n🎉🎉🎉 交易成功广播! TXID = {} 🎉🎉🎉", final_txid); Ok(()) diff --git a/derive/examples/simple_test.rs b/derive/examples/simple_test.rs index 18a7392..9fdfd62 100644 --- a/derive/examples/simple_test.rs +++ b/derive/examples/simple_test.rs @@ -1,8 +1,7 @@ -// 文件: derive/examples/simple_test.rs use bitcoincore_rpc::{Auth, Client, RpcApi}; use bitcoincore_rpc::bitcoin::{ - self, blockdata::opcodes, Address, Amount, Network, OutPoint, ScriptBuf, + blockdata::opcodes, Address, Amount, Network, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Witness, absolute::LockTime, transaction::Version, @@ -12,7 +11,6 @@ use bitcoincore_rpc::bitcoin::{ }; use bitcoincore_rpc::json::AddressType; use std::str::FromStr; -use amplify::hex; // 使用 amplify 的 hex use derive::base58::encode; fn main() -> Result<(), Box> { diff --git a/derive/src/fra.rs b/derive/src/fra.rs index 9884a81..a957956 100644 --- a/derive/src/fra.rs +++ b/derive/src/fra.rs @@ -6,7 +6,7 @@ use bc::{ }; use crate::taptree::{TapTree, LeafInfo, ControlBlockFactory}; -// --- Helper 函数 --- +// --- Helper 函数 --- /// Helper: 将字节块编码到脚本缓冲 fn push_bytes(buf: &mut Vec, data: &[u8]) { @@ -14,7 +14,7 @@ fn push_bytes(buf: &mut Vec, data: &[u8]) { if len == 0 { buf.push(TapCode::PushBytes0 as u8); } else if len <= 75 { - buf.push((TapCode::PushBytes1 as u8).wrapping_add((len - 1) as u8)); + buf.push(len as u8); } else if len < 0x100 { buf.push(TapCode::PushData1 as u8); buf.push(len as u8); @@ -51,14 +51,14 @@ fn push_int(buf: &mut Vec, value: u64) { /// 支持的 FRA 操作类型 #[derive(Clone, Debug, PartialEq, Eq)] pub enum FraAction { - /// 转账:双重签名,先接收方同意再发送方授权 + /// 转账:双重签名 Transfer { - asset_id: [u8; 32], // 资产标识 - amount: u64, // 转账数量 - receiver: XOnlyPk, // 接收方公钥 - sender: XOnlyPk, // 发送方公钥 + asset_id: [u8; 32], + amount: u64, + receiver: XOnlyPk, + sender: XOnlyPk, }, - /// 增发:单方授权,只有增发者能执行 + /// 增发:单方授权 Mint { asset_id: [u8; 32], amount: u64, @@ -140,89 +140,89 @@ pub enum FraAction { pub fn build_fra_script(action: FraAction) -> TapScript { let mut buf = Vec::new(); match action { - // --- 转账动作 (Transfer) --- - // - // 脚本逻辑: - // OP_SWAP OP_CHECKSIGVERIFY OP_SWAP OP_CHECKSIG - // - // 预期 Witness (从栈顶 -> 栈底): - // - sender_sig (发送方签名) - // - receiver_sig (接收方签名) - // - // 执行流程: - // 1. 初始栈: [sender_sig, receiver_sig] - // 2. 推入 receiver_pk -> 栈: [sender_sig, receiver_sig, receiver_pk] - // 3. OP_SWAP -> 栈: [sender_sig, receiver_pk, receiver_sig] - // 4. OP_CHECKSIGVERIFY 消耗 receiver_pk 和 receiver_sig, 验证通过。栈: [sender_sig] - // 5. 推入 sender_pk -> 栈: [sender_sig, sender_pk] - // 6. OP_SWAP -> 栈: [sender_pk, sender_sig] - // 7. OP_CHECKSIG 消耗 sender_pk 和 sender_sig, 验证通过, 最终在栈上留下 TRUE。 + // --- Transfer FraAction::Transfer { receiver, sender, .. } => { - // 注意:当前实现仅包含签名逻辑。 - // 未来可在这里添加对 asset_id 和 amount 的承诺校验 (例如使用 OP_EQUALVERIFY)。 - - // --- 验证接收方签名 --- - push_bytes(&mut buf, &receiver.to_byte_array()); - buf.push(TapCode::Swap as u8); - buf.push(TapCode::CheckSigVerify as u8); - - // --- 验证发送方签名 --- push_bytes(&mut buf, &sender.to_byte_array()); - buf.push(TapCode::Swap as u8); + buf.push(TapCode::CheckSigVerify as u8); + push_bytes(&mut buf, &receiver.to_byte_array()); buf.push(TapCode::CheckSig as u8); } - // --- 增发、销毁等其他操作的脚本实现 --- - // 以下逻辑基于 "数据承诺 + 单签名" 模式,即先将操作数据压栈, - // 然后提供授权者公钥,用 OP_SWAP 交换栈顶的签名和公钥,最后用 OP_CHECKSIGVERIFY 验证。 - // 这是一种健壮且常见的模式。 - + // --- 单签通用模式: Commit -> Cleanup -> Verify --- FraAction::Mint { asset_id, amount, receiver, minter } => { + // Commit: 3 items push_bytes(&mut buf, &asset_id); push_int(&mut buf, amount); push_bytes(&mut buf, &receiver.to_byte_array()); + // Cleanup + buf.push(TapCode::Drop as u8); + buf.push(TapCode::Drop as u8); + buf.push(TapCode::Drop as u8); + // Verify push_bytes(&mut buf, &minter.to_byte_array()); - buf.push(TapCode::Swap as u8); - buf.push(TapCode::CheckSigVerify as u8); + buf.push(TapCode::CheckSig as u8); } FraAction::Burn { asset_id, amount, owner } => { + // Commit: 2 items push_bytes(&mut buf, &asset_id); push_int(&mut buf, amount); + // Cleanup + buf.push(TapCode::Drop2 as u8); // Nip is equivalent to SWAP then DROP + // Verify push_bytes(&mut buf, &owner.to_byte_array()); - buf.push(TapCode::Swap as u8); - buf.push(TapCode::CheckSigVerify as u8); + buf.push(TapCode::CheckSig as u8); } FraAction::Rollback { asset_id, amount, owner, minter } => { + // Commit: 3 items push_bytes(&mut buf, &asset_id); push_int(&mut buf, amount); push_bytes(&mut buf, &minter.to_byte_array()); // 承诺归还目标 + // Cleanup + buf.push(TapCode::Drop as u8); + buf.push(TapCode::Drop as u8); + buf.push(TapCode::Drop as u8); + // Verify push_bytes(&mut buf, &owner.to_byte_array()); - buf.push(TapCode::Swap as u8); - buf.push(TapCode::CheckSigVerify as u8); + buf.push(TapCode::CheckSig as u8); } FraAction::Redeem { asset_id, amount, owner } => { + // Commit: 2 items push_bytes(&mut buf, &asset_id); push_int(&mut buf, amount); + // Cleanup + buf.push(TapCode::Drop2 as u8); + // Verify push_bytes(&mut buf, &owner.to_byte_array()); - buf.push(TapCode::Swap as u8); - buf.push(TapCode::CheckSigVerify as u8); + buf.push(TapCode::CheckSig as u8); } FraAction::Split { asset_id, orig_amount, outputs, owner } => { + // Commit data push_bytes(&mut buf, &asset_id); - push_int(&mut buf, orig_amount); + for (_, pk) in &outputs { + push_bytes(&mut buf, &pk.to_byte_array()); + } + // Math part for (amt, _) in &outputs { push_int(&mut buf, *amt); } for _ in 0..outputs.len().saturating_sub(1) { buf.push(TapCode::Add as u8); } + push_int(&mut buf, orig_amount); buf.push(TapCode::EqualVerify as u8); + // Cleanup data commitments (asset_id + N output pks) + for _ in 0..=outputs.len() { + buf.push(TapCode::Drop as u8); + } + // Verify push_bytes(&mut buf, &owner.to_byte_array()); - buf.push(TapCode::Swap as u8); - buf.push(TapCode::CheckSigVerify as u8); + buf.push(TapCode::CheckSig as u8); } FraAction::Merge { asset_id, inputs, recipient, owner } => { + // Commit data push_bytes(&mut buf, &asset_id); + push_bytes(&mut buf, &recipient.to_byte_array()); + // Math part let merged_amt: u64 = inputs.iter().sum(); for amt in &inputs { push_int(&mut buf, *amt); @@ -232,52 +232,57 @@ pub fn build_fra_script(action: FraAction) -> TapScript { } push_int(&mut buf, merged_amt); buf.push(TapCode::EqualVerify as u8); - push_bytes(&mut buf, &recipient.to_byte_array()); + // Cleanup data commitments (asset_id + recipient_pk) + buf.push(TapCode::Drop2 as u8); + // Verify push_bytes(&mut buf, &owner.to_byte_array()); - buf.push(TapCode::Swap as u8); - buf.push(TapCode::CheckSigVerify as u8); + buf.push(TapCode::CheckSig as u8); } FraAction::Freeze { asset_id, authority } => { push_bytes(&mut buf, &asset_id); + buf.push(TapCode::Drop as u8); push_bytes(&mut buf, &authority.to_byte_array()); - buf.push(TapCode::Swap as u8); - buf.push(TapCode::CheckSigVerify as u8); + buf.push(TapCode::CheckSig as u8); } FraAction::Unfreeze { asset_id, authority } => { push_bytes(&mut buf, &asset_id); + buf.push(TapCode::Drop as u8); push_bytes(&mut buf, &authority.to_byte_array()); - buf.push(TapCode::Swap as u8); - buf.push(TapCode::CheckSigVerify as u8); + buf.push(TapCode::CheckSig as u8); } FraAction::GrantRole { asset_id, role, target, admin } => { push_bytes(&mut buf, &asset_id); push_bytes(&mut buf, &role); push_bytes(&mut buf, &target.to_byte_array()); + buf.push(TapCode::Drop as u8); + buf.push(TapCode::Drop as u8); + buf.push(TapCode::Drop as u8); push_bytes(&mut buf, &admin.to_byte_array()); - buf.push(TapCode::Swap as u8); - buf.push(TapCode::CheckSigVerify as u8); + buf.push(TapCode::CheckSig as u8); } FraAction::RevokeRole { asset_id, role, target, admin } => { push_bytes(&mut buf, &asset_id); push_bytes(&mut buf, &role); push_bytes(&mut buf, &target.to_byte_array()); + buf.push(TapCode::Drop as u8); + buf.push(TapCode::Drop as u8); + buf.push(TapCode::Drop as u8); push_bytes(&mut buf, &admin.to_byte_array()); - buf.push(TapCode::Swap as u8); - buf.push(TapCode::CheckSigVerify as u8); + buf.push(TapCode::CheckSig as u8); } FraAction::Upgrade { asset_id, new_version, authority } => { push_bytes(&mut buf, &asset_id); push_bytes(&mut buf, &new_version); + buf.push(TapCode::Drop2 as u8); push_bytes(&mut buf, &authority.to_byte_array()); - buf.push(TapCode::Swap as u8); - buf.push(TapCode::CheckSigVerify as u8); + buf.push(TapCode::CheckSig as u8); } FraAction::MetadataUpdate { asset_id, metadata, authority } => { push_bytes(&mut buf, &asset_id); push_bytes(&mut buf, &metadata); + buf.push(TapCode::Drop2 as u8); push_bytes(&mut buf, &authority.to_byte_array()); - buf.push(TapCode::Swap as u8); - buf.push(TapCode::CheckSigVerify as u8); + buf.push(TapCode::CheckSig as u8); } } TapScript::from_checked(buf) diff --git a/derive/tests/fra_actions.rs b/derive/tests/fra_actions.rs new file mode 100644 index 0000000..f2f4b33 --- /dev/null +++ b/derive/tests/fra_actions.rs @@ -0,0 +1,401 @@ +// tests/fra_actions.rs + +use std::str::FromStr; +use bitcoincore_rpc::{Auth, Client, RpcApi, json::ListUnspentResultEntry}; +use bitcoincore_rpc::bitcoin::{ + Address, Amount, Network, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Witness, + absolute::LockTime, transaction::Version, + secp256k1::{Secp256k1, Keypair, SecretKey, Message, All}, + taproot::{self, LeafVersion, TaprootBuilder}, + sighash::{self, Prevouts, SighashCache, TapSighash}, + consensus::encode, +}; +use rand::thread_rng; + +// 导入合约逻辑 +use derive::{ + fra::{build_fra_script, FraAction}, + XOnlyPk, OutputPk, +}; +use amplify::Wrapper; // For .as_inner() + +// --- 测试辅助结构体和函数 --- + +// 封装测试环境所需的所有组件 +struct TestEnv { + rpc: Client, + secp: Secp256k1, + funding_utxo: ListUnspentResultEntry, + internal_kp: Keypair, + authority_kp: Keypair, // 用于 Freeze, Unfreeze, Upgrade, Metadata + minter_kp: Keypair, // 用于 Mint + admin_kp: Keypair, // 用于 GrantRole, RevokeRole + sender_kp: Keypair, // 用于 Transfer, a.k.a owner + receiver_kp: Keypair, // 用于 Transfer +} + +// 统一的测试环境设置函数 +fn setup_test_environment() -> Result> { + let rpc = Client::new( + "http://127.0.0.1:18443/wallet/legacy_true", + Auth::UserPass("foo".into(), "bar".into()), + )?; + + // 确保钱包有资金 + if rpc.get_balance(None, Some(true))? < Amount::from_btc(5.0)? { + let addr = rpc.get_new_address(None, None)?.assume_checked(); + rpc.generate_to_address(101, &addr)?; + } + + // 寻找一个可用的 UTXO + let funding_utxo = rpc.list_unspent(Some(1), None, None, None, None)? + .into_iter() + .find(|u| u.amount > Amount::from_sat(100_000)) + .ok_or("No suitable funding UTXO found")?; + + let secp = Secp256k1::new(); + let mut rng = thread_rng(); + + // 生成所有需要的密钥对 + Ok(TestEnv { + rpc, + secp: secp.clone(), + funding_utxo, + internal_kp: Keypair::new(&secp, &mut rng), + authority_kp: Keypair::new(&secp, &mut rng), + minter_kp: Keypair::new(&secp, &mut rng), + admin_kp: Keypair::new(&secp, &mut rng), + sender_kp: Keypair::from_secret_key(&secp, &SecretKey::from_str("1111111111111111111111111111111111111111111111111111111111111111")?), + receiver_kp: Keypair::from_secret_key(&secp, &SecretKey::from_str("2222222222222222222222222222222222222222222222222222222222222222")?), + }) +} + +// 统一的单签操作测试函数 +fn run_single_signer_test( + action: FraAction, + signer_kp: &Keypair, +) -> Result<(), Box> { + let env = setup_test_environment()?; + let (internal_pk, _) = env.internal_kp.x_only_public_key(); + + // 1. 构建脚本和地址 + let script_bytes = build_fra_script(action).as_inner().to_vec(); + let script = ScriptBuf::from(script_bytes); + + let builder = TaprootBuilder::new().add_leaf(0, script.clone()).unwrap(); + let spend_info = builder.finalize(&env.secp, internal_pk).unwrap(); + let address = Address::p2tr(&env.secp, internal_pk, spend_info.merkle_root(), Network::Regtest); + + // 2. 注资 + let funding_txid = env.rpc.send_to_address( + &address, + Amount::from_sat(env.funding_utxo.amount.to_sat() - 10000), + None, None, None, None, None, None, + )?; + let funding_addr = env.rpc.get_new_address(None, None)?.assume_checked(); + env.rpc.generate_to_address(1, &funding_addr)?; + println!("\nAction funded with TXID: {}", funding_txid); + + // 3. 准备花费 + let funding_tx_raw = env.rpc.get_raw_transaction(&funding_txid, None)?; + let (vout, prevout_value) = funding_tx_raw.output.iter().enumerate() + .find(|(_, o)| o.script_pubkey == address.script_pubkey()) + .map(|(i, o)| (i as u32, o.value)) + .expect("Funded UTXO not found"); + + let dest_addr = env.rpc.get_new_address(None, None)?.assume_checked(); + let mut spend_tx = Transaction { + version: Version(2), + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint { txid: funding_txid, vout }, + script_sig: ScriptBuf::new(), + sequence: Sequence::MAX, + witness: Witness::new(), + }], + output: vec![TxOut { + value: prevout_value - Amount::from_sat(10000), + script_pubkey: dest_addr.script_pubkey(), + }], + }; + + // 4. 计算 Sighash 并签名 + let mut sighasher = SighashCache::new(&spend_tx); + let leaf_hash = taproot::TapLeafHash::from_script(&script, LeafVersion::TapScript); + let sighash: TapSighash = sighasher.taproot_script_spend_signature_hash( + 0, + &Prevouts::All(&[TxOut { value: prevout_value, script_pubkey: address.script_pubkey() }]), + leaf_hash, + sighash::TapSighashType::Default, + )?; + let msg = Message::from(sighash); + let signature = env.secp.sign_schnorr(&msg, signer_kp); + + // 5. 构建 Witness 并广播 + let control_block = spend_info.control_block(&(script.clone(), LeafVersion::TapScript)).unwrap(); + let mut witness = Witness::new(); + witness.push(signature.as_ref()); // 单签脚本,只需一个签名 + witness.push(script); + witness.push(control_block.serialize()); + spend_tx.input[0].witness = witness; + + let tx_hex = encode::serialize_hex(&spend_tx); + let final_txid = env.rpc.send_raw_transaction(&*tx_hex)?; + println!("Successfully spent! TXID = {}", final_txid); + + Ok(()) +} + + +// --- 测试用例 --- + +#[test] +fn test_fra_transfer() -> Result<(), Box> { + println!("--- Testing FraAction::Transfer ---"); + let env = setup_test_environment()?; + let (internal_pk, _) = env.internal_kp.x_only_public_key(); + let (sender_pk, _) = env.sender_kp.x_only_public_key(); + let (receiver_pk, _) = env.receiver_kp.x_only_public_key(); + + let action = FraAction::Transfer { + asset_id: [1; 32], + amount: 1000, + receiver: XOnlyPk::from_byte_array(receiver_pk.serialize()).unwrap(), + sender: XOnlyPk::from_byte_array(sender_pk.serialize()).unwrap(), + }; + + let script = ScriptBuf::from(build_fra_script(action).as_inner().to_vec()); + let builder = TaprootBuilder::new().add_leaf(0, script.clone()).unwrap(); + let spend_info = builder.finalize(&env.secp, internal_pk).unwrap(); + let address = Address::p2tr(&env.secp, internal_pk, spend_info.merkle_root(), Network::Regtest); + + let funding_txid = env.rpc.send_to_address(&address, Amount::from_sat(50000), None, None, None, None, None, None)?; + let funding_addr = env.rpc.get_new_address(None, None)?.assume_checked(); + env.rpc.generate_to_address(1, &funding_addr)?; + println!("\nAction funded with TXID: {}", funding_txid); + + let funding_tx_raw = env.rpc.get_raw_transaction(&funding_txid, None)?; + let (vout, prevout_value) = funding_tx_raw.output.iter().enumerate() + .find(|(_, o)| o.script_pubkey == address.script_pubkey()) + .map(|(i, o)| (i as u32, o.value)) + .expect("Funded UTXO not found"); + + let dest_addr = env.rpc.get_new_address(None, None)?.assume_checked(); + let mut spend_tx = Transaction { + version: Version(2), + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint { txid: funding_txid, vout }, + script_sig: ScriptBuf::new(), + sequence: Sequence::MAX, + witness: Witness::new(), + }], + output: vec![TxOut { + value: prevout_value - Amount::from_sat(10000), + script_pubkey: dest_addr.script_pubkey(), + }], + }; + + let mut sighasher = SighashCache::new(&spend_tx); + let leaf_hash = taproot::TapLeafHash::from_script(&script, LeafVersion::TapScript); + let sighash: TapSighash = sighasher.taproot_script_spend_signature_hash(0, &Prevouts::All(&[TxOut { value: prevout_value, script_pubkey: address.script_pubkey() }]), leaf_hash, sighash::TapSighashType::Default)?; + let msg = Message::from(sighash); + + let sig_sender = env.secp.sign_schnorr(&msg, &env.sender_kp); + let sig_receiver = env.secp.sign_schnorr(&msg, &env.receiver_kp); + + let control_block = spend_info.control_block(&(script.clone(), LeafVersion::TapScript)).unwrap(); + let mut witness = Witness::new(); + witness.push(sig_receiver.as_ref()); + witness.push(sig_sender.as_ref()); + witness.push(script); + witness.push(control_block.serialize()); + spend_tx.input[0].witness = witness; + + let tx_hex = encode::serialize_hex(&spend_tx); + let final_txid = env.rpc.send_raw_transaction(&*tx_hex)?; + println!("Successfully spent! TXID = {}", final_txid); + + Ok(()) +} + +#[test] +fn test_fra_mint() -> Result<(), Box> { + println!("--- Testing FraAction::Mint ---"); + let env = setup_test_environment()?; + let (minter_pk, _) = env.minter_kp.x_only_public_key(); + let (receiver_pk, _) = env.receiver_kp.x_only_public_key(); + + let action = FraAction::Mint { + asset_id: [2; 32], + amount: 500, + receiver: OutputPk::from_unchecked(XOnlyPk::from_byte_array(receiver_pk.serialize()).unwrap()), + minter: XOnlyPk::from_byte_array(minter_pk.serialize()).unwrap(), + }; + run_single_signer_test(action, &env.minter_kp) +} + +#[test] +fn test_fra_burn() -> Result<(), Box> { + println!("--- Testing FraAction::Burn ---"); + let env = setup_test_environment()?; + let (owner_pk, _) = env.sender_kp.x_only_public_key(); + + let action = FraAction::Burn { + asset_id: [3; 32], + amount: 200, + owner: XOnlyPk::from_byte_array(owner_pk.serialize()).unwrap(), + }; + run_single_signer_test(action, &env.sender_kp) +} + +#[test] +fn test_fra_rollback() -> Result<(), Box> { + println!("--- Testing FraAction::Rollback ---"); + let env = setup_test_environment()?; + let (owner_pk, _) = env.sender_kp.x_only_public_key(); + let (minter_pk, _) = env.minter_kp.x_only_public_key(); + + let action = FraAction::Rollback { + asset_id: [4; 32], + amount: 150, + owner: XOnlyPk::from_byte_array(owner_pk.serialize()).unwrap(), + minter: XOnlyPk::from_byte_array(minter_pk.serialize()).unwrap(), + }; + run_single_signer_test(action, &env.sender_kp) +} + +#[test] +fn test_fra_redeem() -> Result<(), Box> { + println!("--- Testing FraAction::Redeem ---"); + let env = setup_test_environment()?; + let (owner_pk, _) = env.sender_kp.x_only_public_key(); + + let action = FraAction::Redeem { + asset_id: [5; 32], + amount: 100, + owner: XOnlyPk::from_byte_array(owner_pk.serialize()).unwrap(), + }; + run_single_signer_test(action, &env.sender_kp) +} + +#[test] +fn test_fra_freeze() -> Result<(), Box> { + println!("--- Testing FraAction::Freeze ---"); + let env = setup_test_environment()?; + let (authority_pk, _) = env.authority_kp.x_only_public_key(); + + let action = FraAction::Freeze { + asset_id: [6; 32], + authority: XOnlyPk::from_byte_array(authority_pk.serialize()).unwrap(), + }; + run_single_signer_test(action, &env.authority_kp) +} + +#[test] +fn test_fra_unfreeze() -> Result<(), Box> { + println!("--- Testing FraAction::Unfreeze ---"); + let env = setup_test_environment()?; + let (authority_pk, _) = env.authority_kp.x_only_public_key(); + + let action = FraAction::Unfreeze { + asset_id: [7; 32], + authority: XOnlyPk::from_byte_array(authority_pk.serialize()).unwrap(), + }; + run_single_signer_test(action, &env.authority_kp) +} + +#[test] +fn test_fra_grant_role() -> Result<(), Box> { + println!("--- Testing FraAction::GrantRole ---"); + let env = setup_test_environment()?; + let (admin_pk, _) = env.admin_kp.x_only_public_key(); + let (target_pk, _) = env.receiver_kp.x_only_public_key(); + + let action = FraAction::GrantRole { + asset_id: [8; 32], + role: b"MINTER".to_vec(), + target: XOnlyPk::from_byte_array(target_pk.serialize()).unwrap(), + admin: XOnlyPk::from_byte_array(admin_pk.serialize()).unwrap(), + }; + run_single_signer_test(action, &env.admin_kp) +} + +#[test] +fn test_fra_revoke_role() -> Result<(), Box> { + println!("--- Testing FraAction::RevokeRole ---"); + let env = setup_test_environment()?; + let (admin_pk, _) = env.admin_kp.x_only_public_key(); + let (target_pk, _) = env.receiver_kp.x_only_public_key(); + + let action = FraAction::RevokeRole { + asset_id: [9; 32], + role: b"MINTER".to_vec(), + target: XOnlyPk::from_byte_array(target_pk.serialize()).unwrap(), + admin: XOnlyPk::from_byte_array(admin_pk.serialize()).unwrap(), + }; + run_single_signer_test(action, &env.admin_kp) +} + +#[test] +fn test_fra_upgrade() -> Result<(), Box> { + println!("--- Testing FraAction::Upgrade ---"); + let env = setup_test_environment()?; + let (authority_pk, _) = env.authority_kp.x_only_public_key(); + + let action = FraAction::Upgrade { + asset_id: [10; 32], + new_version: [0xff; 32], + authority: XOnlyPk::from_byte_array(authority_pk.serialize()).unwrap(), + }; + run_single_signer_test(action, &env.authority_kp) +} + +#[test] +fn test_fra_metadata_update() -> Result<(), Box> { + println!("--- Testing FraAction::MetadataUpdate ---"); + let env = setup_test_environment()?; + let (authority_pk, _) = env.authority_kp.x_only_public_key(); + + let action = FraAction::MetadataUpdate { + asset_id: [11; 32], + metadata: b"NEW_METADATA_HASH".to_vec(), + authority: XOnlyPk::from_byte_array(authority_pk.serialize()).unwrap(), + }; + run_single_signer_test(action, &env.authority_kp) +} + +#[test] +fn test_fra_split() -> Result<(), Box> { + println!("--- Testing FraAction::Split ---"); + let env = setup_test_environment()?; + let (owner_pk, _) = env.sender_kp.x_only_public_key(); + let (out_pk, _) = env.receiver_kp.x_only_public_key(); + + let action = FraAction::Split { + asset_id: [12; 32], + orig_amount: 1000, + outputs: vec![ + (600, OutputPk::from_unchecked(XOnlyPk::from_byte_array(out_pk.serialize()).unwrap())), + (400, OutputPk::from_unchecked(XOnlyPk::from_byte_array(out_pk.serialize()).unwrap())), + ], + owner: XOnlyPk::from_byte_array(owner_pk.serialize()).unwrap(), + }; + run_single_signer_test(action, &env.sender_kp) +} + +#[test] +fn test_fra_merge() -> Result<(), Box> { + println!("--- Testing FraAction::Merge ---"); + let env = setup_test_environment()?; + let (owner_pk, _) = env.sender_kp.x_only_public_key(); + let (recipient_pk, _) = env.receiver_kp.x_only_public_key(); + + let action = FraAction::Merge { + asset_id: [13; 32], + inputs: vec![300, 700], + recipient: OutputPk::from_unchecked(XOnlyPk::from_byte_array(recipient_pk.serialize()).unwrap()), + owner: XOnlyPk::from_byte_array(owner_pk.serialize()).unwrap(), + }; + run_single_signer_test(action, &env.sender_kp) +} \ No newline at end of file From 185f6e2d0e4c826518cbd9654110861db789b4b7 Mon Sep 17 00:00:00 2001 From: Thomzin Date: Wed, 13 Aug 2025 14:30:32 +0800 Subject: [PATCH 7/7] fra DEADME attached --- derive/README.md | 138 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 derive/README.md diff --git a/derive/README.md b/derive/README.md new file mode 100644 index 0000000..556c873 --- /dev/null +++ b/derive/README.md @@ -0,0 +1,138 @@ + + +# FRA: 用于 Taproot 的可替代资产脚本库 + +[![测试状态](https://img.shields.io/badge/tests-13/13%20passed-brightgreen)](./tests/fra_actions.rs) +[![许可证](https://img.shields.io/badge/license-Apache--2.0-blue)](./LICENSE) + +`fra` 是一个现代、简约且符合标准的 Rust 模块,专为在比特币 Taproot 上构建可替代资产(Fungible Asset)协议而设计。它提供了一套用于生成特定资产操作脚本的核心功能,完全兼容 `bp-core` 和 `rust-bitcoin` 生态。 + +本项目旨在提供一个纯粹的脚本生成引擎,它不处理私钥,确保了冷钱包环境下的安全性。 + +## 核心特性 + +- **全面的资产操作**: 支持发行、转账、销毁、拆分、合并等多种原子化操作。 +- **Taproot 原生**: 所有脚本都为 Taproot 结构设计,充分利用其效率和隐私优势。 +- **标准兼容**: 遵循 BIP-340/341/342 规范,生成的脚本逻辑清晰、健壮。 +- **无私钥依赖**: 库本身不涉及任何私钥管理和签名操作,仅负责生成脚本,适用于离线环境。 +- **技术栈灵活**: 可与 `rust-bitcoin` 或 `bp-core` 无缝集成,满足不同项目的需求。 +- **经过验证**: 附带了覆盖所有操作的完整集成测试套件,在 Regtest 环境下验证通过。 + +## 核心概念 + +### `FraAction` 枚举 + +这是与库交互的主要入口点。它定义了所有支持的资产操作,每个操作都包含了生成相应脚本所需的所有参数。 + +```rust +pub enum FraAction { + // 双重签名转账 + Transfer { asset_id: [u8; 32], amount: u64, receiver: XOnlyPk, sender: XOnlyPk }, + // 单签增发 + Mint { asset_id: [u8; 32], amount: u64, receiver: OutputPk, minter: XOnlyPk }, + // 单签销毁 + Burn { asset_id: [u8; 32], amount: u64, owner: XOnlyPk }, + // UTXO 拆分 + Split { asset_id: [u8; 32], orig_amount: u64, outputs: Vec<(u64, OutputPk)>, owner: XOnlyPk }, + // UTXO 合并 + Merge { asset_id: [u8; 32], inputs: Vec, recipient: OutputPk, owner: XOnlyPk }, + // ... 以及其他管理类操作,如冻结、授权等 +} +```` + +### `build_fra_script` 函数 + +这是库的核心功能函数。它接收一个 `FraAction` 实例,并返回一个 `bc::TapScript`,其中包含了可在 Taproot 叶子节点中使用的比特币脚本字节码。 + +```rust +pub fn build_fra_script(action: FraAction) -> TapScript; +``` + +### 数据承诺与堆栈清理 + +本库生成的脚本广泛采用“数据承诺”模式,即将关键数据(如 `asset_id`、`amount`、`receiver` 等)直接编码进脚本中。这确保了这些数据被包含在签名哈希(Sighash)内,无法被篡改。 + +同时,所有脚本都经过精心设计,以确保在成功执行后**正确清理堆栈**,只留下一个代表 `TRUE` 的值,从而满足比特币脚本的有效性规则。 + +## 快速上手 + +以下是一个构建 `Mint` 操作脚本并创建 Taproot 地址的简要流程: + +```rust +use bc::{TapScript, TapCode, XOnlyPk, OutputPk, InternalPk}; +use derive::fra::{FraAction, build_fra_script}; +use derive::taptree::{TapTree, LeafInfo}; +use amplify::num::u7; + +// 1. 定义一个资产操作 +let minter_pk = XOnlyPk::from_byte_array([2; 32]).unwrap(); +let receiver_pk = OutputPk::from_unchecked(XOnlyPk::from_byte_array([3; 32]).unwrap()); + +let action = FraAction::Mint { + asset_id: [1; 32], + amount: 1000, + receiver: receiver_pk, + minter: minter_pk, +}; + +// 2. 生成对应的 TapScript +let tap_script = build_fra_script(action); +println!("生成的脚本 (Hex): {}", tap_script.as_inner().to_hex()); + +// 3. 将脚本放入 TapTree 中 +// 在真实的 Taproot 应用中,你可以组合多个脚本 +let internal_key = InternalPk::from_byte_array([4; 32]).unwrap(); +let leaf_info = LeafInfo::tap_script(u7::ZERO, tap_script); +let tap_tree = TapTree::from_leaves(vec![leaf_info]).unwrap(); + +// 4. 计算 Merkle Root 并生成地址 +// (此步骤通常使用 rust-bitcoin 或类似库完成) +let merkle_root = tap_tree.merkle_root(); +let (output_key, _) = internal_key.to_output_pk(Some(merkle_root)); +let address = bc::Address::p2tr_tweaked(output_key, bc::AddressNetwork::Regtest); + +println!("生成的 Taproot 地址: {}", address); +``` + +## 详细用法示例 + +我们提供了两个完整的、可在 Regtest 环境下运行的示例,展示了如何将 `fra.rs` 集成到你的项目中。 + + - **[示例 1: 结合 `rust-bitcoin` 使用]** + 这是最常见的使用方式,利用 `rust-bitcoin` 库构建、签名和广播交易,与 `bitcoincore-rpc` 完美兼容。 + + - **[示例 2: 结合 `bp-core` 使用]** + 如果你整个项目都构建在 `bp-core` 生态之上,此示例展示了如何使用 `bc::Tx` 等类型来完成同样的工作,保持技术栈的一致性。 + +## API 参考 + +### `fra.rs` + + - `pub enum FraAction`: 定义了所有支持的资产操作。 + - `pub fn build_fra_script(action: FraAction) -> TapScript`: 根据 `FraAction` 构建比特币脚本。 + - `pub fn fra_leaf_info(action: FraAction, depth: u7) -> LeafInfo`: 将脚本打包成 `TapTree` 所需的叶子节点信息。 + - `pub fn build_fra_control_blocks(...) -> Vec<(ControlBlock, LeafScript)>`: 批量为一系列 `FraAction` 构建 Taproot 证明。 + +## 如何测试 + +本模块包含一套完整的集成测试,覆盖了 `FraAction` 的所有变体。 + +**前提**: + +1. 确保本地运行一个 Bitcoin Core 节点,并开启 Regtest 模式。 +2. 确保 `bitcoin.conf` 中配置了 RPC 用户名和密码。 +3. 创建一个名为 `legacy_true` 的钱包 (`bitcoin-cli createwallet legacy_true`)。 + +运行以下命令来执行测试: + +```bash +cargo test --test fra_actions -- --nocapture --test-threads=1 +``` + + - `--nocapture`: 实时显示 `println!` 输出,便于调试。 + - `--test-threads=1`: **必须设置**。由于测试需要与同一个 `bitcoind` 实例交互,此参数可防止因并发 RPC 请求导致的竞态条件。 + +## 许可证 + +本项目采用 [Apache 2.0](https://www.google.com/search?q=./LICENSE) 许可证。 +