From 54371f52eff9adea1c91b11fb33ffba41af21efc Mon Sep 17 00:00:00 2001 From: Kobby Date: Wed, 3 Apr 2024 08:11:16 +0000 Subject: [PATCH] Fix/invalid signature error in external signing flow (#15) --- Cargo.toml | 1 + examples/deploy.rs | 6 +- examples/mint.rs | 6 +- examples/transfer.rs | 6 +- examples/utils/fee.rs | 1 + src/error.rs | 2 + src/lib.rs | 1 + src/utils/constants.rs | 2 + src/utils/fees.rs | 257 +++++++++++++++++++++++++++++++++++ src/utils/mod.rs | 1 + src/wallet.rs | 4 +- src/wallet/builder.rs | 235 ++++++++++++++++++++++++++------ src/wallet/builder/signer.rs | 64 ++++----- 13 files changed, 498 insertions(+), 88 deletions(-) create mode 100644 src/utils/fees.rs diff --git a/Cargo.toml b/Cargo.toml index 884ad8c..c0155b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ edition = "2021" async-trait = "0.1" bitcoin = { version = "0.31", features = ["rand"] } ciborium = "0.2" +hex = "0.4" http = "1" log = "0.4" rand = { version = "0.8" } diff --git a/examples/deploy.rs b/examples/deploy.rs index cff42f5..490821a 100644 --- a/examples/deploy.rs +++ b/examples/deploy.rs @@ -4,7 +4,7 @@ use argh::FromArgs; use bitcoin::secp256k1::Secp256k1; use bitcoin::{Address, Network, PrivateKey}; use log::{debug, info}; -use ord_rs::wallet::{CreateCommitTransactionArgs, RevealTransactionArgs}; +use ord_rs::wallet::{CreateCommitTransactionArgsV2, RevealTransactionArgs}; use ord_rs::{Brc20, OrdTransactionBuilder}; use utils::rpc_client; @@ -84,9 +84,9 @@ async fn main() -> anyhow::Result<()> { }; let commit_tx = builder - .build_commit_transaction( + .build_commit_transaction_with_fixed_fees( network, - CreateCommitTransactionArgs { + CreateCommitTransactionArgsV2 { inputs, inscription: Brc20::deploy(ticker, amount, Some(limit), None), txin_script_pubkey: sender_address.script_pubkey(), diff --git a/examples/mint.rs b/examples/mint.rs index 6c9244d..8550509 100644 --- a/examples/mint.rs +++ b/examples/mint.rs @@ -4,7 +4,7 @@ use argh::FromArgs; use bitcoin::secp256k1::Secp256k1; use bitcoin::{Address, Network, PrivateKey}; use log::{debug, info}; -use ord_rs::wallet::{CreateCommitTransactionArgs, RevealTransactionArgs}; +use ord_rs::wallet::{CreateCommitTransactionArgsV2, RevealTransactionArgs}; use ord_rs::{Brc20, OrdTransactionBuilder}; use self::utils::rpc_client; @@ -79,9 +79,9 @@ async fn main() -> anyhow::Result<()> { }; let commit_tx = builder - .build_commit_transaction( + .build_commit_transaction_with_fixed_fees( network, - CreateCommitTransactionArgs { + CreateCommitTransactionArgsV2 { inputs, inscription: Brc20::mint(ticker, amount), txin_script_pubkey: sender_address.script_pubkey(), diff --git a/examples/transfer.rs b/examples/transfer.rs index 50d40eb..645a441 100644 --- a/examples/transfer.rs +++ b/examples/transfer.rs @@ -4,7 +4,7 @@ use argh::FromArgs; use bitcoin::secp256k1::Secp256k1; use bitcoin::{Address, Network, PrivateKey}; use log::{debug, info}; -use ord_rs::wallet::{CreateCommitTransactionArgs, RevealTransactionArgs}; +use ord_rs::wallet::{CreateCommitTransactionArgsV2, RevealTransactionArgs}; use ord_rs::{Brc20, OrdTransactionBuilder}; use self::utils::rpc_client; @@ -78,9 +78,9 @@ async fn main() -> anyhow::Result<()> { }; let commit_tx = builder - .build_commit_transaction( + .build_commit_transaction_with_fixed_fees( network, - CreateCommitTransactionArgs { + CreateCommitTransactionArgsV2 { inputs, inscription: Brc20::transfer(ticker, amount), txin_script_pubkey: sender_address.script_pubkey(), diff --git a/examples/utils/fee.rs b/examples/utils/fee.rs index 1c8d9c1..9ef968f 100644 --- a/examples/utils/fee.rs +++ b/examples/utils/fee.rs @@ -1,5 +1,6 @@ use bitcoin::{Amount, Network}; +#[allow(dead_code)] pub struct Fees { pub commit_fee: Amount, pub reveal_fee: Amount, diff --git a/src/error.rs b/src/error.rs index 902918d..c4b537d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -3,6 +3,8 @@ use thiserror::Error; /// Ordinal transaction handling error types #[derive(Error, Debug)] pub enum OrdError { + #[error("Hex codec error: {0}")] + HexCodec(#[from] hex::FromHexError), #[error("Ord codec error: {0}")] Codec(#[from] serde_json::Error), #[error("Bitcoin sighash error: {0}")] diff --git a/src/lib.rs b/src/lib.rs index 010dead..ff2e8a1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,6 +31,7 @@ pub use inscription::brc20::Brc20; pub use inscription::nft::Nft; pub use inscription::Inscription; pub use result::OrdResult; +pub use utils::fees::{self, MultisigConfig}; pub use wallet::{ CreateCommitTransaction, CreateCommitTransactionArgs, ExternalSigner, OrdParser, OrdTransactionBuilder, RevealTransactionArgs, Utxo, Wallet, WalletType, diff --git a/src/utils/constants.rs b/src/utils/constants.rs index cf233e1..da6cf58 100644 --- a/src/utils/constants.rs +++ b/src/utils/constants.rs @@ -16,3 +16,5 @@ pub const CONTENT_ENCODING_TAG: [u8; 1] = [9]; /// Tag 11, representing a nominated inscription. #[allow(unused)] pub const DELEGATE_TAG: [u8; 1] = [11]; + +pub const POSTAGE: u64 = 333; diff --git a/src/utils/fees.rs b/src/utils/fees.rs new file mode 100644 index 0000000..9d2870f --- /dev/null +++ b/src/utils/fees.rs @@ -0,0 +1,257 @@ +use bitcoin::absolute::LockTime; +use bitcoin::transaction::Version; +use bitcoin::{ + Address, Amount, FeeRate, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Witness, +}; +use serde::{Deserialize, Serialize}; + +use super::constants::POSTAGE; +use crate::wallet::ScriptType; + +/// Single ECDSA signature + SIGHASH type size in bytes. +const ECDSA_SIGHASH_SIZE: usize = 72 + 1; +/// Single Schnorr signature + SIGHASH type size for Taproot in bytes. +const SCHNORR_SIGHASH_SIZE: usize = 64 + 1; + +/// Represents multisig configuration (m of n) for a transaction, if applicable. +/// Encapsulates the number of required signatures and the total number of signatories. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct MultisigConfig { + /// Number of required signatures (m) + pub required: usize, + /// Total number of signatories (n) + pub total: usize, +} + +pub fn estimate_commit_fee( + unsigned_commit_tx: Transaction, + script_type: ScriptType, + current_fee_rate: FeeRate, + multisig_config: &Option, +) -> Amount { + estimate_transaction_fees( + script_type, + unsigned_commit_tx.vsize(), + current_fee_rate, + multisig_config, + ) +} + +pub fn estimate_reveal_fee( + inputs: Vec, + recipient_address: Address, + redeem_script: ScriptBuf, + script_type: ScriptType, + current_fee_rate: FeeRate, + multisig_config: &Option, +) -> Amount { + let tx_out = vec![TxOut { + value: Amount::from_sat(POSTAGE), + script_pubkey: recipient_address.script_pubkey(), + }]; + + let mut tx_in: Vec = inputs + .iter() + .map(|outpoint| TxIn { + previous_output: *outpoint, + script_sig: ScriptBuf::new(), + sequence: Sequence::from_consensus(0xffffffff), + witness: Witness::new(), + }) + .collect(); + + tx_in[0].witness.push(redeem_script.into_bytes()); + + let unsigned_reveal_tx = Transaction { + version: Version::TWO, + lock_time: LockTime::ZERO, + input: tx_in, + output: tx_out, + }; + + estimate_transaction_fees( + script_type, + unsigned_reveal_tx.vsize(), + current_fee_rate, + multisig_config, + ) +} + +fn estimate_transaction_fees( + script_type: ScriptType, + unsigned_tx_size: usize, + current_fee_rate: FeeRate, + multisig_config: &Option, +) -> Amount { + let estimated_sig_size = estimate_signature_size(script_type, multisig_config); + let total_estimated_tx_size = unsigned_tx_size + estimated_sig_size; + + current_fee_rate + .fee_vb(total_estimated_tx_size as u64) + .unwrap() +} + +/// Estimates the total size of signatures for a given script type and multisig configuration. +fn estimate_signature_size( + script_type: ScriptType, + multisig_config: &Option, +) -> usize { + match script_type { + // For P2WSH, calculate based on the multisig configuration if provided. + ScriptType::P2WSH => match multisig_config { + Some(config) => ECDSA_SIGHASH_SIZE * config.required, + None => ECDSA_SIGHASH_SIZE, // Default to single signature size if no multisig config is provided. + }, + // For P2TR, use the fixed Schnorr signature size. + ScriptType::P2TR => SCHNORR_SIGHASH_SIZE, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn estimate_transaction_fees_p2wsh_single_signature() { + let script_type = ScriptType::P2WSH; + let unsigned_tx_size = 100; // in vbytes + let current_fee_rate = FeeRate::from_sat_per_vb(5_u64).unwrap(); + let multisig_config: Option = None; // No multisig config for single signature + + let fee = estimate_transaction_fees( + script_type, + unsigned_tx_size, + current_fee_rate, + &multisig_config, + ); + + // Expected fee calculation: (100 + 73) * 5 = 865 satoshis + assert_eq!(fee, Amount::from_sat(865)); + } + + #[test] + fn estimate_transaction_fees_p2wsh_multisig() { + let script_type = ScriptType::P2WSH; + let unsigned_tx_size = 200; // in vbytes + let current_fee_rate = FeeRate::from_sat_per_vb(10_u64).unwrap(); + let multisig_config = Some(MultisigConfig { + required: 2, + total: 3, + }); // 2-of-3 multisig + + let fee = estimate_transaction_fees( + script_type, + unsigned_tx_size, + current_fee_rate, + &multisig_config, + ); + + // Expected fee calculation: (200 + (73 * 2)) * 10 = 3460 satoshis + assert_eq!(fee, Amount::from_sat(3460)); + } + + #[test] + fn estimate_transaction_fees_p2tr() { + let script_type = ScriptType::P2TR; + let unsigned_tx_size = 150; // in vbytes + let current_fee_rate = FeeRate::from_sat_per_vb(1_u64).unwrap(); + let multisig_config = None; + + let fee = estimate_transaction_fees( + script_type, + unsigned_tx_size, + current_fee_rate, + &multisig_config, + ); + + // Expected fee calculation: (150 + 65) * 1 = 215 satoshis + assert_eq!(fee, Amount::from_sat(215)); + } + + #[test] + #[should_panic] + fn estimate_transaction_fees_overflow() { + let script_type = ScriptType::P2TR; + let unsigned_tx_size = usize::MAX; + let current_fee_rate = FeeRate::from_sat_per_vb(1_u64).unwrap(); + let multisig_config = None; + + let _fee = estimate_transaction_fees( + script_type, + unsigned_tx_size, + current_fee_rate, + &multisig_config, + ); + } + + #[test] + fn estimate_transaction_fees_low_fee_rate() { + let script_type = ScriptType::P2WSH; + let unsigned_tx_size = 250; // in vbytes + let current_fee_rate = FeeRate::from_sat_per_vb(1_u64).unwrap(); // Low fee rate + let multisig_config = Some(MultisigConfig { + required: 3, + total: 5, + }); // 3-of-5 multisig + + let fee = estimate_transaction_fees( + script_type, + unsigned_tx_size, + current_fee_rate, + &multisig_config, + ); + + // Expected fee calculation: (250 + (73 * 3)) * 1 = 469 satoshis + assert_eq!(fee, Amount::from_sat(469)); + } + + #[test] + fn estimate_transaction_fees_high_fee_rate() { + let script_type = ScriptType::P2TR; + let unsigned_tx_size = 180; // in vbytes + let current_fee_rate = FeeRate::from_sat_per_vb(50_u64).unwrap(); // High fee rate + let multisig_config = None; + + let fee = estimate_transaction_fees( + script_type, + unsigned_tx_size, + current_fee_rate, + &multisig_config, + ); + + // Expected fee calculation: (180 + 65) * 50 = 12250 satoshis + assert_eq!(fee, Amount::from_sat(12250)); + } + + #[test] + fn estimate_transaction_fees_varying_fee_rate() { + let script_type = ScriptType::P2WSH; + let unsigned_tx_size = 300; // in vbytes + // Simulating a fee rate that might be seen during network congestion + let fee_rates: Vec = vec![5, 10, 20, 30, 40]; + + for fee_rate in fee_rates { + let current_fee_rate = FeeRate::from_sat_per_vb(fee_rate).unwrap(); + let multisig_config = Some(MultisigConfig { + required: 2, + total: 3, + }); // 2-of-3 multisig + + let fee = estimate_transaction_fees( + script_type, + unsigned_tx_size, + current_fee_rate, + &multisig_config, + ); + + // Expected fee calculation changes with the fee_rate + let expected_fee = (300 + (73 * 2)) as u64 * fee_rate; + assert_eq!( + fee, + Amount::from_sat(expected_fee), + "Fee mismatch at rate: {}", + fee_rate + ); + } + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 7d8fe74..0e564c1 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,4 +1,5 @@ pub mod constants; +pub mod fees; mod push_bytes; #[cfg(test)] pub mod test_utils; diff --git a/src/wallet.rs b/src/wallet.rs index e311e45..d128946 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -3,7 +3,7 @@ mod parser; pub use builder::signer::{ExternalSigner, Wallet, WalletType}; pub use builder::{ - CreateCommitTransaction, CreateCommitTransactionArgs, OrdTransactionBuilder, - RedeemScriptPubkey, RevealTransactionArgs, ScriptType, Utxo, + CreateCommitTransaction, CreateCommitTransactionArgs, CreateCommitTransactionArgsV2, + OrdTransactionBuilder, RedeemScriptPubkey, RevealTransactionArgs, ScriptType, Utxo, }; pub use parser::OrdParser; diff --git a/src/wallet/builder.rs b/src/wallet/builder.rs index 6b37aa8..0729487 100644 --- a/src/wallet/builder.rs +++ b/src/wallet/builder.rs @@ -5,17 +5,17 @@ use bitcoin::absolute::LockTime; use bitcoin::script::Builder as ScriptBuilder; use bitcoin::transaction::Version; use bitcoin::{ - secp256k1, Address, Amount, Network, OutPoint, PublicKey, ScriptBuf, Sequence, Transaction, - TxIn, TxOut, Txid, Witness, XOnlyPublicKey, + secp256k1, Address, Amount, FeeRate, Network, OutPoint, PublicKey, ScriptBuf, Sequence, + Transaction, TxIn, TxOut, Txid, Witness, XOnlyPublicKey, }; use signer::Wallet; use super::builder::taproot::{generate_keypair, TaprootPayload}; use crate::inscription::Inscription; +use crate::utils::constants::POSTAGE; +use crate::utils::fees::{estimate_commit_fee, estimate_reveal_fee, MultisigConfig}; use crate::{OrdError, OrdResult}; -const POSTAGE: u64 = 333; - /// Ordinal-aware transaction builder for arbitrary (`Nft`) /// and `Brc20` inscriptions. pub struct OrdTransactionBuilder { @@ -32,18 +32,18 @@ pub struct CreateCommitTransactionArgs where T: Inscription, { - /// UTXOs to be used as Inputs of the transaction + /// UTXOs to be used as inputs of the transaction pub inputs: Vec, /// Inscription to write pub inscription: T, /// Address to send the leftovers BTC of the trasnsaction pub leftovers_recipient: Address, - /// Fee to pay for the commit transaction - pub commit_fee: Amount, - /// Fee to pay for the reveal transaction - pub reveal_fee: Amount, /// Script pubkey of the inputs pub txin_script_pubkey: ScriptBuf, + /// Current fee rate on the network + pub fee_rate: FeeRate, + /// Multisig configuration, if applicable + pub multisig_config: Option, } #[derive(Debug, Clone)] @@ -94,6 +94,7 @@ impl OrdTransactionBuilder { pub async fn build_commit_transaction( &mut self, network: Network, + recipient_address: Address, args: CreateCommitTransactionArgs, ) -> OrdResult where @@ -113,25 +114,21 @@ impl OrdTransactionBuilder { ScriptType::P2TR => RedeemScriptPubkey::XPublickey(p2tr_keys.unwrap().1), }; - // calc balance - // exceeding amount of transaction to send to leftovers recipient - let leftover_amount = args - .inputs - .iter() - .map(|input| input.amount.to_sat()) - .sum::() - .checked_sub(POSTAGE) - .and_then(|v| v.checked_sub(args.commit_fee.to_sat())) - .and_then(|v| v.checked_sub(args.reveal_fee.to_sat())) - .ok_or(OrdError::InsufficientBalance)?; - debug!("leftover_amount: {leftover_amount}"); + let redeem_script = self.generate_redeem_script(&args.inscription, redeem_script_pubkey)?; + debug!("redeem_script: {redeem_script}"); - let reveal_balance = POSTAGE + args.reveal_fee.to_sat(); + let reveal_fee = estimate_reveal_fee( + vec![OutPoint::null()], + recipient_address, + redeem_script.clone(), + self.script_type, + args.fee_rate, + &args.multisig_config, + ); + + let reveal_balance = POSTAGE + reveal_fee.to_sat(); debug!("reveal_balance: {reveal_balance}"); - // get p2wsh or p2tr address for output of inscription - let redeem_script = self.generate_redeem_script(&args.inscription, redeem_script_pubkey)?; - debug!("redeem_script: {redeem_script}"); let script_output_address = match self.script_type { ScriptType::P2WSH => Address::p2wsh(&redeem_script, network), ScriptType::P2TR => { @@ -151,7 +148,9 @@ impl OrdTransactionBuilder { }; debug!("script_output_address: {script_output_address}"); - let tx_out = vec![ + let mut leftover_amount = 0; + + let mut tx_out = vec![ TxOut { value: Amount::from_sat(reveal_balance), script_pubkey: script_output_address.script_pubkey(), @@ -162,8 +161,7 @@ impl OrdTransactionBuilder { }, ]; - // txin - let tx_in = args + let tx_in: Vec = args .inputs .iter() .map(|input| TxIn { @@ -177,6 +175,33 @@ impl OrdTransactionBuilder { }) .collect(); + let commit_fee = estimate_commit_fee( + Transaction { + version: Version::TWO, + lock_time: LockTime::ZERO, + input: tx_in.clone(), + output: tx_out.clone(), + }, + self.script_type, + args.fee_rate, + &args.multisig_config, + ); + + // calc balance + // exceeding amount of transaction to send to leftovers recipient + leftover_amount = args + .inputs + .iter() + .map(|input| input.amount.to_sat()) + .sum::() + .checked_sub(POSTAGE) + .and_then(|v| v.checked_sub(commit_fee.to_sat())) + .and_then(|v| v.checked_sub(reveal_fee.to_sat())) + .ok_or(OrdError::InsufficientBalance)?; + debug!("leftover_amount: {leftover_amount}"); + + tx_out[1].value = Amount::from_sat(leftover_amount); + // make transaction and sign it let unsigned_tx = Transaction { version: Version::TWO, @@ -274,7 +299,7 @@ impl OrdTransactionBuilder { use signer::WalletType; let public_key = private_key.public_key(&secp256k1::Secp256k1::new()); - let wallet = Wallet::new_with_signer(None, None, WalletType::Local { private_key }); + let wallet = Wallet::new_with_signer(WalletType::Local { private_key }); Self::new(public_key, ScriptType::P2TR, wallet) } @@ -284,9 +309,142 @@ impl OrdTransactionBuilder { use signer::WalletType; let public_key = private_key.public_key(&secp256k1::Secp256k1::new()); - let wallet = Wallet::new_with_signer(None, None, WalletType::Local { private_key }); + let wallet = Wallet::new_with_signer(WalletType::Local { private_key }); Self::new(public_key, ScriptType::P2WSH, wallet) } + + /// Creates the commit transaction with predetermined commit and reveal fees. + pub async fn build_commit_transaction_with_fixed_fees( + &mut self, + network: Network, + args: CreateCommitTransactionArgsV2, + ) -> OrdResult + where + T: Inscription, + { + let secp_ctx = secp256k1::Secp256k1::new(); + + // generate P2TR keyts + let p2tr_keys = match self.script_type { + ScriptType::P2WSH => None, + ScriptType::P2TR => Some(generate_keypair(&secp_ctx)), + }; + + // generate redeem script pubkey based on the current script type + let redeem_script_pubkey = match self.script_type { + ScriptType::P2WSH => RedeemScriptPubkey::Ecdsa(self.public_key), + ScriptType::P2TR => RedeemScriptPubkey::XPublickey(p2tr_keys.unwrap().1), + }; + + // calc balance + // exceeding amount of transaction to send to leftovers recipient + let leftover_amount = args + .inputs + .iter() + .map(|input| input.amount.to_sat()) + .sum::() + .checked_sub(POSTAGE) + .and_then(|v| v.checked_sub(args.commit_fee.to_sat())) + .and_then(|v| v.checked_sub(args.reveal_fee.to_sat())) + .ok_or(OrdError::InsufficientBalance)?; + debug!("leftover_amount: {leftover_amount}"); + + let reveal_balance = POSTAGE + args.reveal_fee.to_sat(); + debug!("reveal_balance: {reveal_balance}"); + + // get p2wsh or p2tr address for output of inscription + let redeem_script = self.generate_redeem_script(&args.inscription, redeem_script_pubkey)?; + debug!("redeem_script: {redeem_script}"); + let script_output_address = match self.script_type { + ScriptType::P2WSH => Address::p2wsh(&redeem_script, network), + ScriptType::P2TR => { + let taproot_payload = TaprootPayload::build( + &secp_ctx, + p2tr_keys.unwrap().0, + p2tr_keys.unwrap().1, + &redeem_script, + reveal_balance, + network, + )?; + + let address = taproot_payload.address.clone(); + self.taproot_payload = Some(taproot_payload); + address + } + }; + debug!("script_output_address: {script_output_address}"); + + let tx_out = vec![ + TxOut { + value: Amount::from_sat(reveal_balance), + script_pubkey: script_output_address.script_pubkey(), + }, + TxOut { + value: Amount::from_sat(leftover_amount), + script_pubkey: args.txin_script_pubkey.clone(), + }, + ]; + + // txin + let tx_in = args + .inputs + .iter() + .map(|input| TxIn { + previous_output: OutPoint { + txid: input.id, + vout: input.index, + }, + script_sig: ScriptBuf::new(), + sequence: Sequence::from_consensus(0xffffffff), + witness: Witness::new(), + }) + .collect(); + + // make transaction and sign it + let unsigned_tx = Transaction { + version: Version::TWO, + lock_time: LockTime::ZERO, + input: tx_in, + output: tx_out, + }; + + // sign transaction and update witness + let tx = self + .signer + .sign_commit_transaction( + &self.public_key, + &args.inputs, + unsigned_tx, + &args.txin_script_pubkey, + ) + .await?; + + Ok(CreateCommitTransaction { + tx, + redeem_script, + reveal_balance: Amount::from_sat(reveal_balance), + }) + } +} + +#[derive(Debug)] +/// Arguments for creating a commit transaction +pub struct CreateCommitTransactionArgsV2 +where + T: Inscription, +{ + /// UTXOs to be used as inputs of the transaction + pub inputs: Vec, + /// Inscription to write + pub inscription: T, + /// Address to send the leftovers BTC of the trasnsaction + pub leftovers_recipient: Address, + /// Fee to pay for the commit transaction + pub commit_fee: Amount, + /// Fee to pay for the reveal transaction + pub reveal_fee: Amount, + /// Script pubkey of the inputs + pub txin_script_pubkey: ScriptBuf, } /// Unspent transaction output to be used as input of a transaction @@ -299,7 +457,6 @@ pub struct Utxo { #[cfg(test)] mod test { - use std::cell::RefCell; use std::str::FromStr; use bitcoin::secp256k1::Secp256k1; @@ -309,14 +466,6 @@ mod test { use super::*; use crate::Brc20; - thread_local! { - // The derivation path to use for ECDSA secp256k1. - static DERIVATION_PATH: Vec> = vec![]; - - // The ECDSA key name. - static KEY_NAME: RefCell = RefCell::new(String::from("")); - } - // const WIF: &str = "cVkWbHmoCx6jS8AyPNQqvFr8V9r2qzDHJLaxGDQgDJfxT73w6fuU"; @@ -332,7 +481,7 @@ mod test { let mut builder = OrdTransactionBuilder::p2wsh(private_key); - let commit_transaction_args = CreateCommitTransactionArgs { + let commit_transaction_args = CreateCommitTransactionArgsV2 { inputs: vec![Utxo { id: Txid::from_str( "791b415dc6946d864d368a0e5ec5c09ee2ad39cf298bc6e3f9aec293732cfda7", @@ -348,7 +497,7 @@ mod test { reveal_fee: Amount::from_sat(4_700), }; let tx_result = builder - .build_commit_transaction(Network::Testnet, commit_transaction_args) + .build_commit_transaction_with_fixed_fees(Network::Testnet, commit_transaction_args) .await .unwrap(); @@ -436,7 +585,7 @@ mod test { let mut builder = OrdTransactionBuilder::p2tr(private_key); - let commit_transaction_args = CreateCommitTransactionArgs { + let commit_transaction_args = CreateCommitTransactionArgsV2 { inputs: vec![Utxo { id: Txid::from_str( "791b415dc6946d864d368a0e5ec5c09ee2ad39cf298bc6e3f9aec293732cfda7", @@ -452,7 +601,7 @@ mod test { reveal_fee: Amount::from_sat(4_700), }; let tx_result = builder - .build_commit_transaction(Network::Testnet, commit_transaction_args) + .build_commit_transaction_with_fixed_fees(Network::Testnet, commit_transaction_args) .await .unwrap(); diff --git a/src/wallet/builder/signer.rs b/src/wallet/builder/signer.rs index 097f74e..032d582 100644 --- a/src/wallet/builder/signer.rs +++ b/src/wallet/builder/signer.rs @@ -15,12 +15,14 @@ use crate::{OrdError, OrdResult}; /// An abstraction over a transaction signer. #[async_trait::async_trait] pub trait ExternalSigner { - async fn sign( - &self, - key_name: String, - derivation_path: Vec>, - message_hash: Vec, - ) -> Vec; + /// Retrieves the ECDSA public key at the given derivation path. + async fn ecdsa_public_key(&self) -> String; + + /// Signs a message with an ECDSA key and returns the signature. + async fn sign_with_ecdsa(&self, message: &str) -> String; + + /// Verifies an ECDSA signature against a message and a public key. + async fn verify_ecdsa(&self, signature_hex: &str, message: &str, public_key_hex: &str) -> bool; } /// Types of signers. @@ -35,23 +37,15 @@ pub enum WalletType { /// An Ordinal-aware Bitcoin wallet. pub struct Wallet { + pub signer: WalletType, secp: Secp256k1, - pub key_name: Option, - pub derivation_path: Option>>, - pub wallet_type: WalletType, } impl Wallet { - pub fn new_with_signer( - key_name: Option, - derivation_path: Option>>, - wallet_type: WalletType, - ) -> Self { + pub fn new_with_signer(signer: WalletType) -> Self { Self { + signer, secp: Secp256k1::new(), - key_name, - derivation_path, - wallet_type, } } @@ -141,7 +135,7 @@ impl Wallet { ) -> OrdResult { let mut hash = SighashCache::new(transaction.clone()); for (index, input) in utxos.iter().enumerate() { - let signature_hash = match transaction_type { + let sighash = match transaction_type { TransactionType::Commit => hash.p2wpkh_signature_hash( index, script, @@ -156,32 +150,34 @@ impl Wallet { )?, }; - let message = secp256k1::Message::from_digest(signature_hash.to_byte_array()); + let message = secp256k1::Message::from_digest(sighash.to_byte_array()); - let signature = match &self.wallet_type { + debug!("Signing transaction and verifying signature"); + let signature = match &self.signer { WalletType::External { signer } => { + let msg_hex = hex::encode(sighash); + // sign + let sig_hex = signer.sign_with_ecdsa(&msg_hex).await; + let signature = Signature::from_compact(&hex::decode(&sig_hex)?)?; + + // verify signer - .sign( - self.key_name.clone().unwrap(), - self.derivation_path.clone().unwrap(), - signature_hash.as_byte_array().to_vec(), - ) - .await + .verify_ecdsa(&sig_hex, &msg_hex, &own_pubkey.to_string()) + .await; + signature } WalletType::Local { private_key } => { - let sig = self.secp.sign_ecdsa(&message, &private_key.inner); - sig.serialize_der().to_vec() + // sign + let signature = self.secp.sign_ecdsa(&message, &private_key.inner); + // verify + self.secp + .verify_ecdsa(&message, &signature, &own_pubkey.inner)?; + signature } }; - let signature = Signature::from_der(&signature)?; debug!("signature: {}", signature.serialize_der()); - // verify signature - debug!("verifying signature"); - self.secp - .verify_ecdsa(&message, &signature, &own_pubkey.inner)?; - debug!("signature verified"); // append witness let signature = bitcoin::ecdsa::Signature::sighash_all(signature).into(); match transaction_type {