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..03fa3ec 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -24,7 +24,12 @@ 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.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/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) 许可证。 + diff --git a/derive/examples/fra_demo.rs b/derive/examples/fra_demo.rs new file mode 100644 index 0000000..e8c6795 --- /dev/null +++ b/derive/examples/fra_demo.rs @@ -0,0 +1,190 @@ +// fra_demo5_final_solution_v3.rs + +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::{Auth, Client, RpcApi}; +use bitcoincore_rpc::json::AddressType; + +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, + )?; + + 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: 地址生成 + // =================================================================== + 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(), + }; + + 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, + }; + + // =================================================================== + // 步骤 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_demo_bpcore.rs b/derive/examples/fra_demo_bpcore.rs new file mode 100644 index 0000000..8e6dd8b --- /dev/null +++ b/derive/examples/fra_demo_bpcore.rs @@ -0,0 +1,212 @@ +// fra_demo4_method_a_aligned.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; +use amplify::hex::ToHex; + +// 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 { + 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()), + )?; + // 导入两个演示用私钥 + 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()); + let internal_xonly_key: XOnlyPublicKey = internal_kp.public_key().x_only_public_key().0; + + // 转为 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 密钥对 + 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) + // OP_CHECKSIGVERIFY OP_CHECKSIG + 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),并转换为 rust-bitcoin TapNodeHash + let leaf_hash = leaf_script.tap_leaf_hash(); + println!("Leaf Hash: {:?}", leaf_hash.to_byte_array().to_hex()); + 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) 生成带脚本路径的 P2TR 地址(rust-bitcoin 计算 tweak) + 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, + )?; + 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_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) 双方 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_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_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_demo_rustbitcoin.rs b/derive/examples/fra_demo_rustbitcoin.rs new file mode 100644 index 0000000..bba49a2 --- /dev/null +++ b/derive/examples/fra_demo_rustbitcoin.rs @@ -0,0 +1,189 @@ +// fra_demo5_final_solution_v3.rs + +use std::str::FromStr; + +use bitcoin::{ + self, + consensus::encode, + 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::{Auth, Client, RpcApi}; +use bitcoincore_rpc::json::AddressType; + +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, + )?; + 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: 地址生成 (调用 build_fra_script) + // =================================================================== + 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(), + }; + 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()); + + 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-11: 交易注资和花费交易骨架构建 + // =================================================================== + 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, + }; + + 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 + // =================================================================== + 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()); + + // =================================================================== + // 步骤 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 顺序必须与脚本消耗顺序相反 --- + // 脚本: 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; + + // =================================================================== + // 步骤 14: 广播交易 + // =================================================================== + let tx_hex = encode::serialize_hex(&spend_tx); + println!("Final TX Hex: {}", tx_hex); + + let final_txid = rpc.send_raw_transaction(&*tx_hex)?; + println!("\n🎉🎉🎉 交易成功广播! TXID = {} 🎉🎉🎉", final_txid); + + 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..9fdfd62 --- /dev/null +++ b/derive/examples/simple_test.rs @@ -0,0 +1,92 @@ + +use bitcoincore_rpc::{Auth, Client, RpcApi}; +use bitcoincore_rpc::bitcoin::{ + 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 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..a957956 --- /dev/null +++ b/derive/src/fra.rs @@ -0,0 +1,312 @@ + +use amplify::num::u7; +use bc::{ + TapScript, TapCode, ControlBlock, LeafScript, XOnlyPk, + InternalPk, OutputPk +}; +use crate::taptree::{TapTree, LeafInfo, ControlBlockFactory}; + +// --- Helper 函数 --- + +/// 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 { + buf.push(len as u8); + } 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) { + 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 操作类型 +#[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, + }, + /// 拆分:将一笔 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, + }, + /// 解冻资产 + Unfreeze { + asset_id: [u8; 32], + authority: XOnlyPk, + }, + /// 授予角色 + GrantRole { + asset_id: [u8; 32], + role: Vec, + target: XOnlyPk, + admin: XOnlyPk, + }, + /// 撤销角色 + RevokeRole { + asset_id: [u8; 32], + role: Vec, + target: XOnlyPk, + admin: XOnlyPk, + }, + /// 升级脚本版本 + Upgrade { + asset_id: [u8; 32], + new_version: [u8; 32], + authority: XOnlyPk, + }, + /// 更新元数据 + MetadataUpdate { + asset_id: [u8; 32], + metadata: Vec, + authority: XOnlyPk, + }, +} + +/// 构造对应操作的 TapScript +pub fn build_fra_script(action: FraAction) -> TapScript { + let mut buf = Vec::new(); + match action { + // --- Transfer + FraAction::Transfer { receiver, sender, .. } => { + push_bytes(&mut buf, &sender.to_byte_array()); + buf.push(TapCode::CheckSigVerify as u8); + push_bytes(&mut buf, &receiver.to_byte_array()); + buf.push(TapCode::CheckSig as u8); + } + + // --- 单签通用模式: 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::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::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::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::CheckSig as u8); + } + FraAction::Split { asset_id, orig_amount, outputs, owner } => { + // Commit data + push_bytes(&mut buf, &asset_id); + 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::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); + } + for _ in 0..inputs.len().saturating_sub(1) { + buf.push(TapCode::Add as u8); + } + push_int(&mut buf, merged_amt); + buf.push(TapCode::EqualVerify as u8); + // 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::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::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::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::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::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::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::CheckSig as u8); + } + } + TapScript::from_checked(buf) +} + +/// 将 TapScript 包装成叶子信息 +pub fn fra_leaf_info(action: FraAction, depth: u7) -> LeafInfo { + let ts = build_fra_script(action); + LeafInfo::tap_script(depth, ts) +} + +/// 将一系列 FRA 动作打包成 Taproot 解锁证明 +pub fn build_fra_control_blocks( + internal_pk: InternalPk, + actions: Vec<(FraAction, u7)>, +) -> Vec<(ControlBlock, LeafScript)> { + let leaf_infos = actions + .into_iter() + .map(|(action, depth)| fra_leaf_info(action, depth)) + .collect::>(); + + let tap_tree = TapTree::from_leaves(leaf_infos) + .expect("FRA script tree build failed"); + + ControlBlockFactory::with(internal_pk, tap_tree) + .collect() +} \ No newline at end of file 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 d5c7880..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, + ControlBlock, InternalPk, LeafScript, OutputPk, Parity, TapBranchHash, TapLeafHash, + TapMerklePath, TapNodeHash, TapScript, }; use commit_verify::merkle::MerkleBuoy; @@ -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 { @@ -140,11 +146,77 @@ impl TapTree { } 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 = 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)); + } + } + + 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 { + // [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)); + + 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(hr); + } + if target_r { + path_r.push(hl); + } + + 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(); + // 现在 path 是 Vec,可以成功创建 TapMerklePath + TapMerklePath::try_from(path).expect("path length within [0..128]") } } @@ -167,7 +239,7 @@ impl TapTree { TapTree( self.into_iter() .map(|leaf| LeafInfo { - depth: leaf.depth, + depth: leaf.depth, script: f(leaf.script), }) .collect(), @@ -178,8 +250,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 +266,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 +381,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 +450,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/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) } 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