From 34da195372c4328fe22a6c51cc2cc75d2096a10a Mon Sep 17 00:00:00 2001 From: jayendramadaram Date: Tue, 11 Jun 2024 03:09:28 +0530 Subject: [PATCH 1/9] feat/added payout update --- node/Cargo.lock | 111 ++++++++++++++++++++ node/Cargo.toml | 3 +- node/uhpo/Cargo.toml | 9 ++ node/uhpo/src/lib.rs | 2 + node/uhpo/src/payout_settlement.rs | 9 ++ node/uhpo/src/payout_update.rs | 161 +++++++++++++++++++++++++++++ 6 files changed, 294 insertions(+), 1 deletion(-) create mode 100644 node/uhpo/Cargo.toml create mode 100644 node/uhpo/src/lib.rs create mode 100644 node/uhpo/src/payout_settlement.rs create mode 100644 node/uhpo/src/payout_update.rs diff --git a/node/Cargo.lock b/node/Cargo.lock index 20e1518..af82bc7 100644 --- a/node/Cargo.lock +++ b/node/Cargo.lock @@ -127,6 +127,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + [[package]] name = "autocfg" version = "1.2.0" @@ -148,6 +154,70 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base58ck" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f" +dependencies = [ + "bitcoin-internals", + "bitcoin_hashes", +] + +[[package]] +name = "bech32" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" + +[[package]] +name = "bitcoin" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea507acc1cd80fc084ace38544bbcf7ced7c2aa65b653b102de0ce718df668f6" +dependencies = [ + "base58ck", + "bech32", + "bitcoin-internals", + "bitcoin-io", + "bitcoin-units", + "bitcoin_hashes", + "hex-conservative", + "hex_lit", + "secp256k1", +] + +[[package]] +name = "bitcoin-internals" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2" + +[[package]] +name = "bitcoin-io" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "340e09e8399c7bd8912f495af6aa58bea0c9214773417ffaa8f6460f93aaee56" + +[[package]] +name = "bitcoin-units" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb54da0b28892f3c52203a7191534033e051b6f4b52bc15480681b57b7e036f5" +dependencies = [ + "bitcoin-internals", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb18c03d0db0247e147a21a6faafd5a7eb851c743db062de72018b6b7e8e4d16" +dependencies = [ + "bitcoin-io", + "hex-conservative", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -572,6 +642,21 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "hex-conservative" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5313b072ce3c597065a808dbf612c4c8e8590bdbf8b579508bf7a762c5eae6cd" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hex_lit" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" + [[package]] name = "humantime" version = "2.1.0" @@ -958,6 +1043,25 @@ dependencies = [ "semver", ] +[[package]] +name = "secp256k1" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0cc0f1cf93f4969faf3ea1c7d8a9faed25918d96affa959720823dfe86d4f3" +dependencies = [ + "bitcoin_hashes", + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1433bd67156263443f14d603720b082dd3121779323fce20cba2aa07b874bc1b" +dependencies = [ + "cc", +] + [[package]] name = "semver" version = "1.0.22" @@ -1306,6 +1410,13 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "uhpo" +version = "0.1.0" +dependencies = [ + "bitcoin", +] + [[package]] name = "unicode-ident" version = "1.0.12" diff --git a/node/Cargo.toml b/node/Cargo.toml index 14c6401..534fa20 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -5,5 +5,6 @@ members = [ "config", "connection", "run", - "protocol" + "protocol", + "uhpo" ] diff --git a/node/uhpo/Cargo.toml b/node/uhpo/Cargo.toml new file mode 100644 index 0000000..989de97 --- /dev/null +++ b/node/uhpo/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "uhpo" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bitcoin = "0.32.2" \ No newline at end of file diff --git a/node/uhpo/src/lib.rs b/node/uhpo/src/lib.rs new file mode 100644 index 0000000..d3d7d83 --- /dev/null +++ b/node/uhpo/src/lib.rs @@ -0,0 +1,2 @@ +pub mod payout_settlement; +pub mod payout_update; diff --git a/node/uhpo/src/payout_settlement.rs b/node/uhpo/src/payout_settlement.rs new file mode 100644 index 0000000..99962f3 --- /dev/null +++ b/node/uhpo/src/payout_settlement.rs @@ -0,0 +1,9 @@ +use std::collections::HashMap; + +use bitcoin::{io::Error, secp256k1::SecretKey, Address, Transaction}; + +pub struct PayoutSettlement { + transaction: Transaction, +} + +impl PayoutSettlement {} diff --git a/node/uhpo/src/payout_update.rs b/node/uhpo/src/payout_update.rs new file mode 100644 index 0000000..a10dbbc --- /dev/null +++ b/node/uhpo/src/payout_update.rs @@ -0,0 +1,161 @@ +use std::fmt::Error; + +use bitcoin::{ + absolute::LockTime, + hashes::Hash, + key::Secp256k1, + secp256k1::{Message, Scalar, SecretKey}, + sighash::{Prevouts, SighashCache, TaprootError}, + transaction::Version, + Address, Amount, OutPoint, ScriptBuf, Sequence, TapSighashType, Transaction, TxIn, TxOut, + Witness, +}; + +struct PayoutUpdate<'a> { + transaction: Transaction, + coinbase_txout: &'a TxOut, + prev_update_txout: Option<&'a TxOut>, +} + +impl<'a> PayoutUpdate<'a> { + fn new( + prev_update_tx: Option<&'a Transaction>, + coinbase_tx: &'a Transaction, + next_out_address: Address, + projected_fee: u64, + ) -> Result { + let prev_update_txout = prev_update_tx.map(|tx| &tx.output[0]); + let coinbase_txout = &coinbase_tx.output[0]; + + let payout_update_tx = build_transaction( + coinbase_tx, + prev_update_tx, + next_out_address, + projected_fee, + coinbase_txout, + prev_update_txout, + )?; + + Ok(PayoutUpdate { + transaction: payout_update_tx, + coinbase_txout, + prev_update_txout, + }) + } + + fn add_coinbase_sig( + &mut self, + private_key: &SecretKey, + tweak: &Scalar, + ) -> Result<(), TaprootError> { + add_signature( + &mut self.transaction, + 0, + &[self.coinbase_txout], + private_key, + tweak, + ) + } + + fn add_prevout_sig( + &mut self, + private_key: &SecretKey, + tweak: &Scalar, + ) -> Result<(), TaprootError> { + let prev_update_txout = self + .prev_update_txout + .ok_or(TaprootError::InvalidSighashType(0))?; + add_signature( + &mut self.transaction, + 1, + &[prev_update_txout], + private_key, + tweak, + ) + } + + fn build(self) -> Transaction { + self.transaction + } +} + +fn build_transaction( + coinbase_tx: &Transaction, + prev_update_tx: Option<&Transaction>, + next_out_address: Address, + projected_fee: u64, + coinbase_txout: &TxOut, + prev_update_txout: Option<&TxOut>, +) -> Result { + let mut total_amount = coinbase_txout.value; + if let Some(tx_out) = prev_update_txout { + total_amount += tx_out.value; + } + + let mut payout_update_tx = Transaction { + version: Version::TWO, + lock_time: LockTime::ZERO, + input: vec![], + output: vec![], + }; + + payout_update_tx.input.push(TxIn { + previous_output: OutPoint { + txid: coinbase_tx.compute_txid(), + vout: 0, + }, + script_sig: ScriptBuf::new(), + sequence: Sequence::MAX, + witness: Witness::default(), + }); + + if let Some(tx) = prev_update_tx { + payout_update_tx.input.push(TxIn { + previous_output: OutPoint { + txid: tx.compute_txid(), + vout: 0, + }, + script_sig: ScriptBuf::new(), + sequence: Sequence::MAX, + witness: Witness::default(), + }); + } + + payout_update_tx.output.push(TxOut { + value: total_amount - Amount::from_sat(projected_fee), + script_pubkey: next_out_address.script_pubkey(), + }); + + Ok(payout_update_tx) +} + +fn add_signature( + transaction: &mut Transaction, + input_idx: usize, + prevouts: &[&TxOut], + private_key: &SecretKey, + tweak: &Scalar, +) -> Result<(), TaprootError> { + let secp = Secp256k1::new(); + let keypair = private_key.keypair(&secp); + + let mut sighash_cache = SighashCache::new(transaction.clone()); + let sighash = sighash_cache.taproot_key_spend_signature_hash( + input_idx, + &Prevouts::All(prevouts), + TapSighashType::All, + )?; + + let message = Message::from_digest(sighash.as_raw_hash().to_byte_array()); + let tweaked_keypair = keypair + .add_xonly_tweak(&secp, tweak) + .map_err(|_| TaprootError::InvalidSighashType(0))?; + + let signature = secp.sign_schnorr_no_aux_rand(&message, &tweaked_keypair); + let mut vec_sig = signature.serialize().to_vec(); + vec_sig.push(0x01); + + transaction.input[input_idx].witness.push(vec_sig); + + Ok(()) +} From 53496f0bad8dace1d010f1481a924dac0b073afe Mon Sep 17 00:00:00 2001 From: jayendramadaram Date: Tue, 18 Jun 2024 11:27:43 +0530 Subject: [PATCH 2/9] feat/added payout update transaction Builder --- .gitignore | 5 +- node/Cargo.lock | 29 +++ node/uhpo/Cargo.toml | 3 +- node/uhpo/docs/spec.md | 320 +++++++++++++++++++++++++++++ node/uhpo/src/error.rs | 46 +++++ node/uhpo/src/lib.rs | 1 + node/uhpo/src/payout_settlement.rs | 8 +- node/uhpo/src/payout_update.rs | 187 +++++++++++++---- 8 files changed, 552 insertions(+), 47 deletions(-) create mode 100644 node/uhpo/docs/spec.md create mode 100644 node/uhpo/src/error.rs diff --git a/.gitignore b/.gitignore index a01a555..dc2d56c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.vscode + **/__pycache__ **/.pytest_cache channels/build @@ -16,4 +18,5 @@ proposal/proposal.blg simulations/logs/* simulations/*.toolbox venv -target \ No newline at end of file +target +todo \ No newline at end of file diff --git a/node/Cargo.lock b/node/Cargo.lock index af82bc7..ddda211 100644 --- a/node/Cargo.lock +++ b/node/Cargo.lock @@ -888,6 +888,12 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + [[package]] name = "predicates" version = "3.1.0" @@ -954,6 +960,27 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + [[package]] name = "rand_core" version = "0.6.4" @@ -1050,6 +1077,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e0cc0f1cf93f4969faf3ea1c7d8a9faed25918d96affa959720823dfe86d4f3" dependencies = [ "bitcoin_hashes", + "rand", "secp256k1-sys", ] @@ -1415,6 +1443,7 @@ name = "uhpo" version = "0.1.0" dependencies = [ "bitcoin", + "rand", ] [[package]] diff --git a/node/uhpo/Cargo.toml b/node/uhpo/Cargo.toml index 989de97..41dfdc5 100644 --- a/node/uhpo/Cargo.toml +++ b/node/uhpo/Cargo.toml @@ -6,4 +6,5 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -bitcoin = "0.32.2" \ No newline at end of file +bitcoin = {version = "0.32.2" , features = ["rand"] } +rand = "0.8.5" diff --git a/node/uhpo/docs/spec.md b/node/uhpo/docs/spec.md new file mode 100644 index 0000000..5cec717 --- /dev/null +++ b/node/uhpo/docs/spec.md @@ -0,0 +1,320 @@ +# Braidpool UHPO Spec + +## Objective + +--- + +- rust crate to build uhpo transactions. +- Include unit and integration tests for the crate. + +Reference Image + +![https://gist.githubusercontent.com/pool2win/77bb9b98f9f3b8c0f90963343c3c840f/raw/8fa2481728e7c12d553608af23ca6e551c7c90c1//uhpo-success.png](https://gist.githubusercontent.com/pool2win/77bb9b98f9f3b8c0f90963343c3c840f/raw/8fa2481728e7c12d553608af23ca6e551c7c90c1//uhpo-success.png) + +## Functional Requirements + +- Should be able to build `payout-update` and `payout-settlement` Transaction. + +## **Specification** + +--- + +### Building Payout-Update/Payout-Settlement Transaction + +`payout-update` is transaction which merges coinbase funds after they achieve maturity. and accumulates in a `eltoo` fashion. payout-update transaction will be cached and broadcasted onchain when coinbase attains maturity.. + +```rust +pub struct PayoutUpdate { + transaction: Transaction, + fee: u64 +} +// Q. let's try to put in the fields here, so we develop an understanding of what is required here. +// A. I could come up with only two params for now. I will add further params as we build. + +[KP] The fee can be derived from the transaction, so we don't need the fee field. + +// Q. We should use the builder pattern here. You'll find writeups on Rust builder pattern. I think rust-bitcoin uses it too? +// I tried modifying to match it to follow builder pattern. +// new() - builds base templete for payout-update transaction +// add-sig() : signs and adds signature to txs +// builds : returns entire transaction. + +[KP] - s/add-sig/add_sig and s/builds/build + +[KP] - generally it is not good to shorten names. next_out_address. Do you want to say next_output_address? + +[KP] - I haven't reviewed the body of the functions below yet. We just want to focus on the signatures of the functions atm. + +[KP] - Re add_coinbase_sig: Rust doesn't do function overloading. So we'll need two functions with different names. + +impl PayoutUpdate { + + pub fn new( + prev_update_tx: Option, // optional because initial payout-update tx is null. + coinbase: Transaction, + next_out_address: Address, + ) -> Self { + /* + Tx | Inputs | Output | + ***************************** + | coinbase | new_payout + | prev_update? | + */ + let mut tx = Transaction::new(); + + let fee = avg_fee.now(24); + let total_output_value: u64 = coinbase.outputs[0].value - fee; + + if let Some(prev_tx) = prev_update_tx { + total_output_value += prev_tx.outputs[0].value; + } + + let output = TxOut::new(total_output_value, next_out_address.script_pubkey()); + tx.output.extend(output); + + let coinbase_input = TxIn { + OutPoint: (coinbase.txid , 0), + }; + tx.input.extend(coinbase_input); + + + if let Some(prev_tx) = prev_update_tx { + let prev_input = TxIn { + OutPoint: (prev_tx.txid , 0), + } + tx.input.extend(prev_input); + } + + + PayoutUpdate { + transaction: tx, + fee: fee + } + } + + // add_coinbase_sig + pub fn add_coinbase_sig(&mut self, private_key: &SecretKey) -> Result<(), Error> { + + // get taproot script pubkey for coinbase + let taproot_script_pub_key = coinbase.output[0].script; + + let funding_output = TxOut { + value: self.transaction.output[0].value, + script_pubkey: taproot_script_pub_key, + }; + + // compute sigHash + let prevout = vec![&funding_output]; + let mut sighash_cache = SighashCache::new(&self.transaction); + let sighash = sighash_cache + .taproot_key_spend_signature_hash(0, &Prevouts::All(&prevout), sighash::TapSighashType::All) + .unwrap(); + + let message = Message::from_digest(sighash.as_raw_hash().to_byte_array()); + let tweaked_keypair = private_key.keypair(&secp).add_xonly_tweak(&secp, &taproot_script_pub_key.tap_tweak().to_scalar()).unwrap(); + + // Sign messege Digest and add signature to witness + let signature = secp.sign_schnorr(&message, &tweaked_keypair); + secp.verify_schnorr( + &signature, + &message, + &taproot_script_pub_key.output_key().to_inner(), + ).unwrap(); + + let mut vec_sig = signature.serialize().to_vec(); + vec_sig.push(0x01); + + keypath_tx.input[0].witness.push(vec_sig); + + Ok(()) + } + + pub fn add_coinbase_sig(&mut self, private_key: &SecretKey) -> Result<(), Error> { + // follows same pseudo code as earlier function but signs second input! + } + + pub fn build(self) Transaction { + self.transaction + } +} +``` + +### Implementation Code for Payout Update Struct + + +--- + +```rust + +let coinbase_tx = Transaction::new(); + +let coinbase_output = TxOut { + value: 50 * 100_000_000, // 50 BTC + script_pubkey: POOL_KEY, +}; +coinbase_tx.output.push(coinbase_output); + +block.txdata.push(coinbase_tx); +block.mine(); + +// Create a random previous output transaction (prev_update_tx) +let prev_update_tx = Transaction { + version: 2, + lock_time: 0, + input: vec![TxIn { + previous_output: OutPoint::from("prev_update_out"), + sequence: 0xFFFFFFFF, + witness: vec![], + }], + output: vec![TxOut { + value: 100 * 100_000_000, // 100 BTC + script_pubkey: TAPROOT_KEY, + }], +}; + +let payout_update = PayoutUpdate::new( + Some(prevout_tx), + coinbase_tx, + Address::from_string("bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq").unwrap(), +); + +// Generate random private keys +let secp = Secp256k1::new(); +let privkey1 = SecretKey::new(&mut rng); +let privkey2 = SecretKey::new(&mut rng); + +// Add signatures with random private keys +payout_update.add_coinbase_sig(&privkey1).unwrap(); +payout_update.add_prevout_sig(&privkey2).unwrap(); + +// Build the transaction +let final_tx = payout_update.build(); + +``` + +## Payout Settlement + +--- + +`payout-settlement` is a transaction which spends funds from from latest `poolkey` which was used as output in `payout-update` transaction. + +```rust +pub struct PayoutSettlement { + transaction: Transaction, + fee: u64 +} + +impl PayoutSettlement { + pub fn new( + latest_eltoo_out_address: Address, + payout_map: HashMap, + ) -> Self { + let mut tx = Transaction::new(); + + let fee = avg_fee.now(24); + + tx.input.extend(TxIn { + outpoint : latest_eltoo_out_address, + vout: 0 + }) + + payout_map.iter().for_each(|address, amount| { + tx.output.extend({ + TxOut { + script : ScriptBuf::from(Address), + amount: amount + } + }) + }); + + PayoutSettlement { + transaction: tx, + fee: fee + } + } + + pub fn add_sig(&mut self, private_key: &SecretKey) -> Result<(), Error> { + // follows same pseudo code as earlier payyout-update struct. + } + + pub fn build(self) Transaction { + self.transaction + } +} +``` + +### Implementation Code for Payout Settlement Struct + +--- + +```rust + +// Create a random previous output transaction (prev_update_tx) +let prev_update_tx = Transaction { + version: 2, + lock_time: 0, + input: vec![TxIn { + previous_output: OutPoint::from("prev_update_out"), + sequence: 0xFFFFFFFF, + witness: vec![], + }], + output: vec![TxOut { + value: 100 * 100_000_000, // 100 BTC + script_pubkey: TAPROOT_KEY, + }], +}; + +let (_, latest_eltoo_out_address) = generate_random_pubkey(); + +let payout_map = HashMap
::new([ + (ALICE , 20), + (BOB , 30), + (CHARLIE , 10), + (DAVID , 40), + ]); + +let payout_settlement = PayoutSettlement::new(latest_eltoo_out_address, payout_map); + +// Generate random private keys +let secp = Secp256k1::new(); +let privkey1 = SecretKey::new(&mut rng); + +// Add signatures with random private keys +payout_settlement.add_sig(&privkey1).unwrap(); + +// Build and print the transaction +let final_tx = payout_settlement.build(); +``` + +## Dependencies + +- Rust-bitcoin crate +- zmq and other networking crates. + +## Testing and Documentation + +- Unit tests for individual modules and integration tests for the overall system. + - includes spining up local regtest node and executing a chain of transaction flows. +- Example usages in the `examples/` directory + +## Expected File Structure + +```rust +rust-uhpo/ +├── src/ +│ ├── lib.rs +│ ├── transaction/ +│ │ ├── mod.rs +│ │ ├── payout_update.rs +│ │ ├── payout_settlement.rs +│ ├── network/ +│ └── utils/ +│ ├── mod.rs +│ └── ... (utility modules) +├── tests/ +│ └── ... (integration tests) +├── examples/ +│ └── ... (example usages) +├── Cargo.toml +└── README.md +``` \ No newline at end of file diff --git a/node/uhpo/src/error.rs b/node/uhpo/src/error.rs new file mode 100644 index 0000000..4153b8b --- /dev/null +++ b/node/uhpo/src/error.rs @@ -0,0 +1,46 @@ +use core::fmt; + +use bitcoin::sighash::TaprootError; + +#[derive(Debug)] +pub enum UhpoError { + TaprootError(TaprootError), + Secp256k1Error(bitcoin::secp256k1::Error), + NoPrevUpdateTxOut, + Other(String), +} + +impl From for UhpoError { + fn from(err: TaprootError) -> Self { + UhpoError::TaprootError(err) + } +} + +impl From for UhpoError { + fn from(err: bitcoin::secp256k1::Error) -> Self { + UhpoError::Secp256k1Error(err) + } +} + +impl fmt::Display for UhpoError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + UhpoError::TaprootError(err) => write!(f, "Taproot error: {}", err), + UhpoError::Secp256k1Error(err) => { + write!(f, "Signature verification error: {}", err) + } + UhpoError::NoPrevUpdateTxOut => { + write!(f, "No previous update transaction output provided") + } + UhpoError::Other(msg) => write!(f, "Other error: {}", msg), + } + } +} + +impl std::error::Error for UhpoError {} + +impl UhpoError { + pub fn new_other_error(message: &str) -> Self { + UhpoError::Other(message.to_string()) + } +} diff --git a/node/uhpo/src/lib.rs b/node/uhpo/src/lib.rs index d3d7d83..ffb5321 100644 --- a/node/uhpo/src/lib.rs +++ b/node/uhpo/src/lib.rs @@ -1,2 +1,3 @@ +pub mod error; pub mod payout_settlement; pub mod payout_update; diff --git a/node/uhpo/src/payout_settlement.rs b/node/uhpo/src/payout_settlement.rs index 99962f3..835fe26 100644 --- a/node/uhpo/src/payout_settlement.rs +++ b/node/uhpo/src/payout_settlement.rs @@ -1,9 +1,3 @@ -use std::collections::HashMap; - -use bitcoin::{io::Error, secp256k1::SecretKey, Address, Transaction}; - -pub struct PayoutSettlement { - transaction: Transaction, -} +pub struct PayoutSettlement {} impl PayoutSettlement {} diff --git a/node/uhpo/src/payout_update.rs b/node/uhpo/src/payout_update.rs index a10dbbc..e2899df 100644 --- a/node/uhpo/src/payout_update.rs +++ b/node/uhpo/src/payout_update.rs @@ -3,26 +3,28 @@ use std::fmt::Error; use bitcoin::{ absolute::LockTime, hashes::Hash, - key::Secp256k1, + key::{Keypair, Secp256k1}, secp256k1::{Message, Scalar, SecretKey}, - sighash::{Prevouts, SighashCache, TaprootError}, + sighash::{Prevouts, SighashCache}, transaction::Version, Address, Amount, OutPoint, ScriptBuf, Sequence, TapSighashType, Transaction, TxIn, TxOut, Witness, }; -struct PayoutUpdate<'a> { +use crate::error::UhpoError; + +pub struct PayoutUpdate<'a> { transaction: Transaction, coinbase_txout: &'a TxOut, prev_update_txout: Option<&'a TxOut>, } impl<'a> PayoutUpdate<'a> { - fn new( + pub fn new( prev_update_tx: Option<&'a Transaction>, coinbase_tx: &'a Transaction, next_out_address: Address, - projected_fee: u64, + projected_fee: Amount, ) -> Result { let prev_update_txout = prev_update_tx.map(|tx| &tx.output[0]); let coinbase_txout = &coinbase_tx.output[0]; @@ -43,38 +45,31 @@ impl<'a> PayoutUpdate<'a> { }) } - fn add_coinbase_sig( + pub fn add_coinbase_sig( &mut self, private_key: &SecretKey, - tweak: &Scalar, - ) -> Result<(), TaprootError> { - add_signature( - &mut self.transaction, - 0, - &[self.coinbase_txout], - private_key, - tweak, - ) + tweak: &Option, + ) -> Result<(), UhpoError> { + let prevouts = match self.prev_update_txout { + Some(prev_update_txout) => vec![self.coinbase_txout, prev_update_txout], + None => vec![self.coinbase_txout], + }; + + add_signature(&mut self.transaction, 0, &prevouts, private_key, tweak) } - fn add_prevout_sig( + pub fn add_prev_update_sig( &mut self, private_key: &SecretKey, - tweak: &Scalar, - ) -> Result<(), TaprootError> { - let prev_update_txout = self - .prev_update_txout - .ok_or(TaprootError::InvalidSighashType(0))?; - add_signature( - &mut self.transaction, - 1, - &[prev_update_txout], - private_key, - tweak, - ) + tweak: &Option, + ) -> Result<(), UhpoError> { + let prev_update_txout = self.prev_update_txout.ok_or(UhpoError::NoPrevUpdateTxOut)?; + let prevouts = vec![self.coinbase_txout, prev_update_txout]; + + add_signature(&mut self.transaction, 1, &prevouts, private_key, tweak) } - fn build(self) -> Transaction { + pub fn build(self) -> Transaction { self.transaction } } @@ -83,7 +78,7 @@ fn build_transaction( coinbase_tx: &Transaction, prev_update_tx: Option<&Transaction>, next_out_address: Address, - projected_fee: u64, + projected_fee: Amount, coinbase_txout: &TxOut, prev_update_txout: Option<&TxOut>, ) -> Result { @@ -122,7 +117,7 @@ fn build_transaction( } payout_update_tx.output.push(TxOut { - value: total_amount - Amount::from_sat(projected_fee), + value: total_amount - projected_fee, script_pubkey: next_out_address.script_pubkey(), }); @@ -134,12 +129,17 @@ fn add_signature( input_idx: usize, prevouts: &[&TxOut], private_key: &SecretKey, - tweak: &Scalar, -) -> Result<(), TaprootError> { + tweak: &Option, +) -> Result<(), UhpoError> { let secp = Secp256k1::new(); - let keypair = private_key.keypair(&secp); + + let keypair: Keypair = match tweak { + Some(tweak) => private_key.keypair(&secp).add_xonly_tweak(&secp, tweak)?, + None => private_key.keypair(&secp), + }; let mut sighash_cache = SighashCache::new(transaction.clone()); + let sighash = sighash_cache.taproot_key_spend_signature_hash( input_idx, &Prevouts::All(prevouts), @@ -147,15 +147,126 @@ fn add_signature( )?; let message = Message::from_digest(sighash.as_raw_hash().to_byte_array()); - let tweaked_keypair = keypair - .add_xonly_tweak(&secp, tweak) - .map_err(|_| TaprootError::InvalidSighashType(0))?; - let signature = secp.sign_schnorr_no_aux_rand(&message, &tweaked_keypair); + let signature = secp.sign_schnorr_with_rng(&message, &keypair, &mut rand::thread_rng()); let mut vec_sig = signature.serialize().to_vec(); vec_sig.push(0x01); + secp.verify_schnorr(&signature, &message, &keypair.x_only_public_key().0)?; + transaction.input[input_idx].witness.push(vec_sig); Ok(()) } + +// unit tests +#[cfg(test)] +mod tests { + use bitcoin::{key::Keypair, secp256k1::All, Network}; + use rand::Rng; + + use super::*; + + fn setup() -> (Secp256k1, Keypair, Address) { + let secp = Secp256k1::new(); + let mut rng = rand::thread_rng(); + + let data: [u8; 32] = rng.gen(); + let keypair = SecretKey::from_slice(&data).unwrap().keypair(&secp); + + let new_payout_address: Address = + Address::p2tr(&secp, keypair.x_only_public_key().0, None, Network::Regtest); + + (secp, keypair, new_payout_address) + } + + #[test] + fn test_new_payout_update_only_cb() { + let (_, _, new_payout_address) = setup(); + + let coinbase_tx = create_dummy_transaction(); + let prev_update_tx = None; + let projected_fee = Amount::from_sat(1000); + + let payout_update = PayoutUpdate::new( + prev_update_tx, + &coinbase_tx, + new_payout_address, + projected_fee, + ) + .expect("Failed to create payout update"); + + assert_eq!(payout_update.transaction.input.len(), 1); + assert_eq!(payout_update.transaction.output.len(), 1); + assert_eq!( + payout_update.transaction.output[0].value, + Amount::from_sat(50000000) - projected_fee + ); + assert!(payout_update.prev_update_txout.is_none()); + } + + #[test] + fn test_new_payout_update() { + let (_, _, new_payout_address) = setup(); + + let coinbase_tx = create_dummy_transaction(); + let prev_update_tx = create_dummy_transaction(); + let projected_fee = Amount::from_sat(1000); + + let payout_update = PayoutUpdate::new( + Some(&prev_update_tx), + &coinbase_tx, + new_payout_address, + projected_fee, + ) + .expect("Failed to create payout update"); + + assert_eq!(payout_update.transaction.input.len(), 2); + assert_eq!(payout_update.transaction.output.len(), 1); + assert_eq!( + payout_update.transaction.output[0].value, + Amount::from_sat(50000000 * 2) - projected_fee + ); + assert!(payout_update.prev_update_txout.is_some()); + } + + #[test] + fn test_add_signature() { + let (_, keypair, new_payout_address) = setup(); + + let coinbase_tx = create_dummy_transaction(); + let prev_update_tx = create_dummy_transaction(); + let projected_fee = Amount::from_sat(1000); + let mut payout_update = PayoutUpdate::new( + Some(&prev_update_tx), + &coinbase_tx, + new_payout_address, + projected_fee, + ) + .expect("Failed to create payout update"); + + payout_update + .add_coinbase_sig(&keypair.secret_key(), &None) + .expect("Failed to add coinbase signature"); + + payout_update + .add_prev_update_sig(&keypair.secret_key(), &None) + .expect("Failed to add prev update signature"); + + assert_eq!(payout_update.transaction.input[0].witness.len(), 1); + assert_eq!(payout_update.transaction.input[1].witness.len(), 1); + } + + fn create_dummy_transaction() -> Transaction { + let tx = Transaction { + version: Version::TWO, + lock_time: LockTime::ZERO, + input: vec![], + output: vec![TxOut { + value: Amount::from_sat(50000000), + script_pubkey: ScriptBuf::new(), + }], + }; + tx + } +} From 86f25c12560b2077eaced6f1392c8f49c518e7c6 Mon Sep 17 00:00:00 2001 From: jayendramadaram Date: Wed, 3 Jul 2024 00:23:12 +0530 Subject: [PATCH 3/9] fix/Refactor PayoutUpdate to use ownership transfer and improve crate --- .gitignore | 6 +- node/uhpo/docs/spec.md | 320 --------------------------------- node/uhpo/src/error.rs | 2 +- node/uhpo/src/payout_update.rs | 49 ++--- 4 files changed, 28 insertions(+), 349 deletions(-) delete mode 100644 node/uhpo/docs/spec.md diff --git a/.gitignore b/.gitignore index dc2d56c..a085767 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,3 @@ -.vscode - **/__pycache__ **/.pytest_cache channels/build @@ -17,6 +15,4 @@ proposal/proposal.bbl proposal/proposal.blg simulations/logs/* simulations/*.toolbox -venv -target -todo \ No newline at end of file +venv \ No newline at end of file diff --git a/node/uhpo/docs/spec.md b/node/uhpo/docs/spec.md deleted file mode 100644 index 5cec717..0000000 --- a/node/uhpo/docs/spec.md +++ /dev/null @@ -1,320 +0,0 @@ -# Braidpool UHPO Spec - -## Objective - ---- - -- rust crate to build uhpo transactions. -- Include unit and integration tests for the crate. - -Reference Image - -![https://gist.githubusercontent.com/pool2win/77bb9b98f9f3b8c0f90963343c3c840f/raw/8fa2481728e7c12d553608af23ca6e551c7c90c1//uhpo-success.png](https://gist.githubusercontent.com/pool2win/77bb9b98f9f3b8c0f90963343c3c840f/raw/8fa2481728e7c12d553608af23ca6e551c7c90c1//uhpo-success.png) - -## Functional Requirements - -- Should be able to build `payout-update` and `payout-settlement` Transaction. - -## **Specification** - ---- - -### Building Payout-Update/Payout-Settlement Transaction - -`payout-update` is transaction which merges coinbase funds after they achieve maturity. and accumulates in a `eltoo` fashion. payout-update transaction will be cached and broadcasted onchain when coinbase attains maturity.. - -```rust -pub struct PayoutUpdate { - transaction: Transaction, - fee: u64 -} -// Q. let's try to put in the fields here, so we develop an understanding of what is required here. -// A. I could come up with only two params for now. I will add further params as we build. - -[KP] The fee can be derived from the transaction, so we don't need the fee field. - -// Q. We should use the builder pattern here. You'll find writeups on Rust builder pattern. I think rust-bitcoin uses it too? -// I tried modifying to match it to follow builder pattern. -// new() - builds base templete for payout-update transaction -// add-sig() : signs and adds signature to txs -// builds : returns entire transaction. - -[KP] - s/add-sig/add_sig and s/builds/build - -[KP] - generally it is not good to shorten names. next_out_address. Do you want to say next_output_address? - -[KP] - I haven't reviewed the body of the functions below yet. We just want to focus on the signatures of the functions atm. - -[KP] - Re add_coinbase_sig: Rust doesn't do function overloading. So we'll need two functions with different names. - -impl PayoutUpdate { - - pub fn new( - prev_update_tx: Option, // optional because initial payout-update tx is null. - coinbase: Transaction, - next_out_address: Address, - ) -> Self { - /* - Tx | Inputs | Output | - ***************************** - | coinbase | new_payout - | prev_update? | - */ - let mut tx = Transaction::new(); - - let fee = avg_fee.now(24); - let total_output_value: u64 = coinbase.outputs[0].value - fee; - - if let Some(prev_tx) = prev_update_tx { - total_output_value += prev_tx.outputs[0].value; - } - - let output = TxOut::new(total_output_value, next_out_address.script_pubkey()); - tx.output.extend(output); - - let coinbase_input = TxIn { - OutPoint: (coinbase.txid , 0), - }; - tx.input.extend(coinbase_input); - - - if let Some(prev_tx) = prev_update_tx { - let prev_input = TxIn { - OutPoint: (prev_tx.txid , 0), - } - tx.input.extend(prev_input); - } - - - PayoutUpdate { - transaction: tx, - fee: fee - } - } - - // add_coinbase_sig - pub fn add_coinbase_sig(&mut self, private_key: &SecretKey) -> Result<(), Error> { - - // get taproot script pubkey for coinbase - let taproot_script_pub_key = coinbase.output[0].script; - - let funding_output = TxOut { - value: self.transaction.output[0].value, - script_pubkey: taproot_script_pub_key, - }; - - // compute sigHash - let prevout = vec![&funding_output]; - let mut sighash_cache = SighashCache::new(&self.transaction); - let sighash = sighash_cache - .taproot_key_spend_signature_hash(0, &Prevouts::All(&prevout), sighash::TapSighashType::All) - .unwrap(); - - let message = Message::from_digest(sighash.as_raw_hash().to_byte_array()); - let tweaked_keypair = private_key.keypair(&secp).add_xonly_tweak(&secp, &taproot_script_pub_key.tap_tweak().to_scalar()).unwrap(); - - // Sign messege Digest and add signature to witness - let signature = secp.sign_schnorr(&message, &tweaked_keypair); - secp.verify_schnorr( - &signature, - &message, - &taproot_script_pub_key.output_key().to_inner(), - ).unwrap(); - - let mut vec_sig = signature.serialize().to_vec(); - vec_sig.push(0x01); - - keypath_tx.input[0].witness.push(vec_sig); - - Ok(()) - } - - pub fn add_coinbase_sig(&mut self, private_key: &SecretKey) -> Result<(), Error> { - // follows same pseudo code as earlier function but signs second input! - } - - pub fn build(self) Transaction { - self.transaction - } -} -``` - -### Implementation Code for Payout Update Struct - - ---- - -```rust - -let coinbase_tx = Transaction::new(); - -let coinbase_output = TxOut { - value: 50 * 100_000_000, // 50 BTC - script_pubkey: POOL_KEY, -}; -coinbase_tx.output.push(coinbase_output); - -block.txdata.push(coinbase_tx); -block.mine(); - -// Create a random previous output transaction (prev_update_tx) -let prev_update_tx = Transaction { - version: 2, - lock_time: 0, - input: vec![TxIn { - previous_output: OutPoint::from("prev_update_out"), - sequence: 0xFFFFFFFF, - witness: vec![], - }], - output: vec![TxOut { - value: 100 * 100_000_000, // 100 BTC - script_pubkey: TAPROOT_KEY, - }], -}; - -let payout_update = PayoutUpdate::new( - Some(prevout_tx), - coinbase_tx, - Address::from_string("bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq").unwrap(), -); - -// Generate random private keys -let secp = Secp256k1::new(); -let privkey1 = SecretKey::new(&mut rng); -let privkey2 = SecretKey::new(&mut rng); - -// Add signatures with random private keys -payout_update.add_coinbase_sig(&privkey1).unwrap(); -payout_update.add_prevout_sig(&privkey2).unwrap(); - -// Build the transaction -let final_tx = payout_update.build(); - -``` - -## Payout Settlement - ---- - -`payout-settlement` is a transaction which spends funds from from latest `poolkey` which was used as output in `payout-update` transaction. - -```rust -pub struct PayoutSettlement { - transaction: Transaction, - fee: u64 -} - -impl PayoutSettlement { - pub fn new( - latest_eltoo_out_address: Address, - payout_map: HashMap, - ) -> Self { - let mut tx = Transaction::new(); - - let fee = avg_fee.now(24); - - tx.input.extend(TxIn { - outpoint : latest_eltoo_out_address, - vout: 0 - }) - - payout_map.iter().for_each(|address, amount| { - tx.output.extend({ - TxOut { - script : ScriptBuf::from(Address), - amount: amount - } - }) - }); - - PayoutSettlement { - transaction: tx, - fee: fee - } - } - - pub fn add_sig(&mut self, private_key: &SecretKey) -> Result<(), Error> { - // follows same pseudo code as earlier payyout-update struct. - } - - pub fn build(self) Transaction { - self.transaction - } -} -``` - -### Implementation Code for Payout Settlement Struct - ---- - -```rust - -// Create a random previous output transaction (prev_update_tx) -let prev_update_tx = Transaction { - version: 2, - lock_time: 0, - input: vec![TxIn { - previous_output: OutPoint::from("prev_update_out"), - sequence: 0xFFFFFFFF, - witness: vec![], - }], - output: vec![TxOut { - value: 100 * 100_000_000, // 100 BTC - script_pubkey: TAPROOT_KEY, - }], -}; - -let (_, latest_eltoo_out_address) = generate_random_pubkey(); - -let payout_map = HashMap
::new([ - (ALICE , 20), - (BOB , 30), - (CHARLIE , 10), - (DAVID , 40), - ]); - -let payout_settlement = PayoutSettlement::new(latest_eltoo_out_address, payout_map); - -// Generate random private keys -let secp = Secp256k1::new(); -let privkey1 = SecretKey::new(&mut rng); - -// Add signatures with random private keys -payout_settlement.add_sig(&privkey1).unwrap(); - -// Build and print the transaction -let final_tx = payout_settlement.build(); -``` - -## Dependencies - -- Rust-bitcoin crate -- zmq and other networking crates. - -## Testing and Documentation - -- Unit tests for individual modules and integration tests for the overall system. - - includes spining up local regtest node and executing a chain of transaction flows. -- Example usages in the `examples/` directory - -## Expected File Structure - -```rust -rust-uhpo/ -├── src/ -│ ├── lib.rs -│ ├── transaction/ -│ │ ├── mod.rs -│ │ ├── payout_update.rs -│ │ ├── payout_settlement.rs -│ ├── network/ -│ └── utils/ -│ ├── mod.rs -│ └── ... (utility modules) -├── tests/ -│ └── ... (integration tests) -├── examples/ -│ └── ... (example usages) -├── Cargo.toml -└── README.md -``` \ No newline at end of file diff --git a/node/uhpo/src/error.rs b/node/uhpo/src/error.rs index 4153b8b..a683845 100644 --- a/node/uhpo/src/error.rs +++ b/node/uhpo/src/error.rs @@ -40,7 +40,7 @@ impl fmt::Display for UhpoError { impl std::error::Error for UhpoError {} impl UhpoError { - pub fn new_other_error(message: &str) -> Self { + pub fn new(message: &str) -> Self { UhpoError::Other(message.to_string()) } } diff --git a/node/uhpo/src/payout_update.rs b/node/uhpo/src/payout_update.rs index e2899df..371a1a8 100644 --- a/node/uhpo/src/payout_update.rs +++ b/node/uhpo/src/payout_update.rs @@ -13,29 +13,29 @@ use bitcoin::{ use crate::error::UhpoError; -pub struct PayoutUpdate<'a> { +pub struct PayoutUpdate { transaction: Transaction, - coinbase_txout: &'a TxOut, - prev_update_txout: Option<&'a TxOut>, + coinbase_txout: TxOut, + prev_update_txout: Option, } -impl<'a> PayoutUpdate<'a> { +impl PayoutUpdate { pub fn new( - prev_update_tx: Option<&'a Transaction>, - coinbase_tx: &'a Transaction, + prev_update_tx: Option, + coinbase_tx: Transaction, next_out_address: Address, projected_fee: Amount, ) -> Result { - let prev_update_txout = prev_update_tx.map(|tx| &tx.output[0]); - let coinbase_txout = &coinbase_tx.output[0]; + let prev_update_txout = prev_update_tx.as_ref().map(|tx| tx.output[0].clone()); + let coinbase_txout = coinbase_tx.output[0].clone(); let payout_update_tx = build_transaction( - coinbase_tx, - prev_update_tx, + &coinbase_tx, + prev_update_tx.as_ref(), next_out_address, projected_fee, - coinbase_txout, - prev_update_txout, + &coinbase_txout, + prev_update_txout.as_ref(), )?; Ok(PayoutUpdate { @@ -50,9 +50,9 @@ impl<'a> PayoutUpdate<'a> { private_key: &SecretKey, tweak: &Option, ) -> Result<(), UhpoError> { - let prevouts = match self.prev_update_txout { - Some(prev_update_txout) => vec![self.coinbase_txout, prev_update_txout], - None => vec![self.coinbase_txout], + let prevouts = match &self.prev_update_txout { + Some(prev_update_txout) => vec![&self.coinbase_txout, prev_update_txout], + None => vec![&self.coinbase_txout], }; add_signature(&mut self.transaction, 0, &prevouts, private_key, tweak) @@ -63,9 +63,12 @@ impl<'a> PayoutUpdate<'a> { private_key: &SecretKey, tweak: &Option, ) -> Result<(), UhpoError> { - let prev_update_txout = self.prev_update_txout.ok_or(UhpoError::NoPrevUpdateTxOut)?; - let prevouts = vec![self.coinbase_txout, prev_update_txout]; - + let prev_update_txout = self + .prev_update_txout + .as_ref() + .ok_or(UhpoError::NoPrevUpdateTxOut)?; + let prevouts = vec![&self.coinbase_txout, prev_update_txout]; + add_signature(&mut self.transaction, 1, &prevouts, private_key, tweak) } @@ -190,7 +193,7 @@ mod tests { let payout_update = PayoutUpdate::new( prev_update_tx, - &coinbase_tx, + coinbase_tx, new_payout_address, projected_fee, ) @@ -214,8 +217,8 @@ mod tests { let projected_fee = Amount::from_sat(1000); let payout_update = PayoutUpdate::new( - Some(&prev_update_tx), - &coinbase_tx, + Some(prev_update_tx), + coinbase_tx, new_payout_address, projected_fee, ) @@ -238,8 +241,8 @@ mod tests { let prev_update_tx = create_dummy_transaction(); let projected_fee = Amount::from_sat(1000); let mut payout_update = PayoutUpdate::new( - Some(&prev_update_tx), - &coinbase_tx, + Some(prev_update_tx), + coinbase_tx, new_payout_address, projected_fee, ) From 8f3fc21af71a95927a005889997794c066362d65 Mon Sep 17 00:00:00 2001 From: jayendramadaram Date: Mon, 8 Jul 2024 14:24:42 +0530 Subject: [PATCH 4/9] feat/add new tx builder --- .gitignore | 3 +- node/Cargo.lock | 2 + node/uhpo/Cargo.toml | 4 + node/uhpo/src/error.rs | 8 +- node/uhpo/src/lib.rs | 5 + node/uhpo/src/payout_update.rs | 203 ++++++++++++++------------- node/uhpo/src/transaction/builder.rs | 42 ++++++ node/uhpo/src/transaction/mod.rs | 3 + 8 files changed, 167 insertions(+), 103 deletions(-) create mode 100644 node/uhpo/src/transaction/builder.rs create mode 100644 node/uhpo/src/transaction/mod.rs diff --git a/.gitignore b/.gitignore index a085767..16b8da7 100644 --- a/.gitignore +++ b/.gitignore @@ -14,5 +14,4 @@ proposal/proposal.log proposal/proposal.bbl proposal/proposal.blg simulations/logs/* -simulations/*.toolbox -venv \ No newline at end of file +simulations/*.toolbox \ No newline at end of file diff --git a/node/Cargo.lock b/node/Cargo.lock index ddda211..4805ac3 100644 --- a/node/Cargo.lock +++ b/node/Cargo.lock @@ -1443,7 +1443,9 @@ name = "uhpo" version = "0.1.0" dependencies = [ "bitcoin", + "mockall", "rand", + "secp256k1", ] [[package]] diff --git a/node/uhpo/Cargo.toml b/node/uhpo/Cargo.toml index 41dfdc5..82b3e9e 100644 --- a/node/uhpo/Cargo.toml +++ b/node/uhpo/Cargo.toml @@ -8,3 +8,7 @@ edition = "2021" [dependencies] bitcoin = {version = "0.32.2" , features = ["rand"] } rand = "0.8.5" + +[dev-dependencies] +mockall = "0.12.1" +secp256k1 = "0.29.0" \ No newline at end of file diff --git a/node/uhpo/src/error.rs b/node/uhpo/src/error.rs index a683845..2550718 100644 --- a/node/uhpo/src/error.rs +++ b/node/uhpo/src/error.rs @@ -4,8 +4,9 @@ use bitcoin::sighash::TaprootError; #[derive(Debug)] pub enum UhpoError { + KeypairCreationError(bitcoin::secp256k1::Error), + SignatureVerificationError(bitcoin::secp256k1::Error), TaprootError(TaprootError), - Secp256k1Error(bitcoin::secp256k1::Error), NoPrevUpdateTxOut, Other(String), } @@ -18,15 +19,16 @@ impl From for UhpoError { impl From for UhpoError { fn from(err: bitcoin::secp256k1::Error) -> Self { - UhpoError::Secp256k1Error(err) + UhpoError::KeypairCreationError(err) } } impl fmt::Display for UhpoError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + UhpoError::KeypairCreationError(err) => write!(f, "Keypair creation error: {}", err), UhpoError::TaprootError(err) => write!(f, "Taproot error: {}", err), - UhpoError::Secp256k1Error(err) => { + UhpoError::SignatureVerificationError(err) => { write!(f, "Signature verification error: {}", err) } UhpoError::NoPrevUpdateTxOut => { diff --git a/node/uhpo/src/lib.rs b/node/uhpo/src/lib.rs index ffb5321..a25bb18 100644 --- a/node/uhpo/src/lib.rs +++ b/node/uhpo/src/lib.rs @@ -1,3 +1,8 @@ pub mod error; pub mod payout_settlement; pub mod payout_update; +mod transaction; + +pub use error::UhpoError; +pub use payout_settlement::PayoutSettlement; +pub use payout_update::PayoutUpdate; diff --git a/node/uhpo/src/payout_update.rs b/node/uhpo/src/payout_update.rs index 371a1a8..39d0f66 100644 --- a/node/uhpo/src/payout_update.rs +++ b/node/uhpo/src/payout_update.rs @@ -1,24 +1,25 @@ use std::fmt::Error; use bitcoin::{ - absolute::LockTime, hashes::Hash, key::{Keypair, Secp256k1}, - secp256k1::{Message, Scalar, SecretKey}, + secp256k1::{Message, Scalar, SecretKey , All}, sighash::{Prevouts, SighashCache}, - transaction::Version, - Address, Amount, OutPoint, ScriptBuf, Sequence, TapSighashType, Transaction, TxIn, TxOut, - Witness, + Address, Amount, TapSighashType, Transaction, TxOut, }; -use crate::error::UhpoError; +use crate::{transaction::TransactionBuilder, UhpoError}; +/// `PayoutUpdate` represents an update to a Eltoo style payout. pub struct PayoutUpdate { transaction: Transaction, + + // coinbase and prev_update txout's are to be store and used while signing respective inputs coinbase_txout: TxOut, prev_update_txout: Option, } + impl PayoutUpdate { pub fn new( prev_update_tx: Option, @@ -26,20 +27,30 @@ impl PayoutUpdate { next_out_address: Address, projected_fee: Amount, ) -> Result { - let prev_update_txout = prev_update_tx.as_ref().map(|tx| tx.output[0].clone()); let coinbase_txout = coinbase_tx.output[0].clone(); - let payout_update_tx = build_transaction( - &coinbase_tx, - prev_update_tx.as_ref(), - next_out_address, - projected_fee, - &coinbase_txout, - prev_update_txout.as_ref(), - )?; + // coinbase created by implementation crate would always have spending vout set to 0 + let mut builder = TransactionBuilder::new().add_input(coinbase_tx.compute_txid(), 0); + + let prev_update_txout = if let Some(tx) = prev_update_tx { + builder = builder.add_input(tx.compute_txid(), 0); + Some(tx.output[0].clone()) + } else { + None + }; + + let total_amount = prev_update_txout + .as_ref() + .map_or(coinbase_txout.value, |txout| { + coinbase_txout.value + txout.value + }); + + let transaction = builder + .add_output(next_out_address, total_amount - projected_fee) + .build(); Ok(PayoutUpdate { - transaction: payout_update_tx, + transaction, coinbase_txout, prev_update_txout, }) @@ -47,21 +58,23 @@ impl PayoutUpdate { pub fn add_coinbase_sig( &mut self, - private_key: &SecretKey, - tweak: &Option, + private_key: SecretKey, + tweak: Option<&Scalar>, + secp: &Secp256k1, ) -> Result<(), UhpoError> { let prevouts = match &self.prev_update_txout { Some(prev_update_txout) => vec![&self.coinbase_txout, prev_update_txout], None => vec![&self.coinbase_txout], }; - add_signature(&mut self.transaction, 0, &prevouts, private_key, tweak) + add_signature(&mut self.transaction, 0, &prevouts, private_key, tweak , secp) } pub fn add_prev_update_sig( &mut self, - private_key: &SecretKey, - tweak: &Option, + private_key: SecretKey, + tweak: Option<&Scalar>, + secp: &Secp256k1, ) -> Result<(), UhpoError> { let prev_update_txout = self .prev_update_txout @@ -69,7 +82,7 @@ impl PayoutUpdate { .ok_or(UhpoError::NoPrevUpdateTxOut)?; let prevouts = vec![&self.coinbase_txout, prev_update_txout]; - add_signature(&mut self.transaction, 1, &prevouts, private_key, tweak) + add_signature(&mut self.transaction, 1, &prevouts, private_key, tweak , secp) } pub fn build(self) -> Transaction { @@ -77,72 +90,24 @@ impl PayoutUpdate { } } -fn build_transaction( - coinbase_tx: &Transaction, - prev_update_tx: Option<&Transaction>, - next_out_address: Address, - projected_fee: Amount, - coinbase_txout: &TxOut, - prev_update_txout: Option<&TxOut>, -) -> Result { - let mut total_amount = coinbase_txout.value; - if let Some(tx_out) = prev_update_txout { - total_amount += tx_out.value; - } - - let mut payout_update_tx = Transaction { - version: Version::TWO, - lock_time: LockTime::ZERO, - input: vec![], - output: vec![], - }; - - payout_update_tx.input.push(TxIn { - previous_output: OutPoint { - txid: coinbase_tx.compute_txid(), - vout: 0, - }, - script_sig: ScriptBuf::new(), - sequence: Sequence::MAX, - witness: Witness::default(), - }); - - if let Some(tx) = prev_update_tx { - payout_update_tx.input.push(TxIn { - previous_output: OutPoint { - txid: tx.compute_txid(), - vout: 0, - }, - script_sig: ScriptBuf::new(), - sequence: Sequence::MAX, - witness: Witness::default(), - }); - } - - payout_update_tx.output.push(TxOut { - value: total_amount - projected_fee, - script_pubkey: next_out_address.script_pubkey(), - }); - - Ok(payout_update_tx) -} - fn add_signature( transaction: &mut Transaction, input_idx: usize, prevouts: &[&TxOut], - private_key: &SecretKey, - tweak: &Option, + private_key: SecretKey, + tweak: Option<&Scalar>, + secp: &Secp256k1, ) -> Result<(), UhpoError> { - let secp = Secp256k1::new(); let keypair: Keypair = match tweak { - Some(tweak) => private_key.keypair(&secp).add_xonly_tweak(&secp, tweak)?, - None => private_key.keypair(&secp), + Some(tweak) => private_key + .keypair(secp) + .add_xonly_tweak(secp, tweak) + .map_err(UhpoError::KeypairCreationError)?, + None => private_key.keypair(secp), }; let mut sighash_cache = SighashCache::new(transaction.clone()); - let sighash = sighash_cache.taproot_key_spend_signature_hash( input_idx, &Prevouts::All(prevouts), @@ -155,7 +120,8 @@ fn add_signature( let mut vec_sig = signature.serialize().to_vec(); vec_sig.push(0x01); - secp.verify_schnorr(&signature, &message, &keypair.x_only_public_key().0)?; + secp.verify_schnorr(&signature, &message, &keypair.x_only_public_key().0) + .map_err(UhpoError::SignatureVerificationError)?; transaction.input[input_idx].witness.push(vec_sig); @@ -165,12 +131,13 @@ fn add_signature( // unit tests #[cfg(test)] mod tests { - use bitcoin::{key::Keypair, secp256k1::All, Network}; - use rand::Rng; - use super::*; + use bitcoin::{ + absolute::LockTime, key::Keypair, secp256k1::All, transaction::Version, Network, ScriptBuf, + }; + use rand::Rng; - fn setup() -> (Secp256k1, Keypair, Address) { + pub fn setup() -> (Secp256k1, Keypair, Address) { let secp = Secp256k1::new(); let mut rng = rand::thread_rng(); @@ -183,6 +150,19 @@ mod tests { (secp, keypair, new_payout_address) } + pub fn create_dummy_transaction() -> Transaction { + let tx = Transaction { + version: Version::TWO, + lock_time: LockTime::ZERO, + input: vec![], + output: vec![TxOut { + value: Amount::from_sat(50000000), + script_pubkey: ScriptBuf::new(), + }], + }; + tx + } + #[test] fn test_new_payout_update_only_cb() { let (_, _, new_payout_address) = setup(); @@ -234,8 +214,8 @@ mod tests { } #[test] - fn test_add_signature() { - let (_, keypair, new_payout_address) = setup(); + fn test_add_signature_with_no_tweak() { + let (secp, keypair, new_payout_address) = setup(); let coinbase_tx = create_dummy_transaction(); let prev_update_tx = create_dummy_transaction(); @@ -249,27 +229,54 @@ mod tests { .expect("Failed to create payout update"); payout_update - .add_coinbase_sig(&keypair.secret_key(), &None) + .add_coinbase_sig(keypair.secret_key(), None , &secp) .expect("Failed to add coinbase signature"); payout_update - .add_prev_update_sig(&keypair.secret_key(), &None) + .add_prev_update_sig(keypair.secret_key(), None , &secp) .expect("Failed to add prev update signature"); assert_eq!(payout_update.transaction.input[0].witness.len(), 1); assert_eq!(payout_update.transaction.input[1].witness.len(), 1); } - fn create_dummy_transaction() -> Transaction { - let tx = Transaction { - version: Version::TWO, - lock_time: LockTime::ZERO, - input: vec![], - output: vec![TxOut { - value: Amount::from_sat(50000000), - script_pubkey: ScriptBuf::new(), - }], - }; - tx + #[test] + fn test_add_signature_with_tweak() { + let (secp, keypair, new_payout_address) = setup(); + + let coinbase_tx = create_dummy_transaction(); + let prev_update_tx = create_dummy_transaction(); + let projected_fee = Amount::from_sat(1000); + let mut payout_update = PayoutUpdate::new( + Some(prev_update_tx), + coinbase_tx, + new_payout_address, + projected_fee, + ) + .expect("Failed to create payout update"); + + payout_update + .add_coinbase_sig( + keypair.secret_key(), + Some(&Scalar::random_custom(&mut rand::thread_rng())), + &secp + ) + .expect("Failed to add coinbase signature"); + + payout_update + .add_prev_update_sig( + keypair.secret_key(), + Some(&Scalar::random_custom(&mut rand::thread_rng())), + &secp + ) + .expect("Failed to add prev update signature"); + + assert_eq!(payout_update.transaction.input[0].witness.len(), 1); + assert_eq!(payout_update.transaction.input[1].witness.len(), 1); } } + +#[cfg(test)] +mod mock_tests { + // unimplemented +} \ No newline at end of file diff --git a/node/uhpo/src/transaction/builder.rs b/node/uhpo/src/transaction/builder.rs new file mode 100644 index 0000000..1c11841 --- /dev/null +++ b/node/uhpo/src/transaction/builder.rs @@ -0,0 +1,42 @@ +use bitcoin::{ + absolute::LockTime, transaction::Version, Address, Amount, OutPoint, ScriptBuf, Sequence, + Transaction, TxIn, TxOut, Txid, Witness, +}; +pub struct TransactionBuilder { + transaction: Transaction, +} + +impl TransactionBuilder { + pub fn new() -> Self { + Self { + transaction: Transaction { + version: Version::TWO, + lock_time: LockTime::ZERO, + input: vec![], + output: vec![], + }, + } + } + + pub fn add_input(mut self, txid: Txid, vout: u32) -> Self { + self.transaction.input.push(TxIn { + previous_output: OutPoint { txid, vout }, + script_sig: ScriptBuf::new(), + sequence: Sequence::MAX, + witness: Witness::default(), + }); + self + } + + pub fn add_output(mut self, address: Address, amount: Amount) -> Self { + self.transaction.output.push(TxOut { + value: amount, + script_pubkey: address.script_pubkey(), + }); + self + } + + pub fn build(self) -> Transaction { + self.transaction + } +} diff --git a/node/uhpo/src/transaction/mod.rs b/node/uhpo/src/transaction/mod.rs new file mode 100644 index 0000000..fe906f9 --- /dev/null +++ b/node/uhpo/src/transaction/mod.rs @@ -0,0 +1,3 @@ +mod builder; + +pub use builder::TransactionBuilder; From 9a26b2a6df4381c8da9c8a7f903dde18f627724a Mon Sep 17 00:00:00 2001 From: jayendramadaram Date: Mon, 8 Jul 2024 21:57:34 +0530 Subject: [PATCH 5/9] test/add mock test for key creation --- node/uhpo/Cargo.toml | 7 +- node/uhpo/src/payout_update.rs | 167 +++++++++++++++++++++++++++++---- 2 files changed, 152 insertions(+), 22 deletions(-) diff --git a/node/uhpo/Cargo.toml b/node/uhpo/Cargo.toml index 82b3e9e..2f23127 100644 --- a/node/uhpo/Cargo.toml +++ b/node/uhpo/Cargo.toml @@ -8,7 +8,8 @@ edition = "2021" [dependencies] bitcoin = {version = "0.32.2" , features = ["rand"] } rand = "0.8.5" - -[dev-dependencies] mockall = "0.12.1" -secp256k1 = "0.29.0" \ No newline at end of file +secp256k1 = "0.29.0" + + +[dev-dependencies] \ No newline at end of file diff --git a/node/uhpo/src/payout_update.rs b/node/uhpo/src/payout_update.rs index 39d0f66..91de817 100644 --- a/node/uhpo/src/payout_update.rs +++ b/node/uhpo/src/payout_update.rs @@ -3,10 +3,13 @@ use std::fmt::Error; use bitcoin::{ hashes::Hash, key::{Keypair, Secp256k1}, - secp256k1::{Message, Scalar, SecretKey , All}, + secp256k1::{All, Message, Scalar, SecretKey, Verification}, sighash::{Prevouts, SighashCache}, - Address, Amount, TapSighashType, Transaction, TxOut, + Address, Amount, TapSighashType, Transaction, TxOut, XOnlyPublicKey, }; +use mockall::automock; +use rand::{CryptoRng, Rng}; +use secp256k1::schnorr::Signature; use crate::{transaction::TransactionBuilder, UhpoError}; @@ -19,7 +22,6 @@ pub struct PayoutUpdate { prev_update_txout: Option, } - impl PayoutUpdate { pub fn new( prev_update_tx: Option, @@ -67,7 +69,14 @@ impl PayoutUpdate { None => vec![&self.coinbase_txout], }; - add_signature(&mut self.transaction, 0, &prevouts, private_key, tweak , secp) + add_signature( + &mut self.transaction, + 0, + &prevouts, + private_key, + tweak, + secp, + ) } pub fn add_prev_update_sig( @@ -82,7 +91,14 @@ impl PayoutUpdate { .ok_or(UhpoError::NoPrevUpdateTxOut)?; let prevouts = vec![&self.coinbase_txout, prev_update_txout]; - add_signature(&mut self.transaction, 1, &prevouts, private_key, tweak , secp) + add_signature( + &mut self.transaction, + 1, + &prevouts, + private_key, + tweak, + secp, + ) } pub fn build(self) -> Transaction { @@ -90,21 +106,97 @@ impl PayoutUpdate { } } -fn add_signature( +#[automock] +pub trait SecretKeyBehavior { + fn keypair(&self, secp: &Secp256k1) -> K; +} + +impl SecretKeyBehavior for SecretKey { + fn keypair(&self, secp: &Secp256k1) -> Keypair { + self.keypair(secp) + } +} + +#[automock] +pub trait KeypairBehavior { + fn add_xonly_tweak( + self, + secp: &Secp256k1, + tweak: &Scalar, + ) -> Result; + + fn to_keypair(&self) -> Keypair; +} + +impl KeypairBehavior for Keypair { + fn add_xonly_tweak( + self, + secp: &Secp256k1, + tweak: &Scalar, + ) -> Result { + self.add_xonly_tweak(secp, tweak) + } + + fn to_keypair(&self) -> Keypair { + self.clone() + } +} + +#[automock] +pub trait Secp256k1Behavior { + fn sign_schnorr_with_rng( + &self, + msg: &Message, + keypair: &Keypair, + rng: &mut R, + ) -> Signature; + + fn verify_schnorr( + &self, + signature: &Signature, + message: &Message, + pubkey: &XOnlyPublicKey, + ) -> Result<(), bitcoin::secp256k1::Error>; +} + +impl Secp256k1Behavior for Secp256k1 { + fn sign_schnorr_with_rng( + &self, + msg: &Message, + keypair: &Keypair, + rng: &mut R, + ) -> Signature { + self.sign_schnorr_with_rng(msg, keypair, rng) + } + + fn verify_schnorr( + &self, + signature: &Signature, + message: &Message, + pubkey: &XOnlyPublicKey, + ) -> Result<(), bitcoin::secp256k1::Error> { + self.verify_schnorr(signature, message, pubkey) + } +} + +fn add_signature( transaction: &mut Transaction, input_idx: usize, prevouts: &[&TxOut], - private_key: SecretKey, + private_key: S, tweak: Option<&Scalar>, secp: &Secp256k1, -) -> Result<(), UhpoError> { - +) -> Result<(), UhpoError> +where + S: SecretKeyBehavior, + K: KeypairBehavior, +{ let keypair: Keypair = match tweak { Some(tweak) => private_key .keypair(secp) .add_xonly_tweak(secp, tweak) .map_err(UhpoError::KeypairCreationError)?, - None => private_key.keypair(secp), + None => private_key.keypair(secp).to_keypair(), }; let mut sighash_cache = SighashCache::new(transaction.clone()); @@ -151,7 +243,7 @@ mod tests { } pub fn create_dummy_transaction() -> Transaction { - let tx = Transaction { + Transaction { version: Version::TWO, lock_time: LockTime::ZERO, input: vec![], @@ -159,8 +251,7 @@ mod tests { value: Amount::from_sat(50000000), script_pubkey: ScriptBuf::new(), }], - }; - tx + } } #[test] @@ -229,11 +320,11 @@ mod tests { .expect("Failed to create payout update"); payout_update - .add_coinbase_sig(keypair.secret_key(), None , &secp) + .add_coinbase_sig(keypair.secret_key(), None, &secp) .expect("Failed to add coinbase signature"); payout_update - .add_prev_update_sig(keypair.secret_key(), None , &secp) + .add_prev_update_sig(keypair.secret_key(), None, &secp) .expect("Failed to add prev update signature"); assert_eq!(payout_update.transaction.input[0].witness.len(), 1); @@ -259,7 +350,7 @@ mod tests { .add_coinbase_sig( keypair.secret_key(), Some(&Scalar::random_custom(&mut rand::thread_rng())), - &secp + &secp, ) .expect("Failed to add coinbase signature"); @@ -267,7 +358,7 @@ mod tests { .add_prev_update_sig( keypair.secret_key(), Some(&Scalar::random_custom(&mut rand::thread_rng())), - &secp + &secp, ) .expect("Failed to add prev update signature"); @@ -278,5 +369,43 @@ mod tests { #[cfg(test)] mod mock_tests { - // unimplemented -} \ No newline at end of file + use super::*; + use bitcoin::{ScriptBuf, TxIn}; + use tests::create_dummy_transaction; + + #[test] + fn test_add_signatures_key_creation_fails() { + let mut mock_secret_key = MockSecretKeyBehavior::::new(); + let mut mock_keypair = MockKeypairBehavior::new(); + + mock_keypair + .expect_add_xonly_tweak() + .return_once(|_: &Secp256k1, _| Err(secp256k1::Error::InvalidTweak)); + + mock_secret_key + .expect_keypair() + .return_once(move |_| mock_keypair); + + let mut transaction = create_dummy_transaction(); + transaction.input.push(TxIn { + ..Default::default() + }); + let prevout = TxOut { + value: Amount::from_sat(0), + script_pubkey: ScriptBuf::new(), + }; + let tweak = Some(Scalar::random_custom(&mut rand::thread_rng())); + let secp = Secp256k1::new(); + + let result = add_signature( + &mut transaction, + 0, + &[&prevout], + mock_secret_key, + tweak.as_ref(), + &secp, + ); + + assert!(matches!(result, Err(UhpoError::KeypairCreationError(_)))); + } +} From 581a2d17b50c6b6ea4f2b946e14576f1459736f1 Mon Sep 17 00:00:00 2001 From: jayendramadaram Date: Tue, 9 Jul 2024 16:52:29 +0530 Subject: [PATCH 6/9] test/add sig verification mock tests and Doc comments --- node/uhpo/Cargo.toml | 4 +- node/uhpo/src/lib.rs | 3 +- node/uhpo/src/payout_update.rs | 213 ++++++--------------------- node/uhpo/src/transaction/builder.rs | 32 +++- 4 files changed, 75 insertions(+), 177 deletions(-) diff --git a/node/uhpo/Cargo.toml b/node/uhpo/Cargo.toml index 2f23127..488d784 100644 --- a/node/uhpo/Cargo.toml +++ b/node/uhpo/Cargo.toml @@ -9,7 +9,7 @@ edition = "2021" bitcoin = {version = "0.32.2" , features = ["rand"] } rand = "0.8.5" mockall = "0.12.1" -secp256k1 = "0.29.0" -[dev-dependencies] \ No newline at end of file +[dev-dependencies] +secp256k1 = "0.29.0" \ No newline at end of file diff --git a/node/uhpo/src/lib.rs b/node/uhpo/src/lib.rs index a25bb18..158cf7b 100644 --- a/node/uhpo/src/lib.rs +++ b/node/uhpo/src/lib.rs @@ -1,7 +1,8 @@ +pub mod crypto; pub mod error; pub mod payout_settlement; pub mod payout_update; -mod transaction; +pub mod transaction; pub use error::UhpoError; pub use payout_settlement::PayoutSettlement; diff --git a/node/uhpo/src/payout_update.rs b/node/uhpo/src/payout_update.rs index 91de817..bf140f5 100644 --- a/node/uhpo/src/payout_update.rs +++ b/node/uhpo/src/payout_update.rs @@ -1,16 +1,12 @@ use std::fmt::Error; use bitcoin::{ - hashes::Hash, - key::{Keypair, Secp256k1}, - secp256k1::{All, Message, Scalar, SecretKey, Verification}, - sighash::{Prevouts, SighashCache}, - Address, Amount, TapSighashType, Transaction, TxOut, XOnlyPublicKey, + key::Secp256k1, + secp256k1::{All, Scalar, SecretKey}, + Address, Amount, Transaction, TxOut, }; -use mockall::automock; -use rand::{CryptoRng, Rng}; -use secp256k1::schnorr::Signature; +use crate::crypto::signature::add_signature; use crate::{transaction::TransactionBuilder, UhpoError}; /// `PayoutUpdate` represents an update to a Eltoo style payout. @@ -23,6 +19,16 @@ pub struct PayoutUpdate { } impl PayoutUpdate { + /// Creates a new PayoutUpdate instance. + /// + /// # Arguments + /// * `prev_update_tx` - Optional previous update transaction + /// * `coinbase_tx` - The coinbase transaction + /// * `next_out_address` - The address for the next output + /// * `projected_fee` - The projected transaction fee + /// + /// # Returns + /// Result containing a new PayoutUpdate instance or an Error pub fn new( prev_update_tx: Option, coinbase_tx: Transaction, @@ -34,6 +40,8 @@ impl PayoutUpdate { // coinbase created by implementation crate would always have spending vout set to 0 let mut builder = TransactionBuilder::new().add_input(coinbase_tx.compute_txid(), 0); + // Add previous update transaction input if it exists + // previous update tx would be None if and only if this is the first update let prev_update_txout = if let Some(tx) = prev_update_tx { builder = builder.add_input(tx.compute_txid(), 0); Some(tx.output[0].clone()) @@ -41,11 +49,11 @@ impl PayoutUpdate { None }; - let total_amount = prev_update_txout - .as_ref() - .map_or(coinbase_txout.value, |txout| { - coinbase_txout.value + txout.value - }); + // Calculate total amount from coinbase and previous update + let total_amount = prev_update_txout.as_ref().map_or_else( + || coinbase_txout.value, + |txout| coinbase_txout.value + txout.value, + ); let transaction = builder .add_output(next_out_address, total_amount - projected_fee) @@ -58,6 +66,15 @@ impl PayoutUpdate { }) } + /// Adds a signature for the coinbase input. + /// + /// # Arguments + /// * `private_key` - The private key for signing + /// * `tweak` - Optional tweak for the private key + /// * `secp` - The Secp256k1 context + /// + /// # Returns + /// Result indicating success or an UhpoError pub fn add_coinbase_sig( &mut self, private_key: SecretKey, @@ -71,7 +88,7 @@ impl PayoutUpdate { add_signature( &mut self.transaction, - 0, + 0, // coinbase input idx &prevouts, private_key, tweak, @@ -79,6 +96,15 @@ impl PayoutUpdate { ) } + /// Adds a signature for the previous update input. + /// + /// # Arguments + /// * `private_key` - The private key for signing + /// * `tweak` - Optional tweak for the private key + /// * `secp` - The Secp256k1 context + /// + /// # Returns + /// Result indicating success or an UhpoError pub fn add_prev_update_sig( &mut self, private_key: SecretKey, @@ -106,120 +132,6 @@ impl PayoutUpdate { } } -#[automock] -pub trait SecretKeyBehavior { - fn keypair(&self, secp: &Secp256k1) -> K; -} - -impl SecretKeyBehavior for SecretKey { - fn keypair(&self, secp: &Secp256k1) -> Keypair { - self.keypair(secp) - } -} - -#[automock] -pub trait KeypairBehavior { - fn add_xonly_tweak( - self, - secp: &Secp256k1, - tweak: &Scalar, - ) -> Result; - - fn to_keypair(&self) -> Keypair; -} - -impl KeypairBehavior for Keypair { - fn add_xonly_tweak( - self, - secp: &Secp256k1, - tweak: &Scalar, - ) -> Result { - self.add_xonly_tweak(secp, tweak) - } - - fn to_keypair(&self) -> Keypair { - self.clone() - } -} - -#[automock] -pub trait Secp256k1Behavior { - fn sign_schnorr_with_rng( - &self, - msg: &Message, - keypair: &Keypair, - rng: &mut R, - ) -> Signature; - - fn verify_schnorr( - &self, - signature: &Signature, - message: &Message, - pubkey: &XOnlyPublicKey, - ) -> Result<(), bitcoin::secp256k1::Error>; -} - -impl Secp256k1Behavior for Secp256k1 { - fn sign_schnorr_with_rng( - &self, - msg: &Message, - keypair: &Keypair, - rng: &mut R, - ) -> Signature { - self.sign_schnorr_with_rng(msg, keypair, rng) - } - - fn verify_schnorr( - &self, - signature: &Signature, - message: &Message, - pubkey: &XOnlyPublicKey, - ) -> Result<(), bitcoin::secp256k1::Error> { - self.verify_schnorr(signature, message, pubkey) - } -} - -fn add_signature( - transaction: &mut Transaction, - input_idx: usize, - prevouts: &[&TxOut], - private_key: S, - tweak: Option<&Scalar>, - secp: &Secp256k1, -) -> Result<(), UhpoError> -where - S: SecretKeyBehavior, - K: KeypairBehavior, -{ - let keypair: Keypair = match tweak { - Some(tweak) => private_key - .keypair(secp) - .add_xonly_tweak(secp, tweak) - .map_err(UhpoError::KeypairCreationError)?, - None => private_key.keypair(secp).to_keypair(), - }; - - let mut sighash_cache = SighashCache::new(transaction.clone()); - let sighash = sighash_cache.taproot_key_spend_signature_hash( - input_idx, - &Prevouts::All(prevouts), - TapSighashType::All, - )?; - - let message = Message::from_digest(sighash.as_raw_hash().to_byte_array()); - - let signature = secp.sign_schnorr_with_rng(&message, &keypair, &mut rand::thread_rng()); - let mut vec_sig = signature.serialize().to_vec(); - vec_sig.push(0x01); - - secp.verify_schnorr(&signature, &message, &keypair.x_only_public_key().0) - .map_err(UhpoError::SignatureVerificationError)?; - - transaction.input[input_idx].witness.push(vec_sig); - - Ok(()) -} - // unit tests #[cfg(test)] mod tests { @@ -229,6 +141,8 @@ mod tests { }; use rand::Rng; + // Helper function for setting up tests + pub fn setup() -> (Secp256k1, Keypair, Address) { let secp = Secp256k1::new(); let mut rng = rand::thread_rng(); @@ -366,46 +280,3 @@ mod tests { assert_eq!(payout_update.transaction.input[1].witness.len(), 1); } } - -#[cfg(test)] -mod mock_tests { - use super::*; - use bitcoin::{ScriptBuf, TxIn}; - use tests::create_dummy_transaction; - - #[test] - fn test_add_signatures_key_creation_fails() { - let mut mock_secret_key = MockSecretKeyBehavior::::new(); - let mut mock_keypair = MockKeypairBehavior::new(); - - mock_keypair - .expect_add_xonly_tweak() - .return_once(|_: &Secp256k1, _| Err(secp256k1::Error::InvalidTweak)); - - mock_secret_key - .expect_keypair() - .return_once(move |_| mock_keypair); - - let mut transaction = create_dummy_transaction(); - transaction.input.push(TxIn { - ..Default::default() - }); - let prevout = TxOut { - value: Amount::from_sat(0), - script_pubkey: ScriptBuf::new(), - }; - let tweak = Some(Scalar::random_custom(&mut rand::thread_rng())); - let secp = Secp256k1::new(); - - let result = add_signature( - &mut transaction, - 0, - &[&prevout], - mock_secret_key, - tweak.as_ref(), - &secp, - ); - - assert!(matches!(result, Err(UhpoError::KeypairCreationError(_)))); - } -} diff --git a/node/uhpo/src/transaction/builder.rs b/node/uhpo/src/transaction/builder.rs index 1c11841..3883ed6 100644 --- a/node/uhpo/src/transaction/builder.rs +++ b/node/uhpo/src/transaction/builder.rs @@ -2,11 +2,17 @@ use bitcoin::{ absolute::LockTime, transaction::Version, Address, Amount, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, Witness, }; +/// A builder for constructing Bitcoin transactions. pub struct TransactionBuilder { + /// The transaction being built. transaction: Transaction, } impl TransactionBuilder { + /// Creates a new TransactionBuilder with default values. + /// + /// # Returns + /// A new TransactionBuilder instance with an empty transaction. pub fn new() -> Self { Self { transaction: Transaction { @@ -18,16 +24,32 @@ impl TransactionBuilder { } } + /// Adds an input to the transaction. + /// + /// # Arguments + /// * `txid` - The transaction ID of the input + /// * `vout` - The output index of the input + /// + /// # Returns + /// Self, allowing for method chaining pub fn add_input(mut self, txid: Txid, vout: u32) -> Self { self.transaction.input.push(TxIn { previous_output: OutPoint { txid, vout }, - script_sig: ScriptBuf::new(), - sequence: Sequence::MAX, - witness: Witness::default(), + script_sig: ScriptBuf::new(), // Empty script signature (would remain empty since most of txs are p2tr) + sequence: Sequence::MAX, // Set sequence to maximum value + witness: Witness::default(), // Default (empty) witness }); self } + /// Adds an output to the transaction. + /// + /// # Arguments + /// * `address` - The recipient's Bitcoin address + /// * `amount` - The amount of Bitcoin to send + /// + /// # Returns + /// Self, allowing for method chaining pub fn add_output(mut self, address: Address, amount: Amount) -> Self { self.transaction.output.push(TxOut { value: amount, @@ -36,6 +58,10 @@ impl TransactionBuilder { self } + /// Finalizes the transaction building process. + /// + /// # Returns + /// The built Transaction pub fn build(self) -> Transaction { self.transaction } From 415e32ed0753e4d6fc483009770b561f2ed4fc3f Mon Sep 17 00:00:00 2001 From: jayendramadaram Date: Tue, 9 Jul 2024 16:52:50 +0530 Subject: [PATCH 7/9] test/add sig verification mock tests and Doc comments --- node/uhpo/src/crypto/mod.rs | 3 + node/uhpo/src/crypto/signature.rs | 269 ++++++++++++++++++++++++++++++ 2 files changed, 272 insertions(+) create mode 100644 node/uhpo/src/crypto/mod.rs create mode 100644 node/uhpo/src/crypto/signature.rs diff --git a/node/uhpo/src/crypto/mod.rs b/node/uhpo/src/crypto/mod.rs new file mode 100644 index 0000000..762499a --- /dev/null +++ b/node/uhpo/src/crypto/mod.rs @@ -0,0 +1,3 @@ +pub(crate) mod signature; + +pub use signature::add_signature; diff --git a/node/uhpo/src/crypto/signature.rs b/node/uhpo/src/crypto/signature.rs new file mode 100644 index 0000000..595c918 --- /dev/null +++ b/node/uhpo/src/crypto/signature.rs @@ -0,0 +1,269 @@ +use bitcoin::{ + hashes::Hash, + sighash::{Prevouts, SighashCache}, + TapSighashType, Transaction, TxOut, XOnlyPublicKey, +}; +use mockall::automock; +use rand::{CryptoRng, Rng}; +use secp256k1::{schnorr::Signature, All, Keypair, Message, Scalar, Secp256k1, SecretKey}; + +use crate::UhpoError; + +// traits for mock testing + +/// Trait for secret key behavior, allowing creation of keypairs. +#[automock] +pub trait SecretKeyBehavior, E: Secp256k1Behavior + 'static> { + fn keypair(&self, secp: &E) -> K; +} + +/// Trait for keypair behavior, allowing tweaking of keypairs, Exclusively used by mock tests to mock Keypair methods +#[automock] +pub trait KeypairBehavior { + fn add_xonly_tweak( + self, + secp: &E, + tweak: &Scalar, + ) -> Result; + + /// Converts the keypair to a standard Keypair type. + fn to_keypair(&self) -> Keypair; +} + +/// Trait for secp256k1 behavior, including signing and verification operations. +#[automock] +pub trait Secp256k1Behavior { + /// Signs a message using Schnorr signature scheme. + fn sign_schnorr_with_rng( + &self, + msg: &Message, + keypair: &Keypair, + rng: &mut R, + ) -> Signature; + + /// Verifies a Schnorr signature. + fn verify_schnorr( + &self, + signature: &Signature, + message: &Message, + pubkey: &XOnlyPublicKey, + ) -> Result<(), bitcoin::secp256k1::Error>; +} + +// Implementations for concrete types + +impl SecretKeyBehavior> for SecretKey { + fn keypair(&self, secp: &Secp256k1) -> Keypair { + self.keypair(secp) + } +} + +impl KeypairBehavior> for Keypair { + fn add_xonly_tweak( + self, + secp: &Secp256k1, + tweak: &Scalar, + ) -> Result { + self.add_xonly_tweak(secp, tweak) + } + + fn to_keypair(&self) -> Keypair { + self.clone() + } +} + +impl Secp256k1Behavior for Secp256k1 { + fn sign_schnorr_with_rng( + &self, + msg: &Message, + keypair: &Keypair, + rng: &mut R, + ) -> Signature { + self.sign_schnorr_with_rng(msg, keypair, rng) + } + + fn verify_schnorr( + &self, + signature: &Signature, + message: &Message, + pubkey: &XOnlyPublicKey, + ) -> Result<(), bitcoin::secp256k1::Error> { + self.verify_schnorr(signature, message, pubkey) + } +} + +/// Adds a signature to a transaction input. +/// +/// This function creates a signature for the specified input of a transaction, +/// optionally applying a tweak to the private key before signing. +pub fn add_signature( + transaction: &mut Transaction, + input_idx: usize, + prevouts: &[&TxOut], + private_key: S, + tweak: Option<&Scalar>, + secp: &E, +) -> Result<(), UhpoError> +where + S: SecretKeyBehavior, + K: KeypairBehavior, + E: Secp256k1Behavior + 'static, +{ + // apply tweak if provided + let keypair: Keypair = match tweak { + Some(tweak) => private_key + .keypair(secp) + .add_xonly_tweak(secp, tweak) + .map_err(UhpoError::KeypairCreationError)?, + None => private_key.keypair(secp).to_keypair(), + }; + + // Compute the sighash + let mut sighash_cache = SighashCache::new(transaction.clone()); + let sighash = sighash_cache.taproot_key_spend_signature_hash( + input_idx, + &Prevouts::All(prevouts), + TapSighashType::All, + )?; + + // Create and sign the message + let message = Message::from_digest(sighash.as_raw_hash().to_byte_array()); + let signature = secp.sign_schnorr_with_rng(&message, &keypair, &mut rand::thread_rng()); + + // Verify the signature + secp.verify_schnorr(&signature, &message, &keypair.x_only_public_key().0) + .map_err(UhpoError::SignatureVerificationError)?; + + // Add the signature to the transaction input + let mut vec_sig = signature.serialize().to_vec(); + vec_sig.push(0x01); + transaction.input[input_idx].witness.push(vec_sig); + + Ok(()) +} + +#[cfg(test)] +mod mock_tests { + use crate::crypto::signature::*; + + use bitcoin::{absolute::LockTime, transaction::Version, Amount, ScriptBuf, TxIn}; + use rand::rngs::ThreadRng; + + // dummy transaction for testing with no inputs + pub fn create_dummy_transaction() -> Transaction { + Transaction { + version: Version::TWO, + lock_time: LockTime::ZERO, + input: vec![], + output: vec![TxOut { + value: Amount::from_sat(50000000), + script_pubkey: ScriptBuf::new(), + }], + } + } + + #[test] + fn test_add_signatures_key_creation_fails() { + // setup mocks + let mock_secp = MockSecp256k1Behavior::new(); + let mut mock_keypair = MockKeypairBehavior::new(); + let mut mock_secret_key = MockSecretKeyBehavior::< + MockKeypairBehavior, + MockSecp256k1Behavior, + >::new(); + + // add_xonly_tweak on keypair, returns an error + mock_keypair + .expect_add_xonly_tweak() + .return_once(|_: &MockSecp256k1Behavior, _| Err(secp256k1::Error::InvalidTweak)); + + // secretKey.keypair returns an MockKeypair + mock_secret_key + .expect_keypair() + .return_once(move |_| mock_keypair); + + let mut transaction = create_dummy_transaction(); + transaction.input.push(TxIn { + ..Default::default() + }); + let prevout = TxOut { + value: Amount::from_sat(0), + script_pubkey: ScriptBuf::new(), + }; + let tweak = Some(Scalar::random_custom(&mut rand::thread_rng())); + + let result = add_signature( + &mut transaction, + 0, + &[&prevout], + mock_secret_key, + tweak.as_ref(), + &mock_secp, + ); + + assert!(matches!(result, Err(UhpoError::KeypairCreationError(_)))); + } + + #[test] + fn test_add_signatures_signature_verification_fails() { + // setup + // valid secp is used in order to generate valid keypair in add_signatures + let real_secp = Secp256k1::new(); + + // setup mocks + let mut mock_secp = MockSecp256k1Behavior::new(); + let mut mock_keypair = MockKeypairBehavior::::new(); + let mut mock_secret_key = MockSecretKeyBehavior::< + MockKeypairBehavior, + MockSecp256k1Behavior, + >::new(); + + // add_xonly_tweak on keypair returns a valid keypair + mock_keypair + .expect_add_xonly_tweak() + .return_once(move |_: &MockSecp256k1Behavior, _| { + Ok(Keypair::new(&real_secp, &mut rand::thread_rng())) + }); + + // secretKey.keypair returns an MockKeypair + mock_secret_key + .expect_keypair() + .return_once(move |_| mock_keypair); + + // sign_schnorr_with_rng returns a signature + mock_secp.expect_sign_schnorr_with_rng().return_once( + |_: &Message, _: &Keypair, _: &mut ThreadRng| { + Signature::from_slice(&[0u8; 64]).unwrap() + }, + ); + + // verify_schnorr returns an error + mock_secp + .expect_verify_schnorr() + .return_once(|_, _, _| Err(secp256k1::Error::InvalidSignature)); + + let mut transaction = create_dummy_transaction(); + transaction.input.push(TxIn { + ..Default::default() + }); + let prevout = TxOut { + value: Amount::from_sat(0), + script_pubkey: ScriptBuf::new(), + }; + let tweak = Some(Scalar::random_custom(&mut rand::thread_rng())); + + let result = add_signature( + &mut transaction, + 0, + &[&prevout], + mock_secret_key, + tweak.as_ref(), + &mock_secp, + ); + + assert!(matches!( + result, + Err(UhpoError::SignatureVerificationError(_)) + )); + } +} From 3ed8a9ff8cfe777a70790a295774d2bc6e9c6ab9 Mon Sep 17 00:00:00 2001 From: jayendramadaram Date: Mon, 15 Jul 2024 13:13:03 +0530 Subject: [PATCH 8/9] test/refractor testcase func handles --- node/uhpo/Cargo.toml | 4 +- node/uhpo/src/crypto/signature.rs | 35 ++++++++++- node/uhpo/src/payout_update.rs | 96 ++++++++++++++++++++++++++----- 3 files changed, 116 insertions(+), 19 deletions(-) diff --git a/node/uhpo/Cargo.toml b/node/uhpo/Cargo.toml index 488d784..2f23127 100644 --- a/node/uhpo/Cargo.toml +++ b/node/uhpo/Cargo.toml @@ -9,7 +9,7 @@ edition = "2021" bitcoin = {version = "0.32.2" , features = ["rand"] } rand = "0.8.5" mockall = "0.12.1" +secp256k1 = "0.29.0" -[dev-dependencies] -secp256k1 = "0.29.0" \ No newline at end of file +[dev-dependencies] \ No newline at end of file diff --git a/node/uhpo/src/crypto/signature.rs b/node/uhpo/src/crypto/signature.rs index 595c918..3b2cea6 100644 --- a/node/uhpo/src/crypto/signature.rs +++ b/node/uhpo/src/crypto/signature.rs @@ -1,11 +1,13 @@ use bitcoin::{ hashes::Hash, + secp256k1::{All, Keypair, Message, Scalar, Secp256k1, SecretKey}, sighash::{Prevouts, SighashCache}, TapSighashType, Transaction, TxOut, XOnlyPublicKey, }; use mockall::automock; use rand::{CryptoRng, Rng}; -use secp256k1::{schnorr::Signature, All, Keypair, Message, Scalar, Secp256k1, SecretKey}; +use secp256k1::schnorr::Signature; +// use secp256k1::schnorr::Signature; use crate::UhpoError; @@ -148,6 +150,7 @@ mod mock_tests { use bitcoin::{absolute::LockTime, transaction::Version, Amount, ScriptBuf, TxIn}; use rand::rngs::ThreadRng; + use secp256k1::{Secp256k1, SecretKey}; // dummy transaction for testing with no inputs pub fn create_dummy_transaction() -> Transaction { @@ -163,7 +166,7 @@ mod mock_tests { } #[test] - fn test_add_signatures_key_creation_fails() { + fn test_add_signature_with_invalid_tweak_error_key_creation_should_fail() { // setup mocks let mock_secp = MockSecp256k1Behavior::new(); let mut mock_keypair = MockKeypairBehavior::new(); @@ -205,7 +208,7 @@ mod mock_tests { } #[test] - fn test_add_signatures_signature_verification_fails() { + fn test_add_signature_with_a_bad_signature_should_fail_during_verification() { // setup // valid secp is used in order to generate valid keypair in add_signatures let real_secp = Secp256k1::new(); @@ -266,4 +269,30 @@ mod mock_tests { Err(UhpoError::SignatureVerificationError(_)) )); } + + #[test] + fn test_add_signature_with_a_valid_signature_should_pass() { + // setup + let secp = Secp256k1::new(); + let mut transaction = create_dummy_transaction(); + transaction.input.push(TxIn { + ..Default::default() + }); + let prevout = TxOut { + value: Amount::from_sat(0), + script_pubkey: ScriptBuf::new(), + }; + let tweak = Some(Scalar::random_custom(&mut rand::thread_rng())); + + let result = add_signature( + &mut transaction, + 0, + &[&prevout], + SecretKey::from_slice(&rand::thread_rng().gen::<[u8; 32]>()).unwrap(), + tweak.as_ref(), + &secp, + ); + + assert!(result.is_ok()); + } } diff --git a/node/uhpo/src/payout_update.rs b/node/uhpo/src/payout_update.rs index bf140f5..e12a4ee 100644 --- a/node/uhpo/src/payout_update.rs +++ b/node/uhpo/src/payout_update.rs @@ -1,12 +1,10 @@ use std::fmt::Error; -use bitcoin::{ - key::Secp256k1, - secp256k1::{All, Scalar, SecretKey}, - Address, Amount, Transaction, TxOut, -}; +use bitcoin::{secp256k1::Scalar, Address, Amount, Transaction, TxOut}; -use crate::crypto::signature::add_signature; +use crate::crypto::signature::{ + add_signature, KeypairBehavior, Secp256k1Behavior, SecretKeyBehavior, +}; use crate::{transaction::TransactionBuilder, UhpoError}; /// `PayoutUpdate` represents an update to a Eltoo style payout. @@ -75,12 +73,17 @@ impl PayoutUpdate { /// /// # Returns /// Result indicating success or an UhpoError - pub fn add_coinbase_sig( + pub fn add_coinbase_sig( &mut self, - private_key: SecretKey, + private_key: S, tweak: Option<&Scalar>, - secp: &Secp256k1, - ) -> Result<(), UhpoError> { + secp: &E, + ) -> Result<(), UhpoError> + where + S: SecretKeyBehavior, + K: KeypairBehavior, + E: Secp256k1Behavior + 'static, + { let prevouts = match &self.prev_update_txout { Some(prev_update_txout) => vec![&self.coinbase_txout, prev_update_txout], None => vec![&self.coinbase_txout], @@ -105,12 +108,17 @@ impl PayoutUpdate { /// /// # Returns /// Result indicating success or an UhpoError - pub fn add_prev_update_sig( + pub fn add_prev_update_sig( &mut self, - private_key: SecretKey, + private_key: S, tweak: Option<&Scalar>, - secp: &Secp256k1, - ) -> Result<(), UhpoError> { + secp: &E, + ) -> Result<(), UhpoError> + where + S: SecretKeyBehavior, + K: KeypairBehavior, + E: Secp256k1Behavior + 'static, + { let prev_update_txout = self .prev_update_txout .as_ref() @@ -135,11 +143,16 @@ impl PayoutUpdate { // unit tests #[cfg(test)] mod tests { + use crate::crypto::signature::{ + MockKeypairBehavior, MockSecp256k1Behavior, MockSecretKeyBehavior, + }; + use super::*; use bitcoin::{ absolute::LockTime, key::Keypair, secp256k1::All, transaction::Version, Network, ScriptBuf, }; use rand::Rng; + use secp256k1::{Secp256k1, SecretKey}; // Helper function for setting up tests @@ -279,4 +292,59 @@ mod tests { assert_eq!(payout_update.transaction.input[0].witness.len(), 1); assert_eq!(payout_update.transaction.input[1].witness.len(), 1); } + + #[test] + fn test_add_signature_mock_error_should_fail() { + let (_, _, new_payout_address) = setup(); + + let coinbase_tx = create_dummy_transaction(); + let prev_update_tx = create_dummy_transaction(); + let projected_fee = Amount::from_sat(1000); + let mut payout_update = PayoutUpdate::new( + Some(prev_update_tx), + coinbase_tx, + new_payout_address, + projected_fee, + ) + .expect("Failed to create payout update"); + + // setup mocks + let mock_secp = MockSecp256k1Behavior::new(); + + let create_mock_secret_key = || { + let mut mock_keypair = MockKeypairBehavior::new(); + let mut mock_secret_key = MockSecretKeyBehavior::< + MockKeypairBehavior, + MockSecp256k1Behavior, + >::new(); + + // add_xonly_tweak on keypair, returns an error + mock_keypair + .expect_add_xonly_tweak() + .return_once(|_: &MockSecp256k1Behavior, _| Err(secp256k1::Error::InvalidTweak)); + + // secretKey.keypair returns an MockKeypair + mock_secret_key + .expect_keypair() + .return_once(move |_| mock_keypair); + + mock_secret_key + }; + + let result = payout_update.add_coinbase_sig( + create_mock_secret_key(), + Some(&Scalar::random_custom(&mut rand::thread_rng())), + &mock_secp, + ); + + assert!(matches!(result, Err(UhpoError::KeypairCreationError(_)))); + + let result = payout_update.add_prev_update_sig( + create_mock_secret_key(), + Some(&Scalar::random_custom(&mut rand::thread_rng())), + &mock_secp, + ); + + assert!(matches!(result, Err(UhpoError::KeypairCreationError(_)))); + } } From b1e825f794f176cf0da7774bb780b853411cf14c Mon Sep 17 00:00:00 2001 From: jayendramadaram Date: Wed, 17 Jul 2024 20:26:01 +0530 Subject: [PATCH 9/9] feat/added --- node/uhpo/src/payout_settlement.rs | 229 ++++++++++++++++++++++++++++- 1 file changed, 227 insertions(+), 2 deletions(-) diff --git a/node/uhpo/src/payout_settlement.rs b/node/uhpo/src/payout_settlement.rs index 835fe26..53d91d6 100644 --- a/node/uhpo/src/payout_settlement.rs +++ b/node/uhpo/src/payout_settlement.rs @@ -1,3 +1,228 @@ -pub struct PayoutSettlement {} +use std::collections::HashMap; -impl PayoutSettlement {} +use bitcoin::{Address, Amount, Transaction, TxOut}; +use secp256k1::Scalar; + +use crate::{ + crypto::{ + add_signature, + signature::{KeypairBehavior, Secp256k1Behavior, SecretKeyBehavior}, + }, + transaction::TransactionBuilder, + UhpoError, +}; + +// `PayoutSettlement` represents an cashout of an Eltoo style payout. +pub struct PayoutSettlement { + transaction: Transaction, + + // `prev_update_txout` is the output of the latest Eltoo style `PayoutUpdate` it is stored and later used to sign inputs. + prev_update_txout: TxOut, +} + +impl PayoutSettlement { + /// Creates a new `PayoutSettlement` + /// + /// # Arguments + /// * `latest_eltoo_out` - The output of the latest `PayoutUpdate` + /// * `payout_map` - A map of payout addresses to payout amounts + /// + /// # Returns + /// Result containing a new `PayoutSettlement` or an `UhpoError` + pub fn new(latest_eltoo_out: Transaction, payout_map: HashMap) -> Self { + // payout_update_tx would always have vout set to 0 + let builder = TransactionBuilder::new().add_input(latest_eltoo_out.compute_txid(), 0); + + let builder = payout_map + .into_iter() + .fold(builder, |builder, (address, amount)| { + builder.add_output(address, amount) + }); + + PayoutSettlement { + transaction: builder.build(), + prev_update_txout: latest_eltoo_out.output[0].clone(), + } + } + + /// Adds a signature to the latest `PayoutUpdate` + /// + /// # Arguments + /// * `private_key` - The private key to sign with + /// * `tweak` - An optional tweak + /// * `secp` - The secp256k1 context + /// + /// # Returns + /// Result indicating success or an `UhpoError` + pub fn add_prev_update_sig( + &mut self, + private_key: S, + tweak: Option<&Scalar>, + secp: &E, + ) -> Result<(), UhpoError> + where + S: SecretKeyBehavior, + K: KeypairBehavior, + E: Secp256k1Behavior + 'static, + { + let prevouts = vec![&self.prev_update_txout]; + + add_signature( + &mut self.transaction, + 0, + &prevouts, + private_key, + tweak, + secp, + ) + } + + pub fn build(self) -> Transaction { + self.transaction + } +} + +// unit tests +#[cfg(test)] +mod tests { + use bitcoin::{absolute::LockTime, transaction::Version, Network, ScriptBuf}; + use rand::Rng; + use secp256k1::{All, Keypair, Secp256k1, SecretKey}; + + use crate::crypto::signature::{ + MockKeypairBehavior, MockSecp256k1Behavior, MockSecretKeyBehavior, + }; + + use super::*; + + fn create_dummy_transaction(amount_out: Amount) -> Transaction { + Transaction { + version: Version::TWO, + lock_time: LockTime::ZERO, + input: vec![], + output: vec![TxOut { + value: amount_out, + script_pubkey: ScriptBuf::default(), + }], + } + } + + fn setup(miner_count: u64) -> (Secp256k1, Keypair, HashMap, u64) { + let secp = Secp256k1::new(); + let mut rng = rand::thread_rng(); + + let data: [u8; 32] = rng.gen(); + let keypair = SecretKey::from_slice(&data).unwrap().keypair(&secp); + + let mut total_amount = 0; + let cashout_map: HashMap = (0..miner_count) + .map(|_| { + let data: [u8; 32] = rng.gen(); + let keypair = SecretKey::from_slice(&data).unwrap().keypair(&secp); + let new_user_address: Address = + Address::p2tr(&secp, keypair.x_only_public_key().0, None, Network::Regtest); + + let amount = rng.gen::(); + total_amount += amount as u64; + (new_user_address, Amount::from_sat(amount as u64)) + }) + .collect(); + + (secp, keypair, cashout_map, total_amount) + } + + #[test] + fn test_payout_settlement() { + let (_, _, cashout_map, total_amount) = setup(3); + let latest_payout_update = create_dummy_transaction(Amount::from_sat(total_amount + 1000)); + + let settlement_tx = + PayoutSettlement::new(latest_payout_update, cashout_map.clone()).build(); + + assert!(settlement_tx.output.len() == 3); + assert!(settlement_tx.input.len() == 1); + + settlement_tx.output.iter().for_each(|o| { + let address = Address::from_script(&o.script_pubkey, Network::Regtest).unwrap(); + assert!( + cashout_map + .get(&address) + .expect("Address not found in cashout_map") + == &o.value + ); + }); + } + + #[test] + fn test_add_signature_with_no_tweak() { + let (secp, keypair, cashout_map, _) = setup(3); + let latest_payout_update = create_dummy_transaction(Amount::from_sat(1000)); + + let mut payout_settlement = PayoutSettlement::new(latest_payout_update, cashout_map); + + payout_settlement + .add_prev_update_sig(keypair.secret_key(), None, &secp) + .expect("Failed to add signature"); + + assert_eq!(payout_settlement.transaction.input[0].witness.len(), 1); + assert_eq!(payout_settlement.transaction.input[0].witness[0].len(), 65); + } + + #[test] + fn test_add_signature_with_tweak() { + let (secp, keypair, cashout_map, _) = setup(3); + let latest_payout_update = create_dummy_transaction(Amount::from_sat(1000)); + + let mut payout_settlement = PayoutSettlement::new(latest_payout_update, cashout_map); + + payout_settlement + .add_prev_update_sig( + keypair.secret_key(), + Some(&Scalar::random_custom(&mut rand::thread_rng())), + &secp, + ) + .expect("Failed to add signature"); + + assert_eq!(payout_settlement.transaction.input[0].witness.len(), 1); + assert_eq!(payout_settlement.transaction.input[0].witness[0].len(), 65); + } + + #[test] + fn test_add_signature_mock_error_should_fail() { + let (_, _, cashout_map, _) = setup(3); + let latest_payout_update = create_dummy_transaction(Amount::from_sat(1000)); + + let mut payout_settlement = PayoutSettlement::new(latest_payout_update, cashout_map); + + // setup mocks + let mock_secp = MockSecp256k1Behavior::new(); + + let create_mock_secret_key = || { + let mut mock_keypair = MockKeypairBehavior::new(); + let mut mock_secret_key = MockSecretKeyBehavior::< + MockKeypairBehavior, + MockSecp256k1Behavior, + >::new(); + + // add_xonly_tweak on keypair, returns an error + mock_keypair + .expect_add_xonly_tweak() + .return_once(|_: &MockSecp256k1Behavior, _| Err(secp256k1::Error::InvalidTweak)); + + // secretKey.keypair returns an MockKeypair + mock_secret_key + .expect_keypair() + .return_once(move |_| mock_keypair); + + mock_secret_key + }; + + let result = payout_settlement.add_prev_update_sig( + create_mock_secret_key(), + Some(&Scalar::random_custom(&mut rand::thread_rng())), + &mock_secp, + ); + + assert!(matches!(result, Err(UhpoError::KeypairCreationError(_)))); + } +}