From a7276494ff75d45e7a9d078b95bdce8a589ce188 Mon Sep 17 00:00:00 2001 From: Maxim Date: Fri, 5 Apr 2024 10:49:57 +0300 Subject: [PATCH 1/6] Add rune transaction creation method --- Cargo.toml | 5 ++ src/error.rs | 4 ++ src/lib.rs | 13 ++-- src/utils/fees.rs | 2 +- src/wallet/builder.rs | 38 +++++++++-- src/wallet/builder/rune.rs | 122 ++++++++++++++++++++++++++++++++++ src/wallet/builder/signer.rs | 125 ++++++++++++++++++++++++++--------- 7 files changed, 265 insertions(+), 44 deletions(-) create mode 100644 src/wallet/builder/rune.rs diff --git a/Cargo.toml b/Cargo.toml index c0155b4..dd31e11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,10 @@ repository = "https://github.com/bitfinity-network/ord-rs" documentation = "https://docs.rs/ord-rs" edition = "2021" +[features] +default = [] +rune = ["ordinals"] + [dependencies] async-trait = "0.1" bitcoin = { version = "0.31", features = ["rand"] } @@ -16,6 +20,7 @@ ciborium = "0.2" hex = "0.4" http = "1" log = "0.4" +ordinals = { git = "https://github.com/ordinals/ord", tag = "0.17.1", optional = true } rand = { version = "0.8" } serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/src/error.rs b/src/error.rs index c4b537d..de94e4f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -33,6 +33,10 @@ pub enum OrdError { Utf8Encoding(#[from] std::str::Utf8Error), #[error("Inscription parser error: {0}")] InscriptionParser(#[from] InscriptionParseError), + #[error("Invalid inputs")] + InvalidInputs, + #[error("Invalid script type")] + InvalidScriptType, } /// Inscription parsing errors. diff --git a/src/lib.rs b/src/lib.rs index 84b28fc..3f40515 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,13 +19,8 @@ extern crate log; #[macro_use] extern crate serde; -mod error; -pub mod inscription; -mod result; -mod utils; -pub mod wallet; - pub use bitcoin; + pub use error::{InscriptionParseError, OrdError}; pub use inscription::brc20::Brc20; pub use inscription::nft::Nft; @@ -38,3 +33,9 @@ pub use wallet::{ OrdTransactionBuilder, RevealTransactionArgs, SignCommitTransactionArgs, Utxo, Wallet, WalletType, }; + +mod error; +pub mod inscription; +mod result; +mod utils; +pub mod wallet; diff --git a/src/utils/fees.rs b/src/utils/fees.rs index ce86895..8d1a54f 100644 --- a/src/utils/fees.rs +++ b/src/utils/fees.rs @@ -79,7 +79,7 @@ pub fn estimate_reveal_fee( ) } -fn estimate_transaction_fees( +pub fn estimate_transaction_fees( script_type: ScriptType, unsigned_tx_size: usize, number_of_inputs: usize, diff --git a/src/wallet/builder.rs b/src/wallet/builder.rs index b11d375..e428279 100644 --- a/src/wallet/builder.rs +++ b/src/wallet/builder.rs @@ -1,6 +1,3 @@ -pub mod signer; -mod taproot; - use bitcoin::absolute::LockTime; use bitcoin::script::{Builder as ScriptBuilder, PushBytesBuf}; use bitcoin::transaction::Version; @@ -8,15 +5,23 @@ use bitcoin::{ 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::utils::push_bytes::bytes_to_push_bytes; use crate::{OrdError, OrdResult}; +use super::builder::taproot::{generate_keypair, TaprootPayload}; + +#[cfg(feature = "rune")] +mod rune; + +pub mod signer; +mod taproot; + /// Ordinal-aware transaction builder for arbitrary (`Nft`) /// and `Brc20` inscriptions. pub struct OrdTransactionBuilder { @@ -263,6 +268,17 @@ impl OrdTransactionBuilder { .await } + /// Sign a generic transaction, returning a new signed transaction. + pub async fn sign_transaction( + &self, + unsigned_tx: &Transaction, + inputs: &[TxInputInfo], + ) -> OrdResult { + self.signer + .sign_transaction(unsigned_tx, inputs, &self.public_key) + .await + } + /// Create the reveal transaction pub async fn build_reveal_transaction( &mut self, @@ -482,6 +498,17 @@ pub struct Utxo { pub amount: Amount, } +/// Output of a previous transaction to be used as an input. +/// +/// This struct contains signature script in contrast to [Utxo] so it can be used to sign inputs +/// from different addresses. +pub struct TxInputInfo { + /// ID of the output. + pub outpoint: OutPoint, + /// Contents of the output. + pub tx_out: TxOut, +} + #[cfg(test)] mod test { use std::str::FromStr; @@ -490,9 +517,10 @@ mod test { use bitcoin::PrivateKey; use hex_literal::hex; - use super::*; use crate::Brc20; + use super::*; + // const WIF: &str = "cVkWbHmoCx6jS8AyPNQqvFr8V9r2qzDHJLaxGDQgDJfxT73w6fuU"; diff --git a/src/wallet/builder/rune.rs b/src/wallet/builder/rune.rs new file mode 100644 index 0000000..147dc09 --- /dev/null +++ b/src/wallet/builder/rune.rs @@ -0,0 +1,122 @@ +use bitcoin::absolute::LockTime; +use bitcoin::transaction::Version; +use bitcoin::{Address, Amount, FeeRate, ScriptBuf, Transaction, TxIn, TxOut}; +use ordinals::{Edict, RuneId, Runestone}; + +use crate::fees::estimate_transaction_fees; +use crate::wallet::builder::TxInputInfo; +use crate::wallet::ScriptType; +use crate::{OrdError, OrdTransactionBuilder}; + +/// Postage amount for rune transaction. +/// +/// The value is same as in `ord` tool. +pub const RUNE_POSTAGE: Amount = Amount::from_sat(10_000); + +/// Arguments for the [`OrdTransactionBuilder::create_edict_transaction`] method. +pub struct CreateEdictTxArgs { + /// Identifier of the rune to be transferred. + rune: RuneId, + /// Inputs that contain rune balance to be transferred. + rune_inputs: Vec, + /// Inputs that contain BTC balance to cover outputs and transaction fees. + funding_inputs: Vec, + /// Address of the recipient of the rune transfer. + destination: Address, + /// Address that will receive leftovers of runes and BTC. + change_address: Address, + /// Amount of the rune to be transferred. + amount: u128, + /// Current BTC fee rate. + fee_rate: FeeRate, +} + +impl CreateEdictTxArgs { + fn inputs(&self) -> impl Iterator { + self.rune_inputs.iter().chain(self.funding_inputs.iter()) + } + + fn input_amount(&self) -> Amount { + self.inputs().fold(Amount::ZERO, |a, b| a + b.tx_out.value) + } +} + +impl OrdTransactionBuilder { + /// Creates an unsigned rune edict transaction. + /// + /// This method doesn't check the runes balances, so it's the responsibility of the caller to + /// check that the inputs have enough of the given rune balance to make the transfer. As per + /// runes standard, if the inputs rune balance is less than specified transfer amount, the + /// amount will be reduced to the available balance amount. + /// + /// # Errors + /// * Returns [`OrdError::InsufficientBalance`] if the inputs BTC amount is not enough + /// to cover the outputs and transaction fee. + pub fn create_edict_transaction( + &self, + args: CreateEdictTxArgs, + ) -> Result { + let runestone = Runestone { + edicts: vec![Edict { + id: args.rune, + amount: args.amount, + output: 2, + }], + etching: None, + mint: None, + pointer: None, + }; + + let runestone_out = TxOut { + value: Amount::ZERO, + script_pubkey: ScriptBuf::from_bytes(runestone.encipher().into_bytes()), + }; + let rune_change_out = TxOut { + value: RUNE_POSTAGE, + script_pubkey: args.change_address.script_pubkey(), + }; + let rune_destination_out = TxOut { + value: RUNE_POSTAGE, + script_pubkey: args.destination.script_pubkey(), + }; + let funding_change_out = TxOut { + value: Amount::ZERO, + script_pubkey: args.change_address.script_pubkey(), + }; + + let outputs = vec![ + runestone_out, + rune_change_out, + rune_destination_out, + funding_change_out, + ]; + + let inputs = args + .inputs() + .map(|rune_input| TxIn { + previous_output: rune_input.outpoint, + script_sig: Default::default(), + sequence: Default::default(), + witness: Default::default(), + }) + .collect(); + + let mut unsigned_tx = Transaction { + version: Version::TWO, + lock_time: LockTime::ZERO, + input: inputs, + output: outputs, + }; + + let fee_amount = + estimate_transaction_fees(ScriptType::P2WSH, unsigned_tx.vsize(), args.fee_rate, &None); + let change_amount = args + .input_amount() + .checked_sub(fee_amount + RUNE_POSTAGE * 2) + .ok_or(OrdError::InsufficientBalance)?; + + unsigned_tx.output[3].value = change_amount; + + Ok(unsigned_tx) + } +} diff --git a/src/wallet/builder/signer.rs b/src/wallet/builder/signer.rs index 032d582..3659ab9 100644 --- a/src/wallet/builder/signer.rs +++ b/src/wallet/builder/signer.rs @@ -1,16 +1,19 @@ +use bitcoin::{ + PrivateKey, PublicKey, ScriptBuf, SegwitV0Sighash, TapLeafHash, TapSighashType, Transaction, + Witness, +}; use bitcoin::hashes::Hash as _; use bitcoin::key::Secp256k1; -use bitcoin::secp256k1::ecdsa::Signature; use bitcoin::secp256k1::{self, All}; +use bitcoin::secp256k1::ecdsa::Signature; use bitcoin::sighash::{Prevouts, SighashCache}; use bitcoin::taproot::{ControlBlock, LeafVersion}; -use bitcoin::{ - PrivateKey, PublicKey, ScriptBuf, TapLeafHash, TapSighashType, Transaction, Witness, -}; + +use crate::{OrdError, OrdResult}; +use crate::wallet::builder::TxInputInfo; use super::super::builder::Utxo; use super::taproot::TaprootPayload; -use crate::{OrdError, OrdResult}; /// An abstraction over a transaction signer. #[async_trait::async_trait] @@ -125,6 +128,54 @@ impl Wallet { Ok(sighash_cache.into_transaction()) } + /// Sign a generic transaction. + /// + /// The given transaction must have the same inputs as the ones given in the `prev_outs` argument. + /// The signature is checked against the given `own_pubkey` public key before being accepted + /// as valid and returned. + pub async fn sign_transaction( + &self, + transaction: &Transaction, + prev_outs: &[TxInputInfo], + own_pubkey: &PublicKey, + ) -> OrdResult { + if transaction.input.len() != prev_outs.len() { + return Err(OrdError::InvalidInputs); + } + + let mut cache = SighashCache::new(transaction.clone()); + for (index, input) in prev_outs.iter().enumerate() { + let sighash = match &input.tx_out.script_pubkey { + s if s.is_p2wpkh() => cache.p2wpkh_signature_hash( + index, + s, + input.tx_out.value, + bitcoin::EcdsaSighashType::All, + )?, + s if s.is_p2wsh() => cache.p2wsh_signature_hash( + index, + s, + input.tx_out.value, + bitcoin::EcdsaSighashType::All, + )?, + _ => return Err(OrdError::InvalidScriptType), + }; + let signature = self.sign_msg_hash(sighash, own_pubkey).await?; + let ord_signature = bitcoin::ecdsa::Signature::sighash_all(signature).into(); + + self.append_witness_to_input( + &mut cache, + ord_signature, + index, + &own_pubkey.inner, + None, + None, + )?; + } + + Ok(cache.into_transaction()) + } + async fn sign_ecdsa( &mut self, own_pubkey: &PublicKey, @@ -150,33 +201,7 @@ impl Wallet { )?, }; - let message = secp256k1::Message::from_digest(sighash.to_byte_array()); - - 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 - .verify_ecdsa(&sig_hex, &msg_hex, &own_pubkey.to_string()) - .await; - signature - } - WalletType::Local { private_key } => { - // sign - let signature = self.secp.sign_ecdsa(&message, &private_key.inner); - // verify - self.secp - .verify_ecdsa(&message, &signature, &own_pubkey.inner)?; - signature - } - }; - - debug!("signature: {}", signature.serialize_der()); + let signature = self.sign_msg_hash(sighash, own_pubkey).await?; // append witness let signature = bitcoin::ecdsa::Signature::sighash_all(signature).into(); @@ -207,6 +232,42 @@ impl Wallet { Ok(hash.into_transaction()) } + async fn sign_msg_hash( + &self, + sighash: SegwitV0Sighash, + own_pubkey: &PublicKey, + ) -> OrdResult { + 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 + .verify_ecdsa(&sig_hex, &msg_hex, &own_pubkey.to_string()) + .await; + signature + } + WalletType::Local { private_key } => { + let message = secp256k1::Message::from_digest(sighash.to_byte_array()); + + // sign + let signature = self.secp.sign_ecdsa(&message, &private_key.inner); + // verify + self.secp + .verify_ecdsa(&message, &signature, &own_pubkey.inner)?; + signature + } + }; + + debug!("signature: {}", signature.serialize_der()); + + Ok(signature) + } + fn append_witness_to_input( &self, sighasher: &mut SighashCache, From 1c06e502ee688a7efb96706fc380ceafba7bdc01 Mon Sep 17 00:00:00 2001 From: Maxim Date: Fri, 5 Apr 2024 11:01:05 +0300 Subject: [PATCH 2/6] Formatting --- src/wallet/builder/rune.rs | 13 +++++++++---- src/wallet/builder/signer.rs | 12 ++++++------ 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/wallet/builder/rune.rs b/src/wallet/builder/rune.rs index 147dc09..74d9380 100644 --- a/src/wallet/builder/rune.rs +++ b/src/wallet/builder/rune.rs @@ -1,12 +1,12 @@ +use bitcoin::{Address, Amount, FeeRate, ScriptBuf, Transaction, TxIn, TxOut}; use bitcoin::absolute::LockTime; use bitcoin::transaction::Version; -use bitcoin::{Address, Amount, FeeRate, ScriptBuf, Transaction, TxIn, TxOut}; use ordinals::{Edict, RuneId, Runestone}; +use crate::{OrdError, OrdTransactionBuilder}; use crate::fees::estimate_transaction_fees; use crate::wallet::builder::TxInputInfo; use crate::wallet::ScriptType; -use crate::{OrdError, OrdTransactionBuilder}; /// Postage amount for rune transaction. /// @@ -108,8 +108,13 @@ impl OrdTransactionBuilder { output: outputs, }; - let fee_amount = - estimate_transaction_fees(ScriptType::P2WSH, unsigned_tx.vsize(), args.fee_rate, &None); + let fee_amount = estimate_transaction_fees( + ScriptType::P2WSH, + unsigned_tx.vsize(), + unsigned_tx.input.len(), + args.fee_rate, + &None, + ); let change_amount = args .input_amount() .checked_sub(fee_amount + RUNE_POSTAGE * 2) diff --git a/src/wallet/builder/signer.rs b/src/wallet/builder/signer.rs index 3659ab9..bb2ccbf 100644 --- a/src/wallet/builder/signer.rs +++ b/src/wallet/builder/signer.rs @@ -1,16 +1,16 @@ -use bitcoin::{ - PrivateKey, PublicKey, ScriptBuf, SegwitV0Sighash, TapLeafHash, TapSighashType, Transaction, - Witness, -}; use bitcoin::hashes::Hash as _; use bitcoin::key::Secp256k1; -use bitcoin::secp256k1::{self, All}; use bitcoin::secp256k1::ecdsa::Signature; +use bitcoin::secp256k1::{self, All}; use bitcoin::sighash::{Prevouts, SighashCache}; use bitcoin::taproot::{ControlBlock, LeafVersion}; +use bitcoin::{ + PrivateKey, PublicKey, ScriptBuf, SegwitV0Sighash, TapLeafHash, TapSighashType, Transaction, + Witness, +}; -use crate::{OrdError, OrdResult}; use crate::wallet::builder::TxInputInfo; +use crate::{OrdError, OrdResult}; use super::super::builder::Utxo; use super::taproot::TaprootPayload; From 470cb0af54e04d390e9ad327868d433fe65800bb Mon Sep 17 00:00:00 2001 From: Maxim Date: Fri, 5 Apr 2024 11:34:06 +0300 Subject: [PATCH 3/6] Formatting --- src/lib.rs | 1 - src/wallet/builder.rs | 7 ++----- src/wallet/builder/rune.rs | 4 ++-- src/wallet/builder/signer.rs | 5 ++--- 4 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 3f40515..0f10460 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,7 +20,6 @@ extern crate log; extern crate serde; pub use bitcoin; - pub use error::{InscriptionParseError, OrdError}; pub use inscription::brc20::Brc20; pub use inscription::nft::Nft; diff --git a/src/wallet/builder.rs b/src/wallet/builder.rs index e428279..c89c826 100644 --- a/src/wallet/builder.rs +++ b/src/wallet/builder.rs @@ -5,17 +5,15 @@ use bitcoin::{ 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::utils::push_bytes::bytes_to_push_bytes; use crate::{OrdError, OrdResult}; -use super::builder::taproot::{generate_keypair, TaprootPayload}; - #[cfg(feature = "rune")] mod rune; @@ -517,9 +515,8 @@ mod test { use bitcoin::PrivateKey; use hex_literal::hex; - use crate::Brc20; - use super::*; + use crate::Brc20; // const WIF: &str = "cVkWbHmoCx6jS8AyPNQqvFr8V9r2qzDHJLaxGDQgDJfxT73w6fuU"; diff --git a/src/wallet/builder/rune.rs b/src/wallet/builder/rune.rs index 74d9380..26b24e8 100644 --- a/src/wallet/builder/rune.rs +++ b/src/wallet/builder/rune.rs @@ -1,12 +1,12 @@ -use bitcoin::{Address, Amount, FeeRate, ScriptBuf, Transaction, TxIn, TxOut}; use bitcoin::absolute::LockTime; use bitcoin::transaction::Version; +use bitcoin::{Address, Amount, FeeRate, ScriptBuf, Transaction, TxIn, TxOut}; use ordinals::{Edict, RuneId, Runestone}; -use crate::{OrdError, OrdTransactionBuilder}; use crate::fees::estimate_transaction_fees; use crate::wallet::builder::TxInputInfo; use crate::wallet::ScriptType; +use crate::{OrdError, OrdTransactionBuilder}; /// Postage amount for rune transaction. /// diff --git a/src/wallet/builder/signer.rs b/src/wallet/builder/signer.rs index bb2ccbf..181f0ab 100644 --- a/src/wallet/builder/signer.rs +++ b/src/wallet/builder/signer.rs @@ -9,11 +9,10 @@ use bitcoin::{ Witness, }; -use crate::wallet::builder::TxInputInfo; -use crate::{OrdError, OrdResult}; - use super::super::builder::Utxo; use super::taproot::TaprootPayload; +use crate::wallet::builder::TxInputInfo; +use crate::{OrdError, OrdResult}; /// An abstraction over a transaction signer. #[async_trait::async_trait] From 7ebcf94cda21c1249119fbc71f2d2f759cdf704d Mon Sep 17 00:00:00 2001 From: Maxim Date: Tue, 16 Apr 2024 06:32:16 +0300 Subject: [PATCH 4/6] Added sign transaction method and test for run tx --- src/wallet/builder/rune.rs | 168 ++++++++++++++++++++++++++++++++--- src/wallet/builder/signer.rs | 93 +++++++++++++------ 2 files changed, 221 insertions(+), 40 deletions(-) diff --git a/src/wallet/builder/rune.rs b/src/wallet/builder/rune.rs index 26b24e8..4b4248f 100644 --- a/src/wallet/builder/rune.rs +++ b/src/wallet/builder/rune.rs @@ -17,14 +17,14 @@ pub const RUNE_POSTAGE: Amount = Amount::from_sat(10_000); pub struct CreateEdictTxArgs { /// Identifier of the rune to be transferred. rune: RuneId, - /// Inputs that contain rune balance to be transferred. - rune_inputs: Vec, - /// Inputs that contain BTC balance to cover outputs and transaction fees. - funding_inputs: Vec, + /// Inputs that contain rune and funding BTC balances. + inputs: Vec, /// Address of the recipient of the rune transfer. destination: Address, - /// Address that will receive leftovers of runes and BTC. + /// Address that will receive leftovers of BTC. change_address: Address, + /// Address that will receive leftovers of runes. + rune_change_address: Address, /// Amount of the rune to be transferred. amount: u128, /// Current BTC fee rate. @@ -32,12 +32,10 @@ pub struct CreateEdictTxArgs { } impl CreateEdictTxArgs { - fn inputs(&self) -> impl Iterator { - self.rune_inputs.iter().chain(self.funding_inputs.iter()) - } - fn input_amount(&self) -> Amount { - self.inputs().fold(Amount::ZERO, |a, b| a + b.tx_out.value) + self.inputs + .iter() + .fold(Amount::ZERO, |a, b| a + b.tx_out.value) } } @@ -54,7 +52,7 @@ impl OrdTransactionBuilder { /// to cover the outputs and transaction fee. pub fn create_edict_transaction( &self, - args: CreateEdictTxArgs, + args: &CreateEdictTxArgs, ) -> Result { let runestone = Runestone { edicts: vec![Edict { @@ -73,7 +71,7 @@ impl OrdTransactionBuilder { }; let rune_change_out = TxOut { value: RUNE_POSTAGE, - script_pubkey: args.change_address.script_pubkey(), + script_pubkey: args.rune_change_address.script_pubkey(), }; let rune_destination_out = TxOut { value: RUNE_POSTAGE, @@ -92,7 +90,8 @@ impl OrdTransactionBuilder { ]; let inputs = args - .inputs() + .inputs + .iter() .map(|rune_input| TxIn { previous_output: rune_input.outpoint, script_sig: Default::default(), @@ -109,7 +108,7 @@ impl OrdTransactionBuilder { }; let fee_amount = estimate_transaction_fees( - ScriptType::P2WSH, + ScriptType::P2TR, unsigned_tx.vsize(), unsigned_tx.input.len(), args.fee_rate, @@ -125,3 +124,144 @@ impl OrdTransactionBuilder { Ok(unsigned_tx) } } + +#[cfg(test)] +mod tests { + use std::io::Cursor; + use std::str::FromStr; + + use bitcoin::consensus::Decodable; + use bitcoin::key::Secp256k1; + use bitcoin::{Network, OutPoint, PrivateKey, PublicKey, Txid}; + + use crate::{Wallet, WalletType}; + + use super::*; + + #[tokio::test] + async fn create_edict_transaction() { + const PRIVATE_KEY: &str = + "66c4e94a319776225307f6f89644a827c61150d2ac21b1fc110d330364088024"; + let private_key = PrivateKey::from_slice( + &hex::decode(PRIVATE_KEY).expect("failed to decode hex private key"), + Network::Regtest, + ) + .expect("invalid private key"); + let public_key = PublicKey::from_private_key(&Secp256k1::new(), &private_key); + let wallet = Wallet::new_with_signer(WalletType::Local { private_key }); + let builder = OrdTransactionBuilder::new(public_key, ScriptType::P2WSH, wallet); + + let args = CreateEdictTxArgs { + rune: RuneId::new(219, 1).unwrap(), + inputs: vec![ + TxInputInfo { + outpoint: OutPoint::new( + Txid::from_str( + "9100acad2da80d2198b257acc5d98a6265fda510bc8f1252334876dad4c289f4", + ) + .unwrap(), + 1, + ), + tx_out: TxOut { + value: Amount::from_sat(10000), + script_pubkey: ScriptBuf::from_hex( + "5120c57c572f5401e740701ce673bf6c826890eec9d7898bc0415f140cb252fdaf72", + ) + .unwrap(), + }, + }, + TxInputInfo { + outpoint: OutPoint::new( + Txid::from_str( + "9100acad2da80d2198b257acc5d98a6265fda510bc8f1252334876dad4c289f4", + ) + .unwrap(), + 2, + ), + tx_out: TxOut { + value: Amount::from_sat(10000), + script_pubkey: ScriptBuf::from_hex( + "51200c7598875b445a85a351dafcb08f05a7dc1e958b5f704d2a3f2aeb31f085abd4", + ) + .unwrap(), + }, + }, + TxInputInfo { + outpoint: OutPoint::new( + Txid::from_str( + "9100acad2da80d2198b257acc5d98a6265fda510bc8f1252334876dad4c289f4", + ) + .unwrap(), + 3, + ), + tx_out: TxOut { + value: Amount::from_sat(9943140), + script_pubkey: ScriptBuf::from_hex( + "5120ddf99a3af83d2f741c955394345df2abd67a33d4e9b27d6256b65cfb24b64236", + ) + .unwrap(), + }, + }, + ], + destination: Address::from_str( + "bcrt1pu8kl0t74qn89ljqs6ez558uyjvht3d93hsa2ha3u7654hgqjmadqlm20ps", + ) + .unwrap() + .assume_checked(), + change_address: Address::from_str( + "bcrt1pxsxjyxykvchklqaz0w6tk5wz28rmqn3efdt472g53s9m9hkwp3fs452s2t", + ) + .unwrap() + .assume_checked(), + rune_change_address: Address::from_str( + "bcrt1prsz63kjxu8qmgt8m0k6em7k9hkwwqqsykpts4ad5fkvq5yqt985sfl88qq", + ) + .unwrap() + .assume_checked(), + amount: 9500, + fee_rate: FeeRate::from_sat_per_vb(10).unwrap(), + }; + let unsigned_tx = builder + .create_edict_transaction(&args) + .expect("failed to create transaction"); + + let signed_tx = builder + .sign_transaction(&unsigned_tx, &args.inputs) + .await + .expect("failed to sign transaction"); + + eprintln!("Signed tx size: {}", signed_tx.vsize()); + + const EXPECTED: &str = "02000000000103f489c2d4da76483352128fbc10a5fd65628ad9c5ac57b298210da82dadac00910100000000fffffffff489c2d4da76483352128fbc10a5fd65628ad9c5ac57b298210da82dadac00910200000000fffffffff489c2d4da76483352128fbc10a5fd65628ad9c5ac57b298210da82dadac00910300000000fdffffff0400000000000000000a6a5d0700db01019c4a0210270000000000002251201c05a8da46e1c1b42cfb7db59dfac5bd9ce00204b0570af5b44d980a100b29e91027000000000000225120e1edf7afd504ce5fc810d6454a1f84932eb8b4b1bc3aabf63cf6a95ba012df5a6cab970000000000225120340d221896662f6f83a27bb4bb51c251c7b04e394b575f29148c0bb2dece0c53014037152ea3d7d70f9ff2a6df17413e71beb1b976e0800ff8c0bf285ac7dfc04345ecee174bccaa40a1ac12c80e141f77d616d58ac5520fa6cc5995e7a8ad0ea17b0140cf46d195ff294e0947cb915e5814806155c6db651c50062628ce72ffa4e078b0ed53ace9ac5ab514af0452420cfb30164867b01f6ad71d0fe92a3f90ead69ccf01405aeab94c3a51768d16ba58431f463691aea05e56b99b074f3f2ccb299c516d9901b81effde3b9964969de29d373dd5608c6fdd6a2c54df94e530b89a2b37488b00000000"; + let expected = + Transaction::consensus_decode(&mut Cursor::new(hex::decode(EXPECTED).unwrap())) + .expect("failed to decode expected transaction"); + + eprintln!("Expected tx size: {}", expected.vsize()); + + assert_eq!(signed_tx.version, expected.version); + assert_eq!(signed_tx.lock_time, expected.lock_time); + assert_eq!(signed_tx.input.len(), expected.input.len()); + assert_eq!(signed_tx.output.len(), expected.output.len()); + + for index in 0..signed_tx.input.len() { + // Do not compare witness since it depends on randomized value in each tx + assert_eq!( + signed_tx.input[index].previous_output, expected.input[index].previous_output, + "Input {index}" + ); + assert_eq!( + signed_tx.input[index].script_sig, expected.input[index].script_sig, + "Input {index}" + ); + } + + for index in 0..signed_tx.output.len() { + assert_eq!( + signed_tx.output[index].script_pubkey, expected.output[index].script_pubkey, + "Output {index}" + ); + //todo: add check of value after https://infinityswap.atlassian.net/browse/EPROD-830 + } + } +} diff --git a/src/wallet/builder/signer.rs b/src/wallet/builder/signer.rs index 181f0ab..4fa80a2 100644 --- a/src/wallet/builder/signer.rs +++ b/src/wallet/builder/signer.rs @@ -1,12 +1,12 @@ use bitcoin::hashes::Hash as _; -use bitcoin::key::Secp256k1; +use bitcoin::key::{Secp256k1, UntweakedKeypair}; use bitcoin::secp256k1::ecdsa::Signature; -use bitcoin::secp256k1::{self, All}; +use bitcoin::secp256k1::{self, All, Message}; use bitcoin::sighash::{Prevouts, SighashCache}; use bitcoin::taproot::{ControlBlock, LeafVersion}; use bitcoin::{ PrivateKey, PublicKey, ScriptBuf, SegwitV0Sighash, TapLeafHash, TapSighashType, Transaction, - Witness, + TxOut, Witness, }; use super::super::builder::Utxo; @@ -127,6 +127,45 @@ impl Wallet { Ok(sighash_cache.into_transaction()) } + fn sign_tr( + &self, + prev_outs: &[&TxOut], + index: usize, + sighash_cache: &mut SighashCache, + ) -> OrdResult<()> { + let prevouts = Prevouts::All(&prev_outs); + let sighash = sighash_cache.taproot_key_spend_signature_hash( + index, + &prevouts, + TapSighashType::Default, + )?; + + let keypair = match self.signer { + WalletType::Local { private_key } => { + UntweakedKeypair::from_secret_key(&self.secp, &private_key.inner) + } + WalletType::External { .. } => return Err(OrdError::TaprootCompute), + }; + let msg = Message::from(sighash); + let signature = self.secp.sign_schnorr_no_aux_rand(&msg, &keypair); + + // verify + self.secp + .verify_schnorr(&signature, &msg, &keypair.x_only_public_key().0)?; + + // append witness + let signature = bitcoin::taproot::Signature { + sig: signature, + hash_ty: TapSighashType::Default, + }; + let mut witness = Witness::new(); + witness.push(&signature.to_vec()); + + *sighash_cache.witness_mut(index).unwrap() = witness; + + Ok(()) + } + /// Sign a generic transaction. /// /// The given transaction must have the same inputs as the ones given in the `prev_outs` argument. @@ -144,32 +183,34 @@ impl Wallet { let mut cache = SighashCache::new(transaction.clone()); for (index, input) in prev_outs.iter().enumerate() { - let sighash = match &input.tx_out.script_pubkey { - s if s.is_p2wpkh() => cache.p2wpkh_signature_hash( - index, - s, - input.tx_out.value, - bitcoin::EcdsaSighashType::All, - )?, - s if s.is_p2wsh() => cache.p2wsh_signature_hash( + match &input.tx_out.script_pubkey { + s if s.is_p2wpkh() || s.is_p2wsh() => { + let sighash = cache.p2wpkh_signature_hash( + index, + s, + input.tx_out.value, + bitcoin::EcdsaSighashType::All, + )?; + + let signature = self.sign_msg_hash(sighash, own_pubkey).await?; + let ord_signature = bitcoin::ecdsa::Signature::sighash_all(signature).into(); + + self.append_witness_to_input( + &mut cache, + ord_signature, + index, + &own_pubkey.inner, + None, + None, + )?; + } + s if s.is_p2tr() => self.sign_tr( + &prev_outs.iter().map(|v| &v.tx_out).collect::>(), index, - s, - input.tx_out.value, - bitcoin::EcdsaSighashType::All, + &mut cache, )?, _ => return Err(OrdError::InvalidScriptType), - }; - let signature = self.sign_msg_hash(sighash, own_pubkey).await?; - let ord_signature = bitcoin::ecdsa::Signature::sighash_all(signature).into(); - - self.append_witness_to_input( - &mut cache, - ord_signature, - index, - &own_pubkey.inner, - None, - None, - )?; + } } Ok(cache.into_transaction()) From 034216ed25b92e9c1c76c2b89f05a48570bb9fbb Mon Sep 17 00:00:00 2001 From: Maxim Date: Tue, 16 Apr 2024 06:34:03 +0300 Subject: [PATCH 5/6] Change ordinals dependency to crates.io --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index dd31e11..44f4575 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ ciborium = "0.2" hex = "0.4" http = "1" log = "0.4" -ordinals = { git = "https://github.com/ordinals/ord", tag = "0.17.1", optional = true } +ordinals = { version = "0.0.7", optional = true } rand = { version = "0.8" } serde = { version = "1", features = ["derive"] } serde_json = "1" From c1f9d2f52530623ebf5f0345950a50a361c62d1d Mon Sep 17 00:00:00 2001 From: Maxim Date: Tue, 16 Apr 2024 06:36:04 +0300 Subject: [PATCH 6/6] Clippy and fmt --- src/wallet/builder/rune.rs | 3 +-- src/wallet/builder/signer.rs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/wallet/builder/rune.rs b/src/wallet/builder/rune.rs index 4b4248f..d778040 100644 --- a/src/wallet/builder/rune.rs +++ b/src/wallet/builder/rune.rs @@ -134,9 +134,8 @@ mod tests { use bitcoin::key::Secp256k1; use bitcoin::{Network, OutPoint, PrivateKey, PublicKey, Txid}; - use crate::{Wallet, WalletType}; - use super::*; + use crate::{Wallet, WalletType}; #[tokio::test] async fn create_edict_transaction() { diff --git a/src/wallet/builder/signer.rs b/src/wallet/builder/signer.rs index 4fa80a2..f16786d 100644 --- a/src/wallet/builder/signer.rs +++ b/src/wallet/builder/signer.rs @@ -133,7 +133,7 @@ impl Wallet { index: usize, sighash_cache: &mut SighashCache, ) -> OrdResult<()> { - let prevouts = Prevouts::All(&prev_outs); + let prevouts = Prevouts::All(prev_outs); let sighash = sighash_cache.taproot_key_spend_signature_hash( index, &prevouts,