diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ccef823b..ae2d0798 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,6 +48,10 @@ jobs: toolchain: stable override: true - run: rustup component add rustfmt + # node and GOATNetwork/bitvm-gc must pin the same BitVM/BitVM rev, + # or Cargo builds two copies of bitvm without complaint. + - name: Single bitvm in Cargo.lock + run: test "$(grep -c '^name = "bitvm"$' Cargo.lock)" = 1 - uses: actions-rs/cargo@v1 with: command: fmt diff --git a/Cargo.lock b/Cargo.lock index b0ff8477..8594f418 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2910,38 +2910,7 @@ dependencies = [ [[package]] name = "bitvm" version = "0.1.0" -source = "git+https://github.com/GOATNetwork/BitVM.git?branch=GA#26b0bd61b61b24b50b2d2443a7fda8e58412edfa" -dependencies = [ - "ark-bn254", - "ark-crypto-primitives", - "ark-ec", - "ark-ff 0.5.0", - "ark-groth16", - "ark-relations", - "ark-serialize 0.5.0", - "ark-std 0.5.0", - "bitcoin", - "bitcoin-script", - "bitcoin-script-stack", - "bitcoin-scriptexec", - "blake3", - "colored", - "itertools 0.13.0", - "num-bigint 0.4.6", - "num-traits", - "paste", - "rand 0.8.8", - "rand_chacha 0.3.1", - "regex", - "serde", - "sha2 0.10.9", - "tqdm", -] - -[[package]] -name = "bitvm" -version = "0.1.0" -source = "git+https://github.com/GOATNetwork/BitVM.git?branch=committee-force-skip#c24f6cc2fb99d61f38b082202c899cf368abdae6" +source = "git+https://github.com/BitVM/BitVM.git?rev=c620dbfaacebdf778e765a6681985350389973b1#c620dbfaacebdf778e765a6681985350389973b1" dependencies = [ "ark-bn254", "ark-crypto-primitives", @@ -2983,7 +2952,7 @@ dependencies = [ "bincode", "bitcoin", "bitcoin-script", - "bitvm 0.1.0 (git+https://github.com/GOATNetwork/BitVM.git?branch=committee-force-skip)", + "bitvm", "chacha20poly1305", "clap", "esplora-client", @@ -3029,7 +2998,7 @@ dependencies = [ "bitcoin", "bitcoin-light-client-circuit", "bitcoin-script", - "bitvm 0.1.0 (git+https://github.com/GOATNetwork/BitVM.git?branch=committee-force-skip)", + "bitvm", "bitvm-gc", "blake3", "borsh", @@ -3525,6 +3494,7 @@ dependencies = [ "esplora-client", "hex", "rand 0.8.8", + "reqwest 0.11.27", "reqwest 0.12.28", "serde", "serde_json", @@ -5155,7 +5125,7 @@ checksum = "42012b0f064e01aa58b545fe3727f90f7dd4020f4a3ea735b50344965f5a57e9" [[package]] name = "garbled-snark-verifier" version = "0.1.0" -source = "git+https://github.com/GOATNetwork/bitvm-gc#a4dab03c56515ad4882dd73fd78c76edbe9dcc56" +source = "git+https://github.com/GOATNetwork/bitvm-gc?rev=8e9a7aceb9d3b016d31eccb64c1fcf51fc139704#8e9a7aceb9d3b016d31eccb64c1fcf51fc139704" dependencies = [ "aes", "ark-bn254", @@ -5312,42 +5282,20 @@ dependencies = [ [[package]] name = "goat" version = "0.1.0" -source = "git+https://github.com/GOATNetwork/BitVM.git?branch=committee-force-skip#c24f6cc2fb99d61f38b082202c899cf368abdae6" dependencies = [ - "ark-bn254", - "ark-crypto-primitives", - "ark-ec", - "ark-ff 0.5.0", - "ark-groth16", - "ark-relations", - "ark-serialize 0.5.0", - "ark-std 0.5.0", - "bincode", "bitcode", "bitcoin", "bitcoin-script", - "bitcoin-scriptexec", - "bitvm 0.1.0 (git+https://github.com/GOATNetwork/BitVM.git?branch=committee-force-skip)", + "bitvm", "blake3", - "clap", - "colored", "hex", "itertools 0.13.0", "musig2", - "num-traits", "rand 0.8.8", - "regex", - "reqwest 0.11.27", "secp256k1 0.29.1", "serde", "serde-big-array", - "serde_json", - "serial_test", - "sha2 0.10.9", "strum 0.27.2", - "strum_macros 0.27.2", - "tokio", - "toml 0.5.11", "zstd", ] @@ -9142,7 +9090,7 @@ dependencies = [ "tempfile", "tokio", "tokio-util", - "toml 0.8.23", + "toml", "tower-http", "tracing", "tracing-subscriber 0.3.23", @@ -11510,14 +11458,14 @@ dependencies = [ [[package]] name = "soldering-host" version = "1.1.0" -source = "git+https://github.com/GOATNetwork/bitvm-gc#a4dab03c56515ad4882dd73fd78c76edbe9dcc56" +source = "git+https://github.com/GOATNetwork/bitvm-gc?rev=8e9a7aceb9d3b016d31eccb64c1fcf51fc139704#8e9a7aceb9d3b016d31eccb64c1fcf51fc139704" dependencies = [ "ark-bn254", "ark-crypto-primitives", "ark-ff 0.5.0", "ark-groth16", "ark-relations", - "bitvm 0.1.0 (git+https://github.com/GOATNetwork/BitVM.git?branch=GA)", + "bitvm", "garbled-snark-verifier", "rand 0.8.8", "rand_chacha 0.3.1", @@ -12192,7 +12140,7 @@ dependencies = [ "serde", "serde_json", "tendermint", - "toml 0.8.23", + "toml", "url", ] @@ -12516,15 +12464,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "toml" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" -dependencies = [ - "serde", -] - [[package]] name = "toml" version = "0.8.23" @@ -13097,7 +13036,7 @@ dependencies = [ [[package]] name = "verifiable-circuit-babe" version = "0.0.1" -source = "git+https://github.com/GOATNetwork/bitvm-gc#a4dab03c56515ad4882dd73fd78c76edbe9dcc56" +source = "git+https://github.com/GOATNetwork/bitvm-gc?rev=8e9a7aceb9d3b016d31eccb64c1fcf51fc139704#8e9a7aceb9d3b016d31eccb64c1fcf51fc139704" dependencies = [ "aes", "ark-bn254", @@ -13109,7 +13048,7 @@ dependencies = [ "ark-serialize 0.5.0", "bincode", "bitcoin", - "bitvm 0.1.0 (git+https://github.com/GOATNetwork/BitVM.git?branch=GA)", + "bitvm", "blake3", "cfg-if", "garbled-snark-verifier", diff --git a/Cargo.toml b/Cargo.toml index b270c52f..38401645 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ members = [ "proof-builder-rpc", "crates/cbft-rpc", "crates/bitvm-gc", + "crates/goat", "crates/store", "crates/engine", "crates/node-macros", @@ -29,8 +30,8 @@ members = [ default-members = ["node"] [workspace.dependencies] -bitvm = { git = "https://github.com/GOATNetwork/BitVM.git", branch = "committee-force-skip" } -goat = { git = "https://github.com/GOATNetwork/BitVM.git", branch = "committee-force-skip" } +bitvm = { git = "https://github.com/BitVM/BitVM.git", rev = "c620dbfaacebdf778e765a6681985350389973b1" } +goat = { path = "crates/goat" } libp2p = { version = "0.57.1", features = ["tokio", "kad", "noise", "tcp", "yamux", "rsa", "ping"] } libp2p-swarm-derive = "0.36.0" @@ -111,9 +112,9 @@ p3-field = { git = "https://github.com/ProjectZKM/Plonky3", rev = "4dd0d47a81765 #zkm-verifier = { path = "../Ziren/crates/verifier" } bitvm-lib = { package = "bitvm-gc", path = "crates/bitvm-gc" } -verifiable-circuit-babe = { git = "https://github.com/GOATNetwork/bitvm-gc", features = ["aes"] } -garbled-snark-verifier = { git = "https://github.com/GOATNetwork/bitvm-gc", features = ["_aes"] } -soldering-host = { git = "https://github.com/GOATNetwork/bitvm-gc", features = ["aes"] } +verifiable-circuit-babe = { git = "https://github.com/GOATNetwork/bitvm-gc", rev = "8e9a7aceb9d3b016d31eccb64c1fcf51fc139704", features = ["aes"] } +garbled-snark-verifier = { git = "https://github.com/GOATNetwork/bitvm-gc", rev = "8e9a7aceb9d3b016d31eccb64c1fcf51fc139704", features = ["_aes"] } +soldering-host = { git = "https://github.com/GOATNetwork/bitvm-gc", rev = "8e9a7aceb9d3b016d31eccb64c1fcf51fc139704", features = ["aes"] } #verifiable-circuit-babe = { path = "../bitvm-gc/verifiable-circuit-babe"} #garbled-snark-verifier = { path = "../bitvm-gc/garbled-snark-verifier"} diff --git a/crates/bitvm-gc/src/verifier/api.rs b/crates/bitvm-gc/src/verifier/api.rs index 20c01a87..1383f0c9 100644 --- a/crates/bitvm-gc/src/verifier/api.rs +++ b/crates/bitvm-gc/src/verifier/api.rs @@ -486,7 +486,7 @@ mod tests { &guest_public_key, &assert_public_key, &expected_constant, - &vec![], + &[], &default_timelock_config(Network::Testnet4), ); let assert_witness = diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index c37af1ee..f050769f 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -6,6 +6,9 @@ edition.workspace = true [dependencies] bitcoin = { workspace = true } esplora-client = { workspace = true } +# esplora-client uses reqwest 0.11. These features used to arrive through goat; +# keeping them keeps its default TLS backend (native-tls, OS trust store). +reqwest-0-11 = { package = "reqwest", version = "0.11", features = ["blocking", "rustls-tls"] } alloy = { workspace = true, features = ["full", "serde"] } serde = { workspace = true } serde_json = { workspace = true } diff --git a/crates/goat/Cargo.toml b/crates/goat/Cargo.toml new file mode 100644 index 00000000..11209b70 --- /dev/null +++ b/crates/goat/Cargo.toml @@ -0,0 +1,24 @@ +# Moved from GOATNetwork/BitVM (branch gc-v2, goat/) at commit 2c76a2c. +# Dependency specs are the ones the BitVM workspace gave this crate. +[package] +name = "goat" +version = "0.1.0" +edition = "2021" +license = "MIT" + +[dependencies] +bitvm = { workspace = true } +# Needed by the paths `bitvm::treepp::script!` expands to. +bitcoin-script = { git = "https://github.com/BitVM/rust-bitcoin-script" } +bitcoin = { version = "0.32.5", features = ["rand-std"] } +strum = { version = "0.27", features = ["derive"] } +serde = { version = "1.0.197", features = ["derive"] } +serde-big-array = "0.5.1" +blake3 = "1.6.1" +rand = "0.8.5" +musig2 = { version = "0.1.0", features = ["serde", "rand"] } +itertools = "0.13.0" +secp256k1 = { version = "0.29.1", features = ["global-context"] } +zstd = "0.13.2" +bitcode = "0.6.3" +hex = "0.4.3" diff --git a/crates/goat/LICENSE b/crates/goat/LICENSE new file mode 100644 index 00000000..245b2918 --- /dev/null +++ b/crates/goat/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 BitVM + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/goat/src/assert_scripts.rs b/crates/goat/src/assert_scripts.rs new file mode 100644 index 00000000..a0edadb8 --- /dev/null +++ b/crates/goat/src/assert_scripts.rs @@ -0,0 +1,188 @@ +use crate::wots::{Wots, Wots96}; +use bitvm::{signatures::WinternitzSecret, treepp::*}; +use serde::{Deserialize, Serialize}; + +pub const INPUT_WIRE_NUM: usize = 768; +pub const PROVER_SIG_LEN: usize = 2 * Wots96::TOTAL_DIGIT_LEN as usize; +pub type OperatorAssertSecretKey = WinternitzSecret; +pub type OperatorAssertPublicKey = ::PublicKey; +pub type OperatorCommitPubinSecretKey = WinternitzSecret; +pub type OperatorCommitPubinPublicKey = ::PublicKey; +pub const OPERATOR_ASSERT_PI1_X_INDEX: usize = 0; +pub const OPERATOR_ASSERT_PI1_Y_INDEX: usize = 1; +pub const OPERATOR_ASSERT_X_D_INDEX: usize = 2; + +pub type Label = Vec; +pub type LabelHash = [u8; 20]; +#[derive(Serialize, Deserialize, Eq, PartialEq, Clone)] +pub struct WireHash { + pub true_label_hash: LabelHash, + pub false_label_hash: LabelHash, +} + +pub fn label_hash(label: &Label) -> LabelHash { + use bitcoin::hashes::{hash160, Hash}; + hash160::Hash::hash(label).to_byte_array() +} + +fn label_hash_script() -> Script { + script! { + OP_HASH160 + } +} + +pub fn wrongly_challenged_hashlocks_script(hashlocks: &[LabelHash]) -> Script { + assert!(!hashlocks.is_empty(), "wrongly challenged script requires at least one hashlock"); + script! { + { label_hash_script() } + { 0 } + for hashlock in hashlocks.iter() { + OP_OVER + { hashlock.to_vec() } + OP_EQUAL + OP_BOOLOR + } + OP_NIP + } +} + +pub fn verify_prover_assert_script_768_wire( + prover_wots_pubkey: &::PublicKey, +) -> Script { + script! { + OP_2DROP + { Wots96::checksig_verify_and_clear_stack(prover_wots_pubkey) } + OP_TRUE + } +} + +pub fn verify_prover_pubin_script(prover_wots_pubkey: &::PublicKey) -> Script { + script! { + { Wots96::checksig_verify_and_clear_stack(prover_wots_pubkey) } + OP_TRUE + } +} + +pub fn verify_verifier_assert_script_768_wire( + prover_wots_pubkey: &::PublicKey, + label_hashes: &[WireHash; 768], +) -> Script { + script! { + for byte_hashes in label_hashes.chunks(8).rev() { + { 0 } + for (bit_index, wire_hash) in byte_hashes.iter().enumerate().rev() { + OP_SWAP + OP_DUP + { label_hash_script() } + { wire_hash.true_label_hash.to_vec() } + OP_EQUAL + OP_IF + OP_DROP + { 1 << bit_index } + OP_ADD + OP_ELSE + { label_hash_script() } + { wire_hash.false_label_hash.to_vec() } + OP_EQUALVERIFY + OP_ENDIF + } + OP_TOALTSTACK + } + { Wots96::checksig_verify(prover_wots_pubkey) } + for _ in 0..Wots96::MSG_BYTE_LEN { + OP_FROMALTSTACK + OP_EQUALVERIFY + } + OP_TRUE + } +} + +#[cfg(test)] +mod tests { + use super::*; + use bitvm::execute_script; + use rand::RngCore; + + #[test] + fn test_wrongly_challenged_script_accepts_any_hashlock_preimage() { + let labels = + [b"first preimage".to_vec(), b"second preimage".to_vec(), b"third preimage".to_vec()]; + let hashlocks: Vec = labels.iter().map(label_hash).collect(); + + for label in labels { + let s = script! { + { label.clone() } + { wrongly_challenged_hashlocks_script(&hashlocks) } + }; + let result = execute_script(s); + assert!(result.success); + assert_eq!(result.final_stack.len(), 1); + } + } + + #[test] + fn test_wrongly_challenged_script_rejects_unknown_preimage() { + let labels = + [b"first preimage".to_vec(), b"second preimage".to_vec(), b"third preimage".to_vec()]; + let hashlocks: Vec = labels.iter().map(label_hash).collect(); + + let s = script! { + { b"unknown preimage".to_vec() } + { wrongly_challenged_hashlocks_script(&hashlocks) } + }; + let result = execute_script(s); + assert!(!result.success); + } + + #[test] + fn test_verify_verifier_assert_script_768_wire() { + let secret = Wots96::generate_secret_key(); + let public_key = Wots96::generate_public_key(&secret); + + let true_labels: Vec> = (0..768) + .map(|i| { + let mut label = vec![0x54; 20]; + label[0] = (i & 0xff) as u8; + label[1] = (i >> 8) as u8; + label + }) + .collect(); + let false_labels: Vec> = (0..768) + .map(|i| { + let mut label = vec![0x46; 20]; + label[0] = (i & 0xff) as u8; + label[1] = (i >> 8) as u8; + label + }) + .collect(); + + let mut msg: [u8; 96] = [0; 96]; + rand::thread_rng().fill_bytes(&mut msg); + + let mut bits = [false; 768]; + for i in 0..768 { + bits[i] = (msg[i / 8] >> (i % 8)) & 1 == 1; + } + + let label_hashes = std::array::from_fn(|i| WireHash { + true_label_hash: label_hash(&true_labels[i]), + false_label_hash: label_hash(&false_labels[i]), + }); + let selected_labels: Vec> = (0..768) + .map(|i| if bits[i] { true_labels[i].clone() } else { false_labels[i].clone() }) + .collect(); + + let s = script! { + { Wots96::sign_to_raw_witness(&secret, &msg) } + for wire_index in 0..768 { + { selected_labels[wire_index].clone() } + } + { verify_verifier_assert_script_768_wire(&public_key, &label_hashes) } + }; + println!("verifier assert full script size: {}", s.len()); + let result = execute_script(s); + println!("verifier assert max stack item size: {:?}", result.stats.max_nb_stack_items); + assert!(result.success); + assert_eq!(result.final_stack.len(), 1); + } +} diff --git a/crates/goat/src/connectors/assert_connectors.rs b/crates/goat/src/connectors/assert_connectors.rs new file mode 100644 index 00000000..290597a2 --- /dev/null +++ b/crates/goat/src/connectors/assert_connectors.rs @@ -0,0 +1,171 @@ +use crate::{assert_scripts::*, constants::TimelockConfig}; +use bitcoin::{ + taproot::{TaprootBuilder, TaprootSpendInfo}, + Address, Network, ScriptBuf, TxIn, XOnlyPublicKey, +}; +use bitvm::{chunk::api::type_conversion_utils::RawWitness, treepp::*}; +use secp256k1::SECP256K1; +use serde::{Deserialize, Serialize}; +use serde_big_array::BigArray; + +use super::{ + super::{error::Error, scripts::*, transactions::base::Input}, + base::*, +}; + +#[derive(Serialize, Deserialize, Eq, PartialEq, Clone)] +pub struct VerifierConnector { + pub network: Network, + pub n_of_n_taproot_public_key: XOnlyPublicKey, + #[serde(with = "BigArray")] + pub operator_wots_public_key: OperatorAssertPublicKey, + #[serde(with = "BigArray")] + pub label_hashes: [WireHash; INPUT_WIRE_NUM], +} + +impl VerifierConnector { + pub fn new( + network: Network, + n_of_n_taproot_public_key: &XOnlyPublicKey, + operator_wots_public_key: &OperatorAssertPublicKey, + label_hashes: [WireHash; INPUT_WIRE_NUM], + ) -> Self { + VerifierConnector { + network, + n_of_n_taproot_public_key: *n_of_n_taproot_public_key, + operator_wots_public_key: *operator_wots_public_key, + label_hashes, + } + } + + fn generate_taproot_leaf_0_script(&self) -> ScriptBuf { + verify_verifier_assert_script_768_wire(&self.operator_wots_public_key, &self.label_hashes) + .compile() + } + + fn generate_taproot_leaf_0_tx_in(&self, input: &Input) -> TxIn { + generate_default_tx_in(input) + } + + pub fn generate_leaf_0_unlock_data( + &self, + labels: [Label; INPUT_WIRE_NUM], + operator_assertion: &RawWitness, + ) -> Result>, Error> { + let mut witness = Vec::with_capacity(operator_assertion.len() + INPUT_WIRE_NUM); + witness.extend(operator_assertion.iter().cloned()); + witness.extend(labels); + let witness_script = script! { + { witness.clone() } + }; + let verification_script = witness_script.push_script(self.generate_taproot_leaf_0_script()); + let exec_result = execute_script(verification_script); + match exec_result.success { + true => Ok(witness), + false => Err(Error::Other("Invalid unlock data for VerifierConnector.")), + } + } +} + +impl TaprootConnector for VerifierConnector { + fn generate_taproot_leaf_script(&self, leaf_index: u32) -> ScriptBuf { + match leaf_index { + 0 => self.generate_taproot_leaf_0_script(), + _ => panic!("Invalid leaf index."), + } + } + + fn generate_taproot_leaf_tx_in(&self, leaf_index: u32, input: &Input) -> TxIn { + match leaf_index { + 0 => self.generate_taproot_leaf_0_tx_in(input), + _ => panic!("Invalid leaf index."), + } + } + + fn generate_taproot_spend_info(&self) -> TaprootSpendInfo { + TaprootBuilder::new() + .add_leaf(0, self.generate_taproot_leaf_0_script()) + .expect("Unable to add leaf 0") + .finalize(SECP256K1, self.n_of_n_taproot_public_key) + .expect("Unable to finalize taproot") + } + + fn generate_taproot_address(&self) -> Address { + Address::p2tr_tweaked(self.generate_taproot_spend_info().output_key(), self.network) + } +} + +pub struct ProverConnector { + pub network: Network, + pub n_of_n_taproot_public_key: XOnlyPublicKey, + pub disprove_blocks_timelock: u32, + pub hashlocks: Vec, +} + +impl ProverConnector { + pub fn new( + network: Network, + n_of_n_taproot_public_key: XOnlyPublicKey, + hashlocks: Vec, + timelock_config: &TimelockConfig, + ) -> Self { + assert!(!hashlocks.is_empty(), "ProverConnector requires at least one hashlock"); + ProverConnector { + network, + n_of_n_taproot_public_key, + disprove_blocks_timelock: timelock_config.prover_connector, + hashlocks, + } + } + + fn generate_taproot_leaf_0_script(&self) -> ScriptBuf { + wrongly_challenged_hashlocks_script(&self.hashlocks).compile() + } + + fn generate_taproot_leaf_0_tx_in(&self, input: &Input) -> TxIn { + generate_default_tx_in(input) + } + + fn generate_taproot_leaf_1_script(&self) -> ScriptBuf { + generate_timelock_taproot_script( + &self.n_of_n_taproot_public_key, + self.disprove_blocks_timelock, + ) + } + + fn generate_taproot_leaf_1_tx_in(&self, input: &Input) -> TxIn { + generate_timelock_tx_in(input, self.disprove_blocks_timelock) + } +} + +impl TaprootConnector for ProverConnector { + fn generate_taproot_leaf_script(&self, leaf_index: u32) -> ScriptBuf { + match leaf_index { + 0 => self.generate_taproot_leaf_0_script(), + 1 => self.generate_taproot_leaf_1_script(), + _ => panic!("Invalid leaf index."), + } + } + + fn generate_taproot_leaf_tx_in(&self, leaf_index: u32, input: &Input) -> TxIn { + match leaf_index { + 0 => self.generate_taproot_leaf_0_tx_in(input), + 1 => self.generate_taproot_leaf_1_tx_in(input), + _ => panic!("Invalid leaf index."), + } + } + + fn generate_taproot_spend_info(&self) -> TaprootSpendInfo { + TaprootBuilder::new() + .add_leaf(1, self.generate_taproot_leaf_0_script()) + .expect("Unable to add leaf 0") + .add_leaf(1, self.generate_taproot_leaf_1_script()) + .expect("Unable to add leaf 1") + .finalize(SECP256K1, self.n_of_n_taproot_public_key) + .expect("Unable to finalize taproot") + } + + fn generate_taproot_address(&self) -> Address { + Address::p2tr_tweaked(self.generate_taproot_spend_info().output_key(), self.network) + } +} diff --git a/crates/goat/src/connectors/base.rs b/crates/goat/src/connectors/base.rs new file mode 100644 index 00000000..4c14c05c --- /dev/null +++ b/crates/goat/src/connectors/base.rs @@ -0,0 +1,65 @@ +use bitcoin::{ + key::TweakedPublicKey, taproot::TaprootSpendInfo, Address, ScriptBuf, Sequence, TapNodeHash, + TxIn, Witness, +}; +use serde::{Deserialize, Serialize}; + +use super::super::transactions::base::Input; + +pub fn generate_default_tx_in(input: &Input) -> TxIn { + TxIn { + previous_output: input.outpoint, + script_sig: ScriptBuf::new(), + sequence: Sequence::MAX, + witness: Witness::default(), + } +} + +pub fn generate_timelock_tx_in(input: &Input, num_blocks: u32) -> TxIn { + let mut tx_in = generate_default_tx_in(input); + tx_in.sequence = Sequence(num_blocks); + tx_in +} + +/* +// TODO figure out if we need to handle CLTV differently than other timelock txs +https://github.com/BlockchainCommons/Learning-Bitcoin-from-the-Command-Line/blob/master/11_2_Using_CLTV_in_Scripts.md +A locking script will only allow a transaction to respend a UTXO locked with a CLTV if OP_CHECKLOCKTIMEVERIFY verifies all of the following: + +The nSequence field must be set to less than 0xffffffff, usually 0xffffffff-1 to avoid confilcts with relative timelocks. +CLTV must pop an operand off the stack and it must be 0 or greater. +Both the stack operand and nLockTime must either be above or below 500 million, to depict the same sort of absolute timelock. +The nLockTime value must be greater than or equal to the stack operand. +So the first thing to note here is that nLockTime is still used with CLTV. To be precise, it's required in the transaction that tries to respend a CLTV-timelocked UTXO. That means that it's not a part of the script's requirements. It's just the timer that's used to release the funds, as defined in the script. + +This is managed through a clever understanding of how nLockTime works: a value for nLockTime must always be chosen that is less than or equal to the present time (or blockheight), so that the respending transaction can be put on the blockchain. However, due to CLTV's requirements, a value must also be chosen that is greater than or equal to CLTV's operand. The union of these two sets is NULL until the present time matches the CLTV operand. Afterward, any value can be chosen between CLTV's operand and the present time. Usually, you'd just set it to the present time (or block). +*/ +pub fn generate_check_lock_time_tx_in(input: &Input, num_blocks: u32) -> TxIn { + let mut tx_in = generate_default_tx_in(input); + tx_in.sequence = Sequence(num_blocks); + tx_in +} + +pub trait P2wshConnector { + fn generate_script(&self) -> ScriptBuf; + + fn generate_address(&self) -> Address; + + fn generate_tx_in(&self, input: &Input) -> TxIn; +} + +#[derive(Serialize, Deserialize, Eq, PartialEq, Clone)] +pub struct TaprootSpendInfoCache { + pub merkle_root: Option, + pub output_key: TweakedPublicKey, +} + +pub trait TaprootConnector { + fn generate_taproot_leaf_script(&self, leaf_index: u32) -> ScriptBuf; + + fn generate_taproot_leaf_tx_in(&self, leaf_index: u32, input: &Input) -> TxIn; + + fn generate_taproot_spend_info(&self) -> TaprootSpendInfo; + + fn generate_taproot_address(&self) -> Address; +} diff --git a/crates/goat/src/connectors/connector_0.rs b/crates/goat/src/connectors/connector_0.rs new file mode 100644 index 00000000..05e5f9aa --- /dev/null +++ b/crates/goat/src/connectors/connector_0.rs @@ -0,0 +1,59 @@ +use bitcoin::{ + taproot::{TaprootBuilder, TaprootSpendInfo}, + Address, Network, ScriptBuf, TxIn, XOnlyPublicKey, +}; +use secp256k1::SECP256K1; +use serde::{Deserialize, Serialize}; + +use super::{ + super::{scripts::*, transactions::base::Input}, + base::*, +}; + +#[derive(Serialize, Deserialize, Eq, PartialEq, Clone)] +pub struct Connector0 { + pub network: Network, + pub n_of_n_taproot_public_key: XOnlyPublicKey, +} + +impl Connector0 { + pub fn new(network: Network, n_of_n_taproot_public_key: &XOnlyPublicKey) -> Self { + Connector0 { network, n_of_n_taproot_public_key: *n_of_n_taproot_public_key } + } + + fn generate_taproot_leaf_0_script(&self) -> ScriptBuf { + generate_pay_to_pubkey_taproot_script(&self.n_of_n_taproot_public_key) + } + + fn generate_taproot_leaf_0_tx_in(&self, input: &Input) -> TxIn { + generate_default_tx_in(input) + } +} + +impl TaprootConnector for Connector0 { + fn generate_taproot_leaf_script(&self, leaf_index: u32) -> ScriptBuf { + match leaf_index { + 0 => self.generate_taproot_leaf_0_script(), + _ => panic!("Invalid leaf index."), + } + } + + fn generate_taproot_leaf_tx_in(&self, leaf_index: u32, input: &Input) -> TxIn { + match leaf_index { + 0 => self.generate_taproot_leaf_0_tx_in(input), + _ => panic!("Invalid leaf index."), + } + } + + fn generate_taproot_spend_info(&self) -> TaprootSpendInfo { + TaprootBuilder::new() + .add_leaf(0, self.generate_taproot_leaf_0_script()) + .expect("Unable to add leaf 0") + .finalize(SECP256K1, self.n_of_n_taproot_public_key) + .expect("Unable to finalize taproot") + } + + fn generate_taproot_address(&self) -> Address { + Address::p2tr_tweaked(self.generate_taproot_spend_info().output_key(), self.network) + } +} diff --git a/crates/goat/src/connectors/connector_a.rs b/crates/goat/src/connectors/connector_a.rs new file mode 100644 index 00000000..92cb976e --- /dev/null +++ b/crates/goat/src/connectors/connector_a.rs @@ -0,0 +1,88 @@ +use bitcoin::{ + taproot::{TaprootBuilder, TaprootSpendInfo}, + Address, Network, ScriptBuf, TxIn, XOnlyPublicKey, +}; +use secp256k1::SECP256K1; +use serde::{Deserialize, Serialize}; + +use crate::constants::TimelockConfig; + +use super::{ + super::{scripts::*, transactions::base::Input}, + base::*, +}; + +#[derive(Serialize, Deserialize, Eq, PartialEq, Clone)] +pub struct ConnectorA { + pub network: Network, + pub operator_taproot_public_key: XOnlyPublicKey, + pub n_of_n_taproot_public_key: XOnlyPublicKey, + pub take1_blocks_timelock: u32, +} + +impl ConnectorA { + pub fn new( + network: Network, + operator_taproot_public_key: &XOnlyPublicKey, + n_of_n_taproot_public_key: &XOnlyPublicKey, + timelock_config: &TimelockConfig, + ) -> Self { + ConnectorA { + network, + operator_taproot_public_key: *operator_taproot_public_key, + n_of_n_taproot_public_key: *n_of_n_taproot_public_key, + take1_blocks_timelock: timelock_config.connector_a, + } + } + + fn generate_taproot_leaf_0_script(&self) -> ScriptBuf { + generate_timelock_taproot_script( + &self.operator_taproot_public_key, + self.take1_blocks_timelock, + ) + } + + fn generate_taproot_leaf_0_tx_in(&self, input: &Input) -> TxIn { + generate_timelock_tx_in(input, self.take1_blocks_timelock) + } + + fn generate_taproot_leaf_1_script(&self) -> ScriptBuf { + generate_pay_to_pubkey_taproot_script(&self.n_of_n_taproot_public_key) + } + + fn generate_taproot_leaf_1_tx_in(&self, input: &Input) -> TxIn { + generate_default_tx_in(input) + } +} + +impl TaprootConnector for ConnectorA { + fn generate_taproot_leaf_script(&self, leaf_index: u32) -> ScriptBuf { + match leaf_index { + 0 => self.generate_taproot_leaf_0_script(), + 1 => self.generate_taproot_leaf_1_script(), + _ => panic!("Invalid leaf index."), + } + } + + fn generate_taproot_leaf_tx_in(&self, leaf_index: u32, input: &Input) -> TxIn { + match leaf_index { + 0 => self.generate_taproot_leaf_0_tx_in(input), + 1 => self.generate_taproot_leaf_1_tx_in(input), + _ => panic!("Invalid leaf index."), + } + } + + fn generate_taproot_spend_info(&self) -> TaprootSpendInfo { + TaprootBuilder::new() + .add_leaf(1, self.generate_taproot_leaf_0_script()) + .expect("Unable to add leaf 0") + .add_leaf(1, self.generate_taproot_leaf_1_script()) + .expect("Unable to add leaf 1") + .finalize(SECP256K1, self.n_of_n_taproot_public_key) + .expect("Unable to finalize taproot") + } + + fn generate_taproot_address(&self) -> Address { + Address::p2tr_tweaked(self.generate_taproot_spend_info().output_key(), self.network) + } +} diff --git a/crates/goat/src/connectors/connector_b.rs b/crates/goat/src/connectors/connector_b.rs new file mode 100644 index 00000000..598ff5dd --- /dev/null +++ b/crates/goat/src/connectors/connector_b.rs @@ -0,0 +1,59 @@ +use bitcoin::{ + taproot::{TaprootBuilder, TaprootSpendInfo}, + Address, Network, ScriptBuf, TxIn, XOnlyPublicKey, +}; +use secp256k1::SECP256K1; +use serde::{Deserialize, Serialize}; + +use super::{ + super::{scripts::*, transactions::base::Input}, + base::*, +}; + +#[derive(Serialize, Deserialize, Eq, PartialEq, Clone)] +pub struct ConnectorB { + pub network: Network, + pub operator_taproot_public_key: XOnlyPublicKey, +} + +impl ConnectorB { + pub fn new(network: Network, operator_taproot_public_key: &XOnlyPublicKey) -> Self { + ConnectorB { network, operator_taproot_public_key: *operator_taproot_public_key } + } + + fn generate_taproot_leaf_0_script(&self) -> ScriptBuf { + generate_pay_to_pubkey_taproot_script(&self.operator_taproot_public_key) + } + + fn generate_taproot_leaf_0_tx_in(&self, input: &Input) -> TxIn { + generate_default_tx_in(input) + } +} + +impl TaprootConnector for ConnectorB { + fn generate_taproot_leaf_script(&self, leaf_index: u32) -> ScriptBuf { + match leaf_index { + 0 => self.generate_taproot_leaf_0_script(), + _ => panic!("Invalid leaf index."), + } + } + + fn generate_taproot_leaf_tx_in(&self, leaf_index: u32, input: &Input) -> TxIn { + match leaf_index { + 0 => self.generate_taproot_leaf_0_tx_in(input), + _ => panic!("Invalid leaf index."), + } + } + + fn generate_taproot_spend_info(&self) -> TaprootSpendInfo { + TaprootBuilder::new() + .add_leaf(0, self.generate_taproot_leaf_0_script()) + .expect("Unable to add leaf 0") + .finalize(SECP256K1, self.operator_taproot_public_key) + .expect("Unable to finalize taproot") + } + + fn generate_taproot_address(&self) -> Address { + Address::p2tr_tweaked(self.generate_taproot_spend_info().output_key(), self.network) + } +} diff --git a/crates/goat/src/connectors/connector_c.rs b/crates/goat/src/connectors/connector_c.rs new file mode 100644 index 00000000..73e32e77 --- /dev/null +++ b/crates/goat/src/connectors/connector_c.rs @@ -0,0 +1,118 @@ +use crate::{assert_scripts::*, utils::remove_script_and_control_block_from_witness}; +use bitcoin::{ + taproot::{TaprootBuilder, TaprootSpendInfo}, + Address, Network, ScriptBuf, TxIn, XOnlyPublicKey, +}; +use bitvm::{chunk::api::type_conversion_utils::RawWitness, treepp::*}; +use secp256k1::SECP256K1; +use serde::{Deserialize, Serialize}; +use serde_big_array::BigArray; + +use super::{ + super::{error::Error, scripts::*, transactions::base::Input, wots::*}, + base::*, +}; + +#[derive(Serialize, Deserialize, Eq, PartialEq, Clone)] +pub struct ConnectorC { + pub network: Network, + pub n_of_n_taproot_public_key: XOnlyPublicKey, + #[serde(with = "BigArray")] + pub operator_wots_public_key: OperatorAssertPublicKey, +} + +impl ConnectorC { + pub fn new( + network: Network, + n_of_n_taproot_public_key: &XOnlyPublicKey, + operator_wots_public_key: &OperatorAssertPublicKey, + ) -> Self { + ConnectorC { + network, + n_of_n_taproot_public_key: *n_of_n_taproot_public_key, + operator_wots_public_key: *operator_wots_public_key, + } + } + + fn generate_taproot_leaf_0_script(&self) -> ScriptBuf { + generate_pay_to_pubkey_taproot_script(&self.n_of_n_taproot_public_key) + } + + fn generate_taproot_leaf_0_tx_in(&self, input: &Input) -> TxIn { + generate_default_tx_in(input) + } + + fn generate_taproot_leaf_1_script(&self) -> ScriptBuf { + verify_prover_assert_script_768_wire(&self.operator_wots_public_key).compile() + } + + fn generate_taproot_leaf_1_tx_in(&self, input: &Input) -> TxIn { + generate_default_tx_in(input) + } + + pub fn generate_leaf_1_unlock_data( + &self, + sk: &OperatorAssertSecretKey, + proof: &[u8; 96], + pi2: &[u8], + pi3: &[u8], + ) -> Result>, Error> { + let witness = Wots96::sign_to_raw_witness(sk, proof); + let witness_script = script! { + { witness.clone() } + { pi2.to_vec() } + { pi3.to_vec() } + }; + let verification_script = witness_script.push_script(self.generate_taproot_leaf_1_script()); + let exec_result = execute_script(verification_script); + match exec_result.success { + true => { + let mut unlock_data = witness.to_vec(); + unlock_data.push(pi2.to_vec()); + unlock_data.push(pi3.to_vec()); + Ok(unlock_data) + } + false => Err(Error::Other("Invalid WOTS secret-key for Connector-C.")), + } + } + + pub fn extract_leaf_1_raw_witness(&self, txin: &TxIn) -> Result { + let witness = txin.witness.to_vec(); + if witness.len() != PROVER_SIG_LEN + 4 { + return Err(Error::Other("Invalid witness length for Connector-C leaf 1.")); + } + Ok(remove_script_and_control_block_from_witness(witness)) + } +} + +impl TaprootConnector for ConnectorC { + fn generate_taproot_leaf_script(&self, leaf_index: u32) -> ScriptBuf { + match leaf_index { + 0 => self.generate_taproot_leaf_0_script(), + 1 => self.generate_taproot_leaf_1_script(), + _ => panic!("Invalid leaf index."), + } + } + + fn generate_taproot_leaf_tx_in(&self, leaf_index: u32, input: &Input) -> TxIn { + match leaf_index { + 0 => self.generate_taproot_leaf_0_tx_in(input), + 1 => self.generate_taproot_leaf_1_tx_in(input), + _ => panic!("Invalid leaf index."), + } + } + + fn generate_taproot_spend_info(&self) -> TaprootSpendInfo { + TaprootBuilder::new() + .add_leaf(1, self.generate_taproot_leaf_0_script()) + .expect("Unable to add leaf 0") + .add_leaf(1, self.generate_taproot_leaf_1_script()) + .expect("Unable to add leaf 1") + .finalize(SECP256K1, self.n_of_n_taproot_public_key) + .expect("Unable to finalize taproot") + } + + fn generate_taproot_address(&self) -> Address { + Address::p2tr_tweaked(self.generate_taproot_spend_info().output_key(), self.network) + } +} diff --git a/crates/goat/src/connectors/connector_d.rs b/crates/goat/src/connectors/connector_d.rs new file mode 100644 index 00000000..b163d521 --- /dev/null +++ b/crates/goat/src/connectors/connector_d.rs @@ -0,0 +1,130 @@ +use bitcoin::{ + taproot::{TaprootBuilder, TaprootSpendInfo}, + Address, Network, ScriptBuf, TxIn, XOnlyPublicKey, +}; +use secp256k1::SECP256K1; +use serde::{Deserialize, Serialize}; +use serde_big_array::BigArray; + +use crate::{ + assert_scripts::{LabelHash, OperatorAssertPublicKey, OperatorCommitPubinPublicKey}, + constants::TimelockConfig, + pubin_disprove_scripts::verify_guest_pubin_commitment, +}; + +use super::{ + super::{scripts::*, transactions::base::Input}, + base::*, +}; + +pub const CONNECTOR_D_TAKE2_LEAF_INDEX: u32 = 0; +pub const CONNECTOR_D_N_OF_N_LEAF_INDEX: u32 = 1; +pub const CONNECTOR_D_PUBIN_DISPROVE_LEAF_INDEX: u32 = 2; + +#[derive(Serialize, Deserialize, Eq, PartialEq, Clone)] +pub struct ConnectorD { + pub network: Network, + pub operator_taproot_public_key: XOnlyPublicKey, + pub n_of_n_taproot_public_key: XOnlyPublicKey, + #[serde(with = "BigArray")] + pub operator_commit_pubin_wots_public_key: OperatorCommitPubinPublicKey, + #[serde(with = "BigArray")] + pub operator_assert_wots_public_key: OperatorAssertPublicKey, + pub pubin_constant_value: [u8; 32], + pub watchtower_hashlocks: Vec, + pub take2_blocks_timelock: u32, +} + +impl ConnectorD { + #[allow(clippy::too_many_arguments)] + pub fn new( + network: Network, + operator_taproot_public_key: &XOnlyPublicKey, + n_of_n_taproot_public_key: &XOnlyPublicKey, + operator_commit_pubin_wots_public_key: &OperatorCommitPubinPublicKey, + operator_assert_wots_public_key: &OperatorAssertPublicKey, + pubin_constant_value: &[u8; 32], + watchtower_hashlocks: &[LabelHash], + timelock_config: &TimelockConfig, + ) -> Self { + ConnectorD { + network, + operator_taproot_public_key: *operator_taproot_public_key, + n_of_n_taproot_public_key: *n_of_n_taproot_public_key, + operator_commit_pubin_wots_public_key: *operator_commit_pubin_wots_public_key, + operator_assert_wots_public_key: *operator_assert_wots_public_key, + pubin_constant_value: *pubin_constant_value, + watchtower_hashlocks: watchtower_hashlocks.to_vec(), + take2_blocks_timelock: timelock_config.connector_d, + } + } + + fn generate_taproot_leaf_0_script(&self) -> ScriptBuf { + generate_timelock_taproot_script( + &self.operator_taproot_public_key, + self.take2_blocks_timelock, + ) + } + + fn generate_taproot_leaf_0_tx_in(&self, input: &Input) -> TxIn { + generate_timelock_tx_in(input, self.take2_blocks_timelock) + } + + fn generate_taproot_leaf_1_script(&self) -> ScriptBuf { + generate_pay_to_pubkey_taproot_script(&self.n_of_n_taproot_public_key) + } + + fn generate_taproot_leaf_1_tx_in(&self, input: &Input) -> TxIn { + generate_default_tx_in(input) + } + + fn generate_taproot_leaf_2_script(&self) -> ScriptBuf { + verify_guest_pubin_commitment( + &self.operator_commit_pubin_wots_public_key, + &self.operator_assert_wots_public_key, + &self.pubin_constant_value, + &self.watchtower_hashlocks, + ) + .compile() + } + + fn generate_taproot_leaf_2_tx_in(&self, input: &Input) -> TxIn { + generate_default_tx_in(input) + } +} + +impl TaprootConnector for ConnectorD { + fn generate_taproot_leaf_script(&self, leaf_index: u32) -> ScriptBuf { + match leaf_index { + CONNECTOR_D_TAKE2_LEAF_INDEX => self.generate_taproot_leaf_0_script(), + CONNECTOR_D_N_OF_N_LEAF_INDEX => self.generate_taproot_leaf_1_script(), + CONNECTOR_D_PUBIN_DISPROVE_LEAF_INDEX => self.generate_taproot_leaf_2_script(), + _ => panic!("Invalid leaf index."), + } + } + + fn generate_taproot_leaf_tx_in(&self, leaf_index: u32, input: &Input) -> TxIn { + match leaf_index { + CONNECTOR_D_TAKE2_LEAF_INDEX => self.generate_taproot_leaf_0_tx_in(input), + CONNECTOR_D_N_OF_N_LEAF_INDEX => self.generate_taproot_leaf_1_tx_in(input), + CONNECTOR_D_PUBIN_DISPROVE_LEAF_INDEX => self.generate_taproot_leaf_2_tx_in(input), + _ => panic!("Invalid leaf index."), + } + } + + fn generate_taproot_spend_info(&self) -> TaprootSpendInfo { + TaprootBuilder::new() + .add_leaf(2, self.generate_taproot_leaf_0_script()) + .expect("Unable to add leaf 0") + .add_leaf(2, self.generate_taproot_leaf_1_script()) + .expect("Unable to add leaf 1") + .add_leaf(1, self.generate_taproot_leaf_2_script()) + .expect("Unable to add leaf 2") + .finalize(SECP256K1, self.n_of_n_taproot_public_key) + .expect("Unable to finalize taproot") + } + + fn generate_taproot_address(&self) -> Address { + Address::p2tr_tweaked(self.generate_taproot_spend_info().output_key(), self.network) + } +} diff --git a/crates/goat/src/connectors/connector_e.rs b/crates/goat/src/connectors/connector_e.rs new file mode 100644 index 00000000..6ff1e94d --- /dev/null +++ b/crates/goat/src/connectors/connector_e.rs @@ -0,0 +1,125 @@ +use crate::{ + assert_scripts::{ + verify_prover_pubin_script, OperatorCommitPubinPublicKey, OperatorCommitPubinSecretKey, + PROVER_SIG_LEN, + }, + constants::TimelockConfig, + utils::remove_script_and_control_block_from_witness, + wots::*, +}; +use bitcoin::{ + taproot::{TaprootBuilder, TaprootSpendInfo}, + Address, Network, ScriptBuf, TxIn, XOnlyPublicKey, +}; +use bitvm::{chunk::api::type_conversion_utils::RawWitness, treepp::*}; +use secp256k1::SECP256K1; +use serde::{Deserialize, Serialize}; +use serde_big_array::BigArray; + +use super::{ + super::{error::Error, scripts::*, transactions::base::Input}, + base::*, +}; + +#[derive(Serialize, Deserialize, Eq, PartialEq, Clone)] +pub struct ConnectorE { + pub network: Network, + pub n_of_n_taproot_public_key: XOnlyPublicKey, + #[serde(with = "BigArray")] + pub operator_commit_pubin_wots_public_key: OperatorCommitPubinPublicKey, + pub operator_commit_blocks_timelock: u32, +} + +impl ConnectorE { + pub fn new( + network: Network, + n_of_n_taproot_public_key: &XOnlyPublicKey, + operator_commit_pubin_wots_public_key: &OperatorCommitPubinPublicKey, + timelock_config: &TimelockConfig, + ) -> Self { + ConnectorE { + network, + n_of_n_taproot_public_key: *n_of_n_taproot_public_key, + operator_commit_pubin_wots_public_key: *operator_commit_pubin_wots_public_key, + operator_commit_blocks_timelock: timelock_config.operator_commit, + } + } + + fn generate_taproot_leaf_0_script(&self) -> ScriptBuf { + verify_prover_pubin_script(&self.operator_commit_pubin_wots_public_key).compile() + } + + fn generate_taproot_leaf_0_tx_in(&self, input: &Input) -> TxIn { + generate_default_tx_in(input) + } + + fn generate_taproot_leaf_1_script(&self) -> ScriptBuf { + generate_timelock_taproot_script( + &self.n_of_n_taproot_public_key, + self.operator_commit_blocks_timelock, + ) + } + + fn generate_taproot_leaf_1_tx_in(&self, input: &Input) -> TxIn { + generate_timelock_tx_in(input, self.operator_commit_blocks_timelock) + } + + pub fn generate_leaf_0_unlock_data( + &self, + sk: &OperatorCommitPubinSecretKey, + pubin_commitment: &[u8; 96], + ) -> Result>, Error> { + let witness = Wots96::sign_to_raw_witness(sk, pubin_commitment); + let witness_script = script! { + { witness.clone() } + }; + let verification_script = witness_script.push_script(self.generate_taproot_leaf_0_script()); + let exec_result = execute_script(verification_script); + match exec_result.success { + true => Ok(witness.to_vec()), + false => { + Err(Error::Other("Invalid operator-commit-pubin WOTS secret-key for Connector-E.")) + } + } + } + + pub fn extract_leaf_0_raw_witness(&self, txin: &TxIn) -> Result { + let witness = txin.witness.to_vec(); + if witness.len() != PROVER_SIG_LEN + 2 { + return Err(Error::Other("Invalid witness length for Connector-E leaf 0.")); + } + Ok(remove_script_and_control_block_from_witness(witness)) + } +} + +impl TaprootConnector for ConnectorE { + fn generate_taproot_leaf_script(&self, leaf_index: u32) -> ScriptBuf { + match leaf_index { + 0 => self.generate_taproot_leaf_0_script(), + 1 => self.generate_taproot_leaf_1_script(), + _ => panic!("Invalid leaf index."), + } + } + + fn generate_taproot_leaf_tx_in(&self, leaf_index: u32, input: &Input) -> TxIn { + match leaf_index { + 0 => self.generate_taproot_leaf_0_tx_in(input), + 1 => self.generate_taproot_leaf_1_tx_in(input), + _ => panic!("Invalid leaf index."), + } + } + + fn generate_taproot_spend_info(&self) -> TaprootSpendInfo { + TaprootBuilder::new() + .add_leaf(1, self.generate_taproot_leaf_0_script()) + .expect("Unable to add leaf 0") + .add_leaf(1, self.generate_taproot_leaf_1_script()) + .expect("Unable to add leaf 1") + .finalize(SECP256K1, self.n_of_n_taproot_public_key) + .expect("Unable to finalize taproot") + } + + fn generate_taproot_address(&self) -> Address { + Address::p2tr_tweaked(self.generate_taproot_spend_info().output_key(), self.network) + } +} diff --git a/crates/goat/src/connectors/connector_f.rs b/crates/goat/src/connectors/connector_f.rs new file mode 100644 index 00000000..7cc787c0 --- /dev/null +++ b/crates/goat/src/connectors/connector_f.rs @@ -0,0 +1,88 @@ +use bitcoin::{ + taproot::{TaprootBuilder, TaprootSpendInfo}, + Address, Network, ScriptBuf, TxIn, XOnlyPublicKey, +}; +use secp256k1::SECP256K1; +use serde::{Deserialize, Serialize}; + +use crate::constants::TimelockConfig; + +use super::{ + super::{scripts::*, transactions::base::Input}, + base::*, +}; + +#[derive(Serialize, Deserialize, Eq, PartialEq, Clone)] +pub struct ConnectorF { + pub network: Network, + pub operator_taproot_public_key: XOnlyPublicKey, + pub n_of_n_taproot_public_key: XOnlyPublicKey, + pub take2_blocks_timelock: u32, +} + +impl ConnectorF { + pub fn new( + network: Network, + operator_taproot_public_key: &XOnlyPublicKey, + n_of_n_taproot_public_key: &XOnlyPublicKey, + timelock_config: &TimelockConfig, + ) -> Self { + ConnectorF { + network, + operator_taproot_public_key: *operator_taproot_public_key, + n_of_n_taproot_public_key: *n_of_n_taproot_public_key, + take2_blocks_timelock: timelock_config.connector_f, + } + } + + fn generate_taproot_leaf_0_script(&self) -> ScriptBuf { + generate_timelock_taproot_script( + &self.operator_taproot_public_key, + self.take2_blocks_timelock, + ) + } + + fn generate_taproot_leaf_0_tx_in(&self, input: &Input) -> TxIn { + generate_timelock_tx_in(input, self.take2_blocks_timelock) + } + + fn generate_taproot_leaf_1_script(&self) -> ScriptBuf { + generate_pay_to_pubkey_taproot_script(&self.n_of_n_taproot_public_key) + } + + fn generate_taproot_leaf_1_tx_in(&self, input: &Input) -> TxIn { + generate_default_tx_in(input) + } +} + +impl TaprootConnector for ConnectorF { + fn generate_taproot_leaf_script(&self, leaf_index: u32) -> ScriptBuf { + match leaf_index { + 0 => self.generate_taproot_leaf_0_script(), + 1 => self.generate_taproot_leaf_1_script(), + _ => panic!("Invalid leaf index."), + } + } + + fn generate_taproot_leaf_tx_in(&self, leaf_index: u32, input: &Input) -> TxIn { + match leaf_index { + 0 => self.generate_taproot_leaf_0_tx_in(input), + 1 => self.generate_taproot_leaf_1_tx_in(input), + _ => panic!("Invalid leaf index."), + } + } + + fn generate_taproot_spend_info(&self) -> TaprootSpendInfo { + TaprootBuilder::new() + .add_leaf(1, self.generate_taproot_leaf_0_script()) + .expect("Unable to add leaf 0") + .add_leaf(1, self.generate_taproot_leaf_1_script()) + .expect("Unable to add leaf 1") + .finalize(SECP256K1, self.n_of_n_taproot_public_key) + .expect("Unable to finalize taproot") + } + + fn generate_taproot_address(&self) -> Address { + Address::p2tr_tweaked(self.generate_taproot_spend_info().output_key(), self.network) + } +} diff --git a/crates/goat/src/connectors/connector_z.rs b/crates/goat/src/connectors/connector_z.rs new file mode 100644 index 00000000..7d443f19 --- /dev/null +++ b/crates/goat/src/connectors/connector_z.rs @@ -0,0 +1,85 @@ +use bitcoin::{ + taproot::{TaprootBuilder, TaprootSpendInfo}, + Address, Network, ScriptBuf, TxIn, XOnlyPublicKey, +}; +use secp256k1::SECP256K1; +use serde::{Deserialize, Serialize}; + +use crate::constants::TimelockConfig; + +use super::{ + super::{scripts::*, transactions::base::Input}, + base::*, +}; + +#[derive(Serialize, Deserialize, Eq, PartialEq, Clone)] +pub struct ConnectorZ { + pub network: Network, + pub n_of_n_taproot_public_key: XOnlyPublicKey, + pub user_taproot_public_key: XOnlyPublicKey, + pub refund_blocks_timelock: u32, +} + +impl ConnectorZ { + pub fn new( + network: Network, + n_of_n_taproot_public_key: &XOnlyPublicKey, + user_taproot_public_key: &XOnlyPublicKey, + timelock_config: &TimelockConfig, + ) -> Self { + ConnectorZ { + network, + n_of_n_taproot_public_key: *n_of_n_taproot_public_key, + user_taproot_public_key: *user_taproot_public_key, + refund_blocks_timelock: timelock_config.connector_z, + } + } + + fn generate_taproot_leaf_0_script(&self) -> ScriptBuf { + generate_pay_to_pubkey_taproot_script(&self.n_of_n_taproot_public_key) + } + + fn generate_taproot_leaf_0_tx_in(&self, input: &Input) -> TxIn { + generate_default_tx_in(input) + } + + fn generate_taproot_leaf_1_script(&self) -> ScriptBuf { + generate_timelock_taproot_script(&self.user_taproot_public_key, self.refund_blocks_timelock) + } + + fn generate_taproot_leaf_1_tx_in(&self, input: &Input) -> TxIn { + generate_timelock_tx_in(input, self.refund_blocks_timelock) + } +} + +impl TaprootConnector for ConnectorZ { + fn generate_taproot_leaf_script(&self, leaf_index: u32) -> ScriptBuf { + match leaf_index { + 0 => self.generate_taproot_leaf_0_script(), + 1 => self.generate_taproot_leaf_1_script(), + _ => panic!("Invalid leaf index."), + } + } + + fn generate_taproot_leaf_tx_in(&self, leaf_index: u32, input: &Input) -> TxIn { + match leaf_index { + 0 => self.generate_taproot_leaf_0_tx_in(input), + 1 => self.generate_taproot_leaf_1_tx_in(input), + _ => panic!("Invalid leaf index."), + } + } + + fn generate_taproot_spend_info(&self) -> TaprootSpendInfo { + TaprootBuilder::new() + .add_leaf(1, self.generate_taproot_leaf_0_script()) + .expect("Unable to add leaf 0") + .add_leaf(1, self.generate_taproot_leaf_1_script()) + .expect("Unable to add leaf 1") + .finalize(SECP256K1, self.n_of_n_taproot_public_key) + .expect("Unable to finalize taproot") + } + + fn generate_taproot_address(&self) -> Address { + Address::p2tr_tweaked(self.generate_taproot_spend_info().output_key(), self.network) + } +} diff --git a/crates/goat/src/connectors/kickoff_connectors.rs b/crates/goat/src/connectors/kickoff_connectors.rs new file mode 100644 index 00000000..9ca00cbb --- /dev/null +++ b/crates/goat/src/connectors/kickoff_connectors.rs @@ -0,0 +1,206 @@ +use bitcoin::{ + taproot::{TaprootBuilder, TaprootSpendInfo}, + Address, Network, ScriptBuf, TxIn, XOnlyPublicKey, +}; +use secp256k1::SECP256K1; +use serde::{Deserialize, Serialize}; + +use super::{ + super::{scripts::*, transactions::base::Input}, + base::*, +}; + +#[derive(Serialize, Deserialize, Eq, PartialEq, Clone)] +pub struct GuardianConnector { + pub network: Network, + pub operator_taproot_public_key: XOnlyPublicKey, +} + +impl GuardianConnector { + pub fn new(network: Network, operator_taproot_public_key: &XOnlyPublicKey) -> Self { + GuardianConnector { network, operator_taproot_public_key: *operator_taproot_public_key } + } + + fn generate_taproot_leaf_0_script(&self) -> ScriptBuf { + generate_pay_to_pubkey_taproot_script(&self.operator_taproot_public_key) + } + + fn generate_taproot_leaf_0_tx_in(&self, input: &Input) -> TxIn { + generate_default_tx_in(input) + } +} + +impl TaprootConnector for GuardianConnector { + fn generate_taproot_leaf_script(&self, leaf_index: u32) -> ScriptBuf { + match leaf_index { + 0 => self.generate_taproot_leaf_0_script(), + _ => panic!("Invalid leaf index."), + } + } + + fn generate_taproot_leaf_tx_in(&self, leaf_index: u32, input: &Input) -> TxIn { + match leaf_index { + 0 => self.generate_taproot_leaf_0_tx_in(input), + _ => panic!("Invalid leaf index."), + } + } + + fn generate_taproot_spend_info(&self) -> TaprootSpendInfo { + TaprootBuilder::new() + .add_leaf(0, self.generate_taproot_leaf_0_script()) + .expect("Unable to add leaf 0") + .finalize(SECP256K1, self.operator_taproot_public_key) + .expect("Unable to finalize taproot") + } + + fn generate_taproot_address(&self) -> Address { + Address::p2tr_tweaked(self.generate_taproot_spend_info().output_key(), self.network) + } +} + +#[derive(Serialize, Deserialize, Eq, PartialEq, Clone)] +pub struct PrekickoffConnector { + pub network: Network, + pub operator_taproot_public_key: XOnlyPublicKey, +} + +impl PrekickoffConnector { + pub fn new(network: Network, operator_taproot_public_key: &XOnlyPublicKey) -> Self { + PrekickoffConnector { network, operator_taproot_public_key: *operator_taproot_public_key } + } + + fn generate_taproot_leaf_0_script(&self) -> ScriptBuf { + generate_pay_to_pubkey_taproot_script(&self.operator_taproot_public_key) + } + + fn generate_taproot_leaf_0_tx_in(&self, input: &Input) -> TxIn { + generate_default_tx_in(input) + } +} + +impl TaprootConnector for PrekickoffConnector { + fn generate_taproot_leaf_script(&self, leaf_index: u32) -> ScriptBuf { + match leaf_index { + 0 => self.generate_taproot_leaf_0_script(), + _ => panic!("Invalid leaf index."), + } + } + + fn generate_taproot_leaf_tx_in(&self, leaf_index: u32, input: &Input) -> TxIn { + match leaf_index { + 0 => self.generate_taproot_leaf_0_tx_in(input), + _ => panic!("Invalid leaf index."), + } + } + + fn generate_taproot_spend_info(&self) -> TaprootSpendInfo { + TaprootBuilder::new() + .add_leaf(0, self.generate_taproot_leaf_0_script()) + .expect("Unable to add leaf 0") + .finalize(SECP256K1, self.operator_taproot_public_key) + .expect("Unable to finalize taproot") + } + + fn generate_taproot_address(&self) -> Address { + Address::p2tr_tweaked(self.generate_taproot_spend_info().output_key(), self.network) + } +} + +#[derive(Serialize, Deserialize, Eq, PartialEq, Clone)] +pub struct KickoffConnector { + pub network: Network, + pub operator_taproot_public_key: XOnlyPublicKey, +} + +impl KickoffConnector { + pub fn new(network: Network, operator_taproot_public_key: &XOnlyPublicKey) -> Self { + KickoffConnector { network, operator_taproot_public_key: *operator_taproot_public_key } + } + + fn generate_taproot_leaf_0_script(&self) -> ScriptBuf { + generate_pay_to_pubkey_taproot_script(&self.operator_taproot_public_key) + } + + fn generate_taproot_leaf_0_tx_in(&self, input: &Input) -> TxIn { + generate_default_tx_in(input) + } +} + +impl TaprootConnector for KickoffConnector { + fn generate_taproot_leaf_script(&self, leaf_index: u32) -> ScriptBuf { + match leaf_index { + 0 => self.generate_taproot_leaf_0_script(), + _ => panic!("Invalid leaf index."), + } + } + + fn generate_taproot_leaf_tx_in(&self, leaf_index: u32, input: &Input) -> TxIn { + match leaf_index { + 0 => self.generate_taproot_leaf_0_tx_in(input), + _ => panic!("Invalid leaf index."), + } + } + + fn generate_taproot_spend_info(&self) -> TaprootSpendInfo { + TaprootBuilder::new() + .add_leaf(0, self.generate_taproot_leaf_0_script()) + .expect("Unable to add leaf 0") + .finalize(SECP256K1, self.operator_taproot_public_key) + .expect("Unable to finalize taproot") + } + + fn generate_taproot_address(&self) -> Address { + Address::p2tr_tweaked(self.generate_taproot_spend_info().output_key(), self.network) + } +} + +/// Output 0 of the next Pre-Kickoff, spent only by the committee-signed +/// Quick-Challenge or Force-Skip-Kickoff. The operator holds no key to it, so +/// it cannot sweep the output and make those penalties unspendable. +#[derive(Serialize, Deserialize, Eq, PartialEq, Clone)] +pub struct ForceSkipConnector { + pub network: Network, + pub n_of_n_taproot_public_key: XOnlyPublicKey, +} + +impl ForceSkipConnector { + pub fn new(network: Network, n_of_n_taproot_public_key: &XOnlyPublicKey) -> Self { + ForceSkipConnector { network, n_of_n_taproot_public_key: *n_of_n_taproot_public_key } + } + + fn generate_taproot_leaf_0_script(&self) -> ScriptBuf { + generate_pay_to_pubkey_taproot_script(&self.n_of_n_taproot_public_key) + } + + fn generate_taproot_leaf_0_tx_in(&self, input: &Input) -> TxIn { + generate_default_tx_in(input) + } +} + +impl TaprootConnector for ForceSkipConnector { + fn generate_taproot_leaf_script(&self, leaf_index: u32) -> ScriptBuf { + match leaf_index { + 0 => self.generate_taproot_leaf_0_script(), + _ => panic!("Invalid leaf index."), + } + } + + fn generate_taproot_leaf_tx_in(&self, leaf_index: u32, input: &Input) -> TxIn { + match leaf_index { + 0 => self.generate_taproot_leaf_0_tx_in(input), + _ => panic!("Invalid leaf index."), + } + } + + fn generate_taproot_spend_info(&self) -> TaprootSpendInfo { + TaprootBuilder::new() + .add_leaf(0, self.generate_taproot_leaf_0_script()) + .expect("Unable to add leaf 0") + .finalize(SECP256K1, self.n_of_n_taproot_public_key) + .expect("Unable to finalize taproot") + } + + fn generate_taproot_address(&self) -> Address { + Address::p2tr_tweaked(self.generate_taproot_spend_info().output_key(), self.network) + } +} diff --git a/crates/goat/src/connectors/mod.rs b/crates/goat/src/connectors/mod.rs new file mode 100644 index 00000000..8c6dbbba --- /dev/null +++ b/crates/goat/src/connectors/mod.rs @@ -0,0 +1,12 @@ +pub mod assert_connectors; +pub mod base; +pub mod connector_0; +pub mod connector_a; +pub mod connector_b; +pub mod connector_c; +pub mod connector_d; +pub mod connector_e; +pub mod connector_f; +pub mod connector_z; +pub mod kickoff_connectors; +pub mod watchtower_connectors; diff --git a/crates/goat/src/connectors/watchtower_connectors.rs b/crates/goat/src/connectors/watchtower_connectors.rs new file mode 100644 index 00000000..667c66ec --- /dev/null +++ b/crates/goat/src/connectors/watchtower_connectors.rs @@ -0,0 +1,197 @@ +use bitcoin::{ + taproot::{TaprootBuilder, TaprootSpendInfo}, + Address, Network, ScriptBuf, TxIn, XOnlyPublicKey, +}; +use bitvm::{chunk::api::type_conversion_utils::script_to_witness, treepp::*}; +use secp256k1::SECP256K1; +use serde::{Deserialize, Serialize}; + +use crate::{assert_scripts::LabelHash, constants::TimelockConfig}; + +use super::{ + super::{error::Error, scripts::*, transactions::base::Input}, + base::*, +}; + +pub type WatchtowerConnectors = (WatchtowerChallengeConnector, AckConnector); + +#[derive(Serialize, Deserialize, Eq, PartialEq, Clone)] +pub struct WatchtowerChallengeConnector { + pub network: Network, + pub operator_taproot_public_key: XOnlyPublicKey, + pub watchtower_taproot_public_key: XOnlyPublicKey, + pub watchtower_challenge_blocks_timelock: u32, +} +impl WatchtowerChallengeConnector { + pub fn new( + network: Network, + operator_taproot_public_key: &XOnlyPublicKey, + watchtower_taproot_public_key: &XOnlyPublicKey, + timelock_config: &TimelockConfig, + ) -> Self { + WatchtowerChallengeConnector { + network, + operator_taproot_public_key: *operator_taproot_public_key, + watchtower_taproot_public_key: *watchtower_taproot_public_key, + watchtower_challenge_blocks_timelock: timelock_config.watchtower_challenge, + } + } + + fn generate_taproot_leaf_0_script(&self) -> ScriptBuf { + // for Watchtower-Challenge tx + generate_pay_to_pubkey_taproot_script(&self.watchtower_taproot_public_key) + } + + fn generate_taproot_leaf_0_tx_in(&self, input: &Input) -> TxIn { + generate_default_tx_in(input) + } + + fn generate_taproot_leaf_1_script(&self) -> ScriptBuf { + // for Watchtower-Challenge-Timeout tx + generate_timelock_taproot_script( + &self.operator_taproot_public_key, + self.watchtower_challenge_blocks_timelock, + ) + } + + fn generate_taproot_leaf_1_tx_in(&self, input: &Input) -> TxIn { + generate_timelock_tx_in(input, self.watchtower_challenge_blocks_timelock) + } +} +impl TaprootConnector for WatchtowerChallengeConnector { + fn generate_taproot_leaf_script(&self, leaf_index: u32) -> ScriptBuf { + match leaf_index { + 0 => self.generate_taproot_leaf_0_script(), + 1 => self.generate_taproot_leaf_1_script(), + _ => panic!("Invalid leaf index."), + } + } + + fn generate_taproot_leaf_tx_in(&self, leaf_index: u32, input: &Input) -> TxIn { + match leaf_index { + 0 => self.generate_taproot_leaf_0_tx_in(input), + 1 => self.generate_taproot_leaf_1_tx_in(input), + _ => panic!("Invalid leaf index."), + } + } + + fn generate_taproot_spend_info(&self) -> TaprootSpendInfo { + TaprootBuilder::new() + .add_leaf(1, self.generate_taproot_leaf_0_script()) + .expect("Unable to add leaf 0") + .add_leaf(1, self.generate_taproot_leaf_1_script()) + .expect("Unable to add leaf 1") + .finalize(SECP256K1, self.watchtower_taproot_public_key) + .expect("Unable to finalize taproot") + } + + fn generate_taproot_address(&self) -> Address { + Address::p2tr_tweaked(self.generate_taproot_spend_info().output_key(), self.network) + } +} + +#[derive(Serialize, Deserialize, Eq, PartialEq, Clone)] +pub struct AckConnector { + pub network: Network, + pub n_of_n_taproot_public_key: XOnlyPublicKey, + pub hashlock: LabelHash, + pub ack_blocks_timelock: u32, +} + +impl AckConnector { + pub fn new( + network: Network, + n_of_n_taproot_public_key: &XOnlyPublicKey, + hashlock: LabelHash, + timelock_config: &TimelockConfig, + ) -> Self { + AckConnector { + network, + n_of_n_taproot_public_key: *n_of_n_taproot_public_key, + ack_blocks_timelock: timelock_config.operator_ack, + hashlock, + } + } + + fn generate_taproot_leaf_0_script(&self) -> ScriptBuf { + // for Watchtower-Challenge-Timeout tx + generate_pay_to_pubkey_taproot_script(&self.n_of_n_taproot_public_key) + } + + fn generate_taproot_leaf_0_tx_in(&self, input: &Input) -> TxIn { + generate_default_tx_in(input) + } + + fn generate_taproot_leaf_1_script(&self) -> ScriptBuf { + // for Operator-Challenge-ACK tx + script! { + OP_HASH160 + { self.hashlock.to_vec() } + OP_EQUALVERIFY + OP_TRUE + } + .compile() + } + + pub fn generate_leaf_1_unlock_data(&self, preimage: &[u8]) -> Result>, Error> { + let witness_script = script! { + { preimage.to_vec() } + }; + let witness = script_to_witness(witness_script.clone()); + let verification_script = witness_script.push_script(self.generate_taproot_leaf_1_script()); + let exec_result = execute_script(verification_script); + match exec_result.success { + true => Ok(witness), + false => Err(Error::Other("Invalid preimage for ACK connector.")), + } + } + + fn generate_taproot_leaf_1_tx_in(&self, input: &Input) -> TxIn { + generate_default_tx_in(input) + } + + fn generate_taproot_leaf_2_script(&self) -> ScriptBuf { + // for Operator-Challenge-NACK tx + generate_timelock_taproot_script(&self.n_of_n_taproot_public_key, self.ack_blocks_timelock) + } + + fn generate_taproot_leaf_2_tx_in(&self, input: &Input) -> TxIn { + generate_timelock_tx_in(input, self.ack_blocks_timelock) + } +} + +impl TaprootConnector for AckConnector { + fn generate_taproot_leaf_script(&self, leaf_index: u32) -> ScriptBuf { + match leaf_index { + 0 => self.generate_taproot_leaf_0_script(), + 1 => self.generate_taproot_leaf_1_script(), + 2 => self.generate_taproot_leaf_2_script(), + _ => panic!("Invalid leaf index."), + } + } + + fn generate_taproot_leaf_tx_in(&self, leaf_index: u32, input: &Input) -> TxIn { + match leaf_index { + 0 => self.generate_taproot_leaf_0_tx_in(input), + 1 => self.generate_taproot_leaf_1_tx_in(input), + 2 => self.generate_taproot_leaf_2_tx_in(input), + _ => panic!("Invalid leaf index."), + } + } + + fn generate_taproot_spend_info(&self) -> TaprootSpendInfo { + TaprootBuilder::new() + .add_leaf(2, self.generate_taproot_leaf_0_script()) + .expect("Unable to add leaf 0") + .add_leaf(2, self.generate_taproot_leaf_1_script()) + .expect("Unable to add leaf 1") + .add_leaf(1, self.generate_taproot_leaf_2_script()) + .expect("Unable to add leaf 2") + .finalize(SECP256K1, self.n_of_n_taproot_public_key) + .expect("Unable to finalize taproot") + } + + fn generate_taproot_address(&self) -> Address { + Address::p2tr_tweaked(self.generate_taproot_spend_info().output_key(), self.network) + } +} diff --git a/crates/goat/src/constants.rs b/crates/goat/src/constants.rs new file mode 100644 index 00000000..c16adebb --- /dev/null +++ b/crates/goat/src/constants.rs @@ -0,0 +1,44 @@ +use serde::{Deserialize, Serialize}; + +pub const N_SEQUENCE_FOR_LOCK_TIME: u32 = 0xFFFFFFFE; // The nSequence field must be set to less than 0xffffffff, usually 0xffffffff-1 to avoid confilcts with relative timelocks. + +/// Connector timelock values in bitcoin blocks. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct TimelockConfig { + pub connector_z: u32, + pub connector_a: u32, + pub prover_connector: u32, + pub connector_d: u32, + pub watchtower_challenge: u32, + pub operator_ack: u32, + pub operator_commit: u32, + pub connector_f: u32, +} + +impl TimelockConfig { + #[allow(clippy::too_many_arguments)] + pub const fn new( + connector_z: u32, + connector_a: u32, + prover_connector: u32, + connector_d: u32, + watchtower_challenge: u32, + operator_ack: u32, + operator_commit: u32, + connector_f: u32, + ) -> Self { + TimelockConfig { + connector_z, + connector_a, + prover_connector, + connector_d, + watchtower_challenge, + operator_ack, + operator_commit, + connector_f, + } + } +} + +// Commitment message parameters. Hardcoded number of bytes per message. +pub const BITCOIN_TXID_LENGTH: usize = 32; diff --git a/crates/goat/src/contexts/base.rs b/crates/goat/src/contexts/base.rs new file mode 100644 index 00000000..13eaf7f6 --- /dev/null +++ b/crates/goat/src/contexts/base.rs @@ -0,0 +1,34 @@ +use bitcoin::{ + key::Keypair, secp256k1::PublicKey as Secp256k1PublicKey, Network, PrivateKey, PublicKey, + XOnlyPublicKey, +}; +use musig2::{secp::Point, KeyAggContext}; +use secp256k1::SECP256K1; + +pub trait BaseContext { + fn network(&self) -> Network; + fn n_of_n_public_keys(&self) -> &Vec; + fn n_of_n_public_key(&self) -> &PublicKey; + fn n_of_n_taproot_public_key(&self) -> &XOnlyPublicKey; +} + +pub fn generate_keys_from_secret(network: Network, secret: &str) -> (Keypair, PublicKey) { + let keypair = Keypair::from_seckey_str_global(secret).unwrap(); + let private_key = PrivateKey::new(keypair.secret_key(), network); + let public_key = PublicKey::from_private_key(SECP256K1, &private_key); + + (keypair, public_key) +} + +pub fn generate_n_of_n_public_key(n_of_n_public_keys: &[PublicKey]) -> (PublicKey, XOnlyPublicKey) { + let public_keys: Vec = + n_of_n_public_keys.iter().map(|&public_key| public_key.inner.into()).collect(); + + let key_agg_context = KeyAggContext::new(public_keys).unwrap(); + let aggregated_key: Secp256k1PublicKey = key_agg_context.aggregated_pubkey(); + + let n_of_n_public_key = PublicKey::from(aggregated_key); + let n_of_n_taproot_public_key = XOnlyPublicKey::from(n_of_n_public_key); + + (n_of_n_public_key, n_of_n_taproot_public_key) +} diff --git a/crates/goat/src/contexts/committee.rs b/crates/goat/src/contexts/committee.rs new file mode 100644 index 00000000..e42871cd --- /dev/null +++ b/crates/goat/src/contexts/committee.rs @@ -0,0 +1,49 @@ +use bitcoin::{key::Keypair, Network, PublicKey, XOnlyPublicKey}; + +use super::base::{generate_keys_from_secret, generate_n_of_n_public_key, BaseContext}; + +#[derive(Debug)] +pub struct CommitteeContext { + pub network: Network, + + pub committee_keypair: Keypair, + pub committee_public_key: PublicKey, + + pub n_of_n_public_keys: Vec, + pub n_of_n_public_key: PublicKey, + pub n_of_n_taproot_public_key: XOnlyPublicKey, +} + +impl BaseContext for CommitteeContext { + fn network(&self) -> Network { + self.network + } + fn n_of_n_public_keys(&self) -> &Vec { + &self.n_of_n_public_keys + } + fn n_of_n_public_key(&self) -> &PublicKey { + &self.n_of_n_public_key + } + fn n_of_n_taproot_public_key(&self) -> &XOnlyPublicKey { + &self.n_of_n_taproot_public_key + } +} + +impl CommitteeContext { + pub fn new(network: Network, committee_secret: &str, n_of_n_public_keys: &[PublicKey]) -> Self { + let (keypair, public_key) = generate_keys_from_secret(network, committee_secret); + let (n_of_n_public_key, n_of_n_taproot_public_key) = + generate_n_of_n_public_key(n_of_n_public_keys); + + CommitteeContext { + network, + + committee_keypair: keypair, + committee_public_key: public_key, + + n_of_n_public_keys: n_of_n_public_keys.to_owned(), + n_of_n_public_key, + n_of_n_taproot_public_key, + } + } +} diff --git a/crates/goat/src/contexts/mod.rs b/crates/goat/src/contexts/mod.rs new file mode 100644 index 00000000..7f0cec2c --- /dev/null +++ b/crates/goat/src/contexts/mod.rs @@ -0,0 +1,3 @@ +pub mod base; +pub mod committee; +pub mod operator; diff --git a/crates/goat/src/contexts/operator.rs b/crates/goat/src/contexts/operator.rs new file mode 100644 index 00000000..52efa624 --- /dev/null +++ b/crates/goat/src/contexts/operator.rs @@ -0,0 +1,51 @@ +use bitcoin::{key::Keypair, Network, PublicKey, XOnlyPublicKey}; + +use super::base::{generate_keys_from_secret, generate_n_of_n_public_key, BaseContext}; + +#[derive(Debug)] +pub struct OperatorContext { + pub network: Network, + + pub operator_keypair: Keypair, + pub operator_public_key: PublicKey, + pub operator_taproot_public_key: XOnlyPublicKey, + + pub n_of_n_public_keys: Vec, + pub n_of_n_public_key: PublicKey, + pub n_of_n_taproot_public_key: XOnlyPublicKey, +} + +impl BaseContext for OperatorContext { + fn network(&self) -> Network { + self.network + } + fn n_of_n_public_keys(&self) -> &Vec { + &self.n_of_n_public_keys + } + fn n_of_n_public_key(&self) -> &PublicKey { + &self.n_of_n_public_key + } + fn n_of_n_taproot_public_key(&self) -> &XOnlyPublicKey { + &self.n_of_n_taproot_public_key + } +} + +impl OperatorContext { + pub fn new(network: Network, operator_secret: &str, n_of_n_public_keys: &[PublicKey]) -> Self { + let (keypair, public_key) = generate_keys_from_secret(network, operator_secret); + let (n_of_n_public_key, n_of_n_taproot_public_key) = + generate_n_of_n_public_key(n_of_n_public_keys); + + OperatorContext { + network, + + operator_keypair: keypair, + operator_public_key: public_key, + operator_taproot_public_key: XOnlyPublicKey::from(public_key), + + n_of_n_public_keys: n_of_n_public_keys.to_owned(), + n_of_n_public_key, + n_of_n_taproot_public_key, + } + } +} diff --git a/crates/goat/src/error.rs b/crates/goat/src/error.rs new file mode 100644 index 00000000..9d2b279f --- /dev/null +++ b/crates/goat/src/error.rs @@ -0,0 +1,47 @@ +use super::transactions::{base::BaseTransaction, pre_signed::PreSignedTransaction}; +use bitcoin::Txid; +use std::fmt; +use strum::Display; + +#[derive(Debug)] +pub struct NamedTx { + pub txid: Txid, + pub name: &'static str, + pub confirmed: bool, +} + +impl NamedTx { + pub fn for_tx(tx: &(impl BaseTransaction + PreSignedTransaction), confirmed: bool) -> Self { + Self { txid: tx.tx().compute_txid(), name: tx.name(), confirmed } + } +} + +#[derive(Debug, Display)] +pub enum ConnectorError { + ConnectorCCommitsPublicKeyEmpty, +} + +#[derive(Debug)] +pub enum TransactionError { + AlreadyMined(Txid), + InsufficientInputAmount, +} + +#[derive(Debug)] +pub enum ChunkerError { + ValidProof, +} + +#[derive(Debug)] +pub enum Error { + Transaction(TransactionError), + Chunker(ChunkerError), + Connector(ConnectorError), + Other(&'static str), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} diff --git a/crates/goat/src/lib.rs b/crates/goat/src/lib.rs new file mode 100644 index 00000000..d083f6e7 --- /dev/null +++ b/crates/goat/src/lib.rs @@ -0,0 +1,10 @@ +pub mod assert_scripts; +pub mod connectors; +pub mod constants; +pub mod contexts; +pub mod error; +pub mod pubin_disprove_scripts; +pub mod scripts; +pub mod transactions; +pub mod utils; +pub mod wots; diff --git a/crates/goat/src/pubin_disprove_scripts.rs b/crates/goat/src/pubin_disprove_scripts.rs new file mode 100644 index 00000000..0467ebe9 --- /dev/null +++ b/crates/goat/src/pubin_disprove_scripts.rs @@ -0,0 +1,599 @@ +use crate::{ + assert_scripts::{ + LabelHash, OperatorAssertPublicKey, OperatorCommitPubinPublicKey, OPERATOR_ASSERT_X_D_INDEX, + }, + wots::{Wots, Wots96}, +}; +use bitvm::{bigint::U256, hash::blake3::blake3_compute_script_with_limb, treepp::*}; + +pub const GUEST_PUBIN_NUM: usize = 3; +pub const GUEST_PUBIN_BLOCKHASH_INDEX: usize = 0; +pub const GUEST_PUBIN_CONSTANT_INDEX: usize = 1; +pub const GUEST_PUBIN_INCLUDED_MAP_INDEX: usize = 2; + +pub fn verify_guest_pubin_commitment( + guest_pubin_wots_pubkey: &OperatorCommitPubinPublicKey, + operator_assert_wots_pubkey: &OperatorAssertPublicKey, + constant_value: &[u8; 32], + hashes: &[LabelHash], +) -> Script { + let wots96_msg_stack_items_num = Wots96::MSG_BYTE_LEN as usize; + let wots96_sig_stack_items_num = Wots96::TOTAL_DIGIT_LEN as usize * 2; + + script! { + { Wots96::checksig_verify(guest_pubin_wots_pubkey) } + + { 1 } OP_TOALTSTACK + + { verify_hashlock_pubin_segment_dup(GUEST_PUBIN_INCLUDED_MAP_INDEX, hashes) } + OP_FROMALTSTACK OP_BOOLAND OP_TOALTSTACK + + { verify_constant_pubin_segment_dup(GUEST_PUBIN_CONSTANT_INDEX, constant_value) } + OP_FROMALTSTACK OP_BOOLAND OP_TOALTSTACK + + // Consume the operator assertion WOTS witness before computing the guest + // pubin commitment. Leaving its 196 raw witness items on the stack while + // SHA256 runs pushes the combined stack over Bitcoin's 1000-item limit. + { roll_n(wots96_msg_stack_items_num, wots96_sig_stack_items_num) } + { Wots96::checksig_verify(operator_assert_wots_pubkey) } + + // Keep only x_d from the 96-byte operator assertion message. The other + // 64 bytes are pi1.x and pi1.y and are not needed by PubinDisprove. + { lift_and_reverse_bytes(OPERATOR_ASSERT_X_D_INDEX * 32, 32) } + { roll_n(32, OPERATOR_ASSERT_X_D_INDEX * 32) } + for _ in 0..(OPERATOR_ASSERT_X_D_INDEX * 32) { + OP_DROP + } + + // Move the 96-byte guest pubin above x_d and compute its commitment. + // BLAKE3 requires the main stack to contain only its compact message, + // so keep x_d on the alt stack until the digest is available. + { roll_n(32, wots96_msg_stack_items_num) } + for _ in 0..32 { + { wots96_msg_stack_items_num } OP_ROLL OP_TOALTSTACK + } + { generate_guest_pubin_commitment(GUEST_PUBIN_NUM as u32) } + for _ in 0..32 { + OP_FROMALTSTACK + } + // Restore x_d below the 64-nibble digest, preserving the old comparison + // stack layout: x_d | guest pubin commitment (top). + { roll_n(32, 64) } + { zip_nibbles_bytes32() } + + { 1 } + for i in (0..32).rev() { + OP_SWAP { i + 2 } OP_ROLL OP_NUMEQUAL OP_BOOLAND + } + + // Pubin-Disprove succeeds when the guest pubin commitment differs from + // x_d in OperatorAssertTransaction, i.e. Prover-Assert in the protocol + // graph. The operator/prover assert pubin layout is: + // pi1.x LE-32 | pi1.y LE-32 | x_d LE-32, and x_d is the + // guest-pubin commitment encoded as a Groth16 public input. The + // generated commitment is also left in LE field-byte order here. + OP_FROMALTSTACK + OP_BOOLAND + OP_NOT + } +} + +pub fn verify_constant_pubin_segment_dup( + segment_index: usize, + constant_value: &[u8; 32], +) -> Script { + script! { + { copy_guest_pubin_segment_to_top(segment_index) } + { verify_constant_pubin_script_inner(constant_value) } + } +} + +pub fn verify_constant_pubin_script_inner(constant_value: &[u8; 32]) -> Script { + script! { + { 1 } + for byte in *constant_value { + OP_SWAP + { byte } + OP_NUMEQUAL + OP_BOOLAND + } + } +} + +pub fn verify_hashlock_pubin_segment_dup(segment_index: usize, hashes: &[LabelHash]) -> Script { + let preimage_roll_depths = hashlock_preimage_roll_depths(hashes.len()); + script! { + for depth in preimage_roll_depths { + { depth } OP_ROLL OP_TOALTSTACK + } + { copy_guest_pubin_segment_to_top(segment_index) } + { verify_hashlock_pubin_script_inner(hashes) } + } +} + +fn hashlock_preimage_roll_depths(hash_count: usize) -> Vec { + let mut consume_order = Vec::with_capacity(hash_count); + for chunk_start in (0..hash_count).step_by(8) { + let chunk_end = (chunk_start + 8).min(hash_count); + for index in (chunk_start..chunk_end).rev() { + consume_order.push(index); + } + } + + let mut remaining: Vec = (0..hash_count).collect(); + consume_order + .into_iter() + .rev() + .map(|target| { + let position = remaining + .iter() + .position(|index| *index == target) + .expect("target preimage index must exist"); + let depth = Wots96::MSG_BYTE_LEN as usize + remaining.len() - 1 - position; + remaining.remove(position); + depth + }) + .collect() +} + +fn verify_hashlock_pubin_script_inner(hashes: &[LabelHash]) -> Script { + fn chunk_count(len: usize, chunk_size: usize) -> usize { + len.div_ceil(chunk_size) + } + + script! { + { 1 } + for chunk in hashes.chunks(8) { + OP_SWAP + for i in (1..8).rev() { + if i >= chunk.len() { + { 1 << i } OP_2DUP + OP_GREATERTHANOREQUAL + OP_IF + OP_SUB + OP_ELSE + OP_DROP + OP_ENDIF + } else { + { 1 << i } OP_2DUP + OP_GREATERTHANOREQUAL + OP_IF + OP_SUB OP_FROMALTSTACK OP_DROP + OP_ELSE + OP_DROP + OP_SWAP + OP_FROMALTSTACK + OP_HASH160 + { chunk[i].to_vec() } + OP_EQUAL + OP_NOT + OP_BOOLAND + OP_SWAP + OP_ENDIF + } + } + OP_IF + OP_FROMALTSTACK OP_DROP + OP_ELSE + OP_FROMALTSTACK + OP_HASH160 + { chunk[0].to_vec() } + OP_EQUAL + OP_NOT + OP_BOOLAND + OP_ENDIF + } + for _ in 0..(32 - chunk_count(hashes.len(), 8)) { + OP_NIP + } + } +} + +fn copy_guest_pubin_segment_to_top(segment_index: usize) -> Script { + script! { + for i in 0..32 { + { segment_index * 32 + i } OP_PICK + OP_TOALTSTACK + } + for _ in 0..32 { + OP_FROMALTSTACK + } + } +} + +fn generate_guest_pubin_blake3_input() -> Script { + script! { + // Build the four U256s in reverse final-stack order on the alt stack. + // The witness has byte 0 at the top. Roll block0.part1 out first, then + // consume block0.part0 and block1.part0 from the top of the main stack. + // Restoring the four U256s leaves the BLAKE3 format required by the + // helper: block1.p0 | block1.p1 | block0.p0 | block0.p1 (top). + + // block 0, part 1. + for word_index in 0..8 { + for _ in 0..4 { + { 32 + 4 * word_index + 3 } OP_ROLL + } + } + { bytes32_to_u4() } + for _ in 0..64 { + OP_TOALTSTACK + } + + // block 0, part 0. + for word_index in 0..8 { + for _ in 0..4 { + { 4 * word_index + 3 } OP_ROLL + } + } + { bytes32_to_u4() } + for _ in 0..64 { + OP_TOALTSTACK + } + + // block 1, part 1: zero padding in compact nibble form. + for _ in 0..32 { + OP_0 OP_0 + } + for _ in 0..64 { + OP_TOALTSTACK + } + + // block 1, part 0. + for word_index in 0..8 { + for _ in 0..4 { + { 4 * word_index + 3 } OP_ROLL + } + } + { bytes32_to_u4() } + for _ in 0..64 { + OP_TOALTSTACK + } + + for _ in 0..4 * 64 { + OP_FROMALTSTACK + } + } +} + +pub fn generate_guest_pubin_commitment(guest_pubin_num: u32) -> Script { + assert_eq!(guest_pubin_num, GUEST_PUBIN_NUM as u32); + + script! { + { generate_guest_pubin_blake3_input() } + { blake3_compute_script_with_limb(GUEST_PUBIN_NUM * 32, 4) } + { reverse_bytes_u4(32) } + OP_SWAP { mod2_u4() } OP_SWAP + } +} + +fn roll(d: usize) -> Script { + match d { + 0 => script! {}, + 1 => script! { OP_SWAP }, + 2 => script! { OP_ROT }, + _ => script! { { d } OP_ROLL }, + } +} + +fn roll_n(depth: usize, num_items: usize) -> Script { + script! { + for _ in 0..num_items { + { roll(depth + num_items - 1) } + } + } +} + +fn lift_and_reverse_bytes(depth: usize, num_bytes: usize) -> Script { + script! { + for i in 0..num_bytes { + { roll(depth + i) } + } + } +} + +fn bytes32_to_u4() -> Script { + script! { + { U256::transform_limbsize(8, 4) } + } +} + +fn lift_and_reverse_bytes_u4(depth: usize, num_bytes: usize) -> Script { + script! { + for i in 0..num_bytes { + { roll(depth + 2 * i + 1) } + { roll(depth + 2 * i + 1) } + } + } +} + +fn reverse_bytes_u4(num_bytes: usize) -> Script { + lift_and_reverse_bytes_u4(0, num_bytes) +} + +fn mod2_u4() -> Script { + script! { + OP_DUP + OP_8 + OP_GREATERTHANOREQUAL + OP_IF + OP_8 + OP_SUB + OP_ENDIF + + OP_DUP + OP_4 + OP_GREATERTHANOREQUAL + OP_IF + OP_4 + OP_SUB + OP_ENDIF + + OP_DUP + OP_3 + OP_EQUAL + + OP_SWAP + OP_1 + OP_EQUAL + OP_BOOLOR + } +} + +fn zip_nibbles_bytes32() -> Script { + script! { + { U256::transform_limbsize(4, 8) } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + assert_scripts::{label_hash, OPERATOR_ASSERT_X_D_INDEX}, + wots::Wots96, + }; + use bitvm::{execute_script, FmtStack}; + + fn guest_pubin(blockhash: &[u8; 32], constant: &[u8; 32], included_map: &[u8; 32]) -> [u8; 96] { + let mut msg = [0u8; 96]; + msg[0..32].copy_from_slice(blockhash); + msg[32..64].copy_from_slice(constant); + msg[64..96].copy_from_slice(included_map); + msg + } + + fn operator_assert_pubin(pi1_x: &[u8; 32], pi1_y: &[u8; 32], x_d: &[u8; 32]) -> [u8; 96] { + let mut msg = [0u8; 96]; + msg[0..32].copy_from_slice(pi1_x); + msg[32..64].copy_from_slice(pi1_y); + let x_d_start = OPERATOR_ASSERT_X_D_INDEX * 32; + msg[x_d_start..x_d_start + 32].copy_from_slice(x_d); + msg + } + + fn compute_guest_commitment(guest: &[u8; 96]) -> [u8; 32] { + let s = script! { + for byte in guest.iter().rev() { + { *byte } + } + { generate_guest_pubin_commitment(GUEST_PUBIN_NUM as u32) } + { zip_nibbles_bytes32() } + }; + let result = execute_script(s); + assert_eq!(result.error, None, "{result}"); + assert!(result.final_stack.len() >= 32, "{result}"); + let commitment_start = result.final_stack.len() - 32; + parse_bytes_from_stack(commitment_start, &result.final_stack) + } + + #[test] + fn test_guest_pubin_blake3_input_matches_static_builder() { + let guest: [u8; 96] = std::array::from_fn(|index| index as u8); + let secret = Wots96::generate_secret_key(); + let public_key = Wots96::generate_public_key(&secret); + let dynamic = execute_script(script! { + { Wots96::sign_to_raw_witness(&secret, &guest) } + { Wots96::checksig_verify(&public_key) } + { generate_guest_pubin_blake3_input() } + }); + let static_input = execute_script(script! { + { bitvm::hash::blake3::blake3_push_message_script_with_limb(&guest, 4) } + }); + + assert_eq!(dynamic.error, None, "{dynamic}"); + assert_eq!(static_input.error, None, "{static_input}"); + assert_eq!(dynamic.final_stack.len(), static_input.final_stack.len()); + for index in 0..dynamic.final_stack.len() { + assert_eq!( + dynamic.final_stack.get(index), + static_input.final_stack.get(index), + "different packed BLAKE3 element at stack index {index}" + ); + } + } + + #[test] + fn test_generate_guest_pubin_commitment_matches_reference_vector() { + let blockhash: [u8; 32] = + hex::decode("5a690bba0ba076d621f77665398f4b1ddbfc2349bbb3e8880307625ac5cfa900") + .unwrap() + .try_into() + .unwrap(); + let constant: [u8; 32] = + hex::decode("2df7bde0605973f5809a1338094cb47a309bc38363dd35ce178c088cd3cda79f") + .unwrap() + .try_into() + .unwrap(); + let included_map: [u8; 32] = + hex::decode("0100000000000000000000000000000000000000000000000000000000000000") + .unwrap() + .try_into() + .unwrap(); + let guest = guest_pubin(&blockhash, &constant, &included_map); + let mut expected = *blake3::hash(&guest).as_bytes(); + // Preserve the existing field-element high-bit reduction. + expected[0] &= 0x1f; + + let mut actual_be = compute_guest_commitment(&guest); + actual_be.reverse(); + + assert_eq!(actual_be, expected); + } + + fn parse_bytes_from_stack(start_index_from_bottom: usize, stack: &FmtStack) -> [u8; 32] { + let mut bytes = [0u8; 32]; + for (i, byte) in bytes.iter_mut().enumerate() { + *byte = stack_value_from_bottom(stack, start_index_from_bottom + i); + } + bytes + } + + fn stack_value_from_bottom(stack: &FmtStack, index_from_bottom: usize) -> u8 { + let element = stack.get(index_from_bottom); + if element.is_empty() { + 0 + } else { + element[0] + } + } + + fn run_pubin_disprove( + guest_msg: &[u8; 96], + operator_assert_msg: &[u8; 96], + constant: &[u8; 32], + hashes: &[LabelHash], + preimages: &[Vec], + ) -> bool { + let guest_secret = Wots96::generate_secret_key(); + let guest_public_key = Wots96::generate_public_key(&guest_secret); + let operator_assert_secret = Wots96::generate_secret_key(); + let operator_assert_public_key = Wots96::generate_public_key(&operator_assert_secret); + + let s = script! { + { Wots96::sign_to_raw_witness(&operator_assert_secret, operator_assert_msg) } + { preimages.to_vec() } + { Wots96::sign_to_raw_witness(&guest_secret, guest_msg) } + { verify_guest_pubin_commitment( + &guest_public_key, + &operator_assert_public_key, + constant, + hashes, + ) } + }; + let result = execute_script(s); + assert!( + result.stats.max_nb_stack_items <= 1000, + "PubinDisprove exceeded Bitcoin's stack limit: {}", + result.stats.max_nb_stack_items + ); + assert_eq!(result.error, None, "{result}"); + result.success + } + + #[test] + fn test_pubin_disprove_fails_when_pubin_is_valid() { + let blockhash = [0x11; 32]; + let constant = [0x22; 32]; + let mut included_map = [0u8; 32]; + included_map[0] = 0b0000_0011; + let guest_msg = guest_pubin(&blockhash, &constant, &included_map); + let x_d = compute_guest_commitment(&guest_msg); + let operator_assert_msg = operator_assert_pubin(&[0x33; 32], &[0x44; 32], &x_d); + let preimages = vec![b"included-watchtower".to_vec(), b"missing-watchtower".to_vec()]; + let hashes = vec![label_hash(&preimages[0]), label_hash(&preimages[1])]; + + assert!(!run_pubin_disprove( + &guest_msg, + &operator_assert_msg, + &constant, + &hashes, + &preimages, + )); + } + + #[test] + fn test_pubin_disprove_succeeds_on_constant_mismatch() { + let blockhash = [0x11; 32]; + let constant = [0x22; 32]; + let wrong_constant = [0x23; 32]; + let included_map = [0xff; 32]; + let guest_msg = guest_pubin(&blockhash, &wrong_constant, &included_map); + let x_d = compute_guest_commitment(&guest_msg); + let operator_assert_msg = operator_assert_pubin(&[0x33; 32], &[0x44; 32], &x_d); + let preimages = [b"included-watchtower".to_vec(), b"also-included".to_vec()]; + let nil_preimages: [Vec; 2] = [vec![], vec![]]; + let hashes = vec![label_hash(&preimages[0]), label_hash(&preimages[1])]; + + assert!(run_pubin_disprove( + &guest_msg, + &operator_assert_msg, + &constant, + &hashes, + &nil_preimages, + )); + } + + #[test] + fn test_pubin_disprove_succeeds_on_hashlock_mismatch() { + let blockhash = [0x11; 32]; + let constant = [0x22; 32]; + let included_map = [0u8; 32]; + let guest_msg = guest_pubin(&blockhash, &constant, &included_map); + let x_d = compute_guest_commitment(&guest_msg); + let operator_assert_msg = operator_assert_pubin(&[0x33; 32], &[0x44; 32], &x_d); + let preimages = vec![b"watchtower-0".to_vec(), b"watchtower-1".to_vec()]; + let hashes = vec![label_hash(&preimages[0]), label_hash(&preimages[1])]; + + assert!(run_pubin_disprove( + &guest_msg, + &operator_assert_msg, + &constant, + &hashes, + &preimages, + )); + } + + #[test] + fn test_pubin_disprove_succeeds_on_second_hashlock_byte_mismatch() { + let blockhash = [0x11; 32]; + let constant = [0x22; 32]; + let mut included_map = [0xff; 32]; + included_map[1] = 0b0000_0001; + let guest_msg = guest_pubin(&blockhash, &constant, &included_map); + let x_d = compute_guest_commitment(&guest_msg); + let operator_assert_msg = operator_assert_pubin(&[0x33; 32], &[0x44; 32], &x_d); + let preimages = + (0..10).map(|index| format!("watchtower-{index}").into_bytes()).collect::>(); + let hashes = preimages.iter().map(label_hash).collect::>(); + + assert!(run_pubin_disprove( + &guest_msg, + &operator_assert_msg, + &constant, + &hashes, + &preimages, + )); + } + + #[test] + fn test_pubin_disprove_succeeds_on_x_d_mismatch() { + let blockhash = [0x11; 32]; + let constant = [0x22; 32]; + let included_map = [0xff; 32]; + let guest_msg = guest_pubin(&blockhash, &constant, &included_map); + let mut wrong_x_d = compute_guest_commitment(&guest_msg); + wrong_x_d[0] ^= 0x01; + let operator_assert_msg = operator_assert_pubin(&[0x33; 32], &[0x44; 32], &wrong_x_d); + let preimages = vec![b"included-watchtower".to_vec(), b"also-included".to_vec()]; + let hashes = vec![label_hash(&preimages[0]), label_hash(&preimages[1])]; + + assert!(run_pubin_disprove( + &guest_msg, + &operator_assert_msg, + &constant, + &hashes, + &preimages, + )); + } +} diff --git a/crates/goat/src/scripts.rs b/crates/goat/src/scripts.rs new file mode 100644 index 00000000..de481d55 --- /dev/null +++ b/crates/goat/src/scripts.rs @@ -0,0 +1,245 @@ +use bitcoin::{ + hashes::{ripemd160::Hash as Ripemd160, sha256::Hash as Sha256, Hash}, + Address, Amount, CompressedPublicKey, Network, PubkeyHash, PublicKey, ScriptBuf, TxOut, + XOnlyPublicKey, +}; +use bitvm::treepp::script; +use std::{str::FromStr, sync::LazyLock}; + +use crate::transactions::base::{DUST_AMOUNT, P2A_AMOUNT}; + +// TODO replace these public keys +pub static UNSPENDABLE_PUBLIC_KEY: LazyLock = LazyLock::new(|| { + PublicKey::from_str( + "0405f818748aecbc8c67a4e61a03cee506888f49480cf343363b04908ed51e25b9615f244c38311983fb0f5b99e3fd52f255c5cc47a03ee2d85e78eaf6fa76bb9d" + ) + .unwrap() +}); +pub static UNSPENDABLE_TAPROOT_PUBLIC_KEY: LazyLock = LazyLock::new(|| { + XOnlyPublicKey::from_str("50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0") + .unwrap() +}); + +pub fn generate_burn_script() -> ScriptBuf { + generate_pay_to_pubkey_script(&UNSPENDABLE_PUBLIC_KEY) +} + +pub fn generate_burn_script_address(network: Network) -> Address { + Address::p2wsh(&generate_burn_script(), network) +} + +pub fn generate_burn_taproot_script() -> ScriptBuf { + generate_pay_to_pubkey_taproot_script(&UNSPENDABLE_TAPROOT_PUBLIC_KEY) +} + +pub fn generate_pay_to_pubkey_script(public_key: &PublicKey) -> ScriptBuf { + script! { + { *public_key } + OP_CHECKSIG + } + .compile() +} + +pub fn generate_pay_to_pubkey_hash_with_inscription_script( + public_key_hash: &PubkeyHash, + timestamp: u32, + evm_address: &str, +) -> ScriptBuf { + let inscription = [ + public_key_hash.as_byte_array().to_vec(), + timestamp.to_be_bytes().to_vec(), + evm_address.as_bytes().to_vec(), + ] + .concat(); + let inscription_hash = Ripemd160::hash(&Sha256::hash(&inscription).to_byte_array()); + script! { + OP_FALSE + OP_IF + { inscription_hash.to_byte_array().to_vec() } + OP_ENDIF + OP_DUP + OP_HASH160 + { public_key_hash.as_byte_array().to_vec() } + OP_EQUALVERIFY + OP_CHECKSIG + } + .compile() +} + +pub fn generate_p2pkh_address(network: Network, public_key: &PublicKey) -> Address { + Address::p2pkh( + CompressedPublicKey::try_from(*public_key).expect("Could not compress public key"), + network, + ) +} + +pub fn generate_p2wpkh_address(network: Network, public_key: &PublicKey) -> Address { + Address::p2wpkh( + &CompressedPublicKey::try_from(*public_key).expect("Could not compress public key"), + network, + ) +} + +pub fn generate_pay_to_pubkey_script_address(network: Network, public_key: &PublicKey) -> Address { + Address::p2wsh(&generate_pay_to_pubkey_script(public_key), network) +} + +pub fn generate_pay_to_pubkey_hash_with_inscription_script_address( + network: Network, + public_key_hash: &PubkeyHash, + timestamp: u32, + evm_address: &str, +) -> Address { + Address::p2wsh( + &generate_pay_to_pubkey_hash_with_inscription_script( + public_key_hash, + timestamp, + evm_address, + ), + network, + ) +} + +pub fn generate_pay_to_pubkey_taproot_script(public_key: &XOnlyPublicKey) -> ScriptBuf { + script! { + { *public_key } + OP_CHECKSIG + } + .compile() +} + +pub fn generate_pay_to_pubkey_taproot_script_address( + network: Network, + public_key: &XOnlyPublicKey, +) -> Address { + Address::p2wsh(&generate_pay_to_pubkey_taproot_script(public_key), network) +} + +pub fn generate_timelock_script(public_key: &PublicKey, num_blocks_timelock: u32) -> ScriptBuf { + script! { + { num_blocks_timelock } + OP_CSV + OP_DROP + { *public_key } + OP_CHECKSIG + } + .compile() +} + +pub fn generate_timelock_script_address( + network: Network, + public_key: &PublicKey, + num_blocks_timelock: u32, +) -> Address { + Address::p2wsh(&generate_timelock_script(public_key, num_blocks_timelock), network) +} + +pub fn generate_timelock_taproot_script( + public_key: &XOnlyPublicKey, + num_blocks_timelock: u32, +) -> ScriptBuf { + script! { + { num_blocks_timelock } + OP_CSV + OP_DROP + { *public_key } + OP_CHECKSIG + } + .compile() +} + +pub fn generate_timelock_taproot_script_address( + network: Network, + public_key: &XOnlyPublicKey, + num_blocks_timelock: u32, +) -> Address { + Address::p2wsh(&generate_timelock_taproot_script(public_key, num_blocks_timelock), network) +} + +pub fn generate_opreturn_script(msg: Vec) -> ScriptBuf { + assert!(msg.len() <= 80, "message must less than 80 bytes"); + script! { + OP_RETURN + {msg} + } + .compile() +} + +pub fn p2a_amount() -> Amount { + Amount::from_sat(P2A_AMOUNT) +} + +pub fn p2a_script() -> ScriptBuf { + ScriptBuf::from_bytes(hex::decode("51024e73").unwrap()) +} + +pub fn p2a_output() -> TxOut { + TxOut { value: p2a_amount(), script_pubkey: p2a_script() } +} + +pub fn generate_data_commitment_outputs(data: &[u8]) -> Vec { + let data_len = data.len(); + if data_len <= 80 { + vec![TxOut { value: Amount::ZERO, script_pubkey: generate_opreturn_script(data.to_vec()) }] + } else { + let mut txouts = vec![]; + let mut data = data.to_vec(); + let opreturn_len = data_len - (data_len - 80).div_ceil(32) * 32; + let opreturn_data = data.split_off(data_len - opreturn_len); + for chunk in data.chunks(32) { + txouts.push(TxOut { + value: Amount::from_sat(DUST_AMOUNT), + script_pubkey: script! { + OP_0 + { chunk.to_vec() } + } + .compile(), + }); + } + txouts.push(TxOut { + value: Amount::ZERO, + script_pubkey: generate_opreturn_script(opreturn_data), + }); + txouts + } +} + +pub fn is_valid_commitment_outputs(txouts: &[TxOut]) -> bool { + if txouts.is_empty() { + return false; + } + let last_txout = &txouts[txouts.len() - 1]; + if !last_txout.script_pubkey.is_op_return() { + return false; + } + for txout in &txouts[..txouts.len() - 1] { + if !txout.script_pubkey.is_p2wsh() { + return false; + } + } + true +} + +pub fn extract_data_from_commitment_outputs(txouts: &[TxOut]) -> Vec { + let mut data = vec![]; + for txout in txouts { + let script = &txout.script_pubkey; + let instructions = script.instructions_minimal().collect::, _>>().unwrap(); + if let bitcoin::blockdata::script::Instruction::PushBytes(bytes) = &instructions[1] { + data.extend_from_slice(bytes.as_bytes()); + } + if script.is_op_return() { + break; + } + } + data +} + +#[test] +fn test_commitment_outputs() { + let data = b"Hello, this is a test message for commitment outputs. It should be split into multiple outputs as it exceeds opreturn size limit."; + let outputs = generate_data_commitment_outputs(data); + assert!(is_valid_commitment_outputs(&outputs)); + let extracted_data = extract_data_from_commitment_outputs(&outputs); + assert_eq!(extracted_data, data.to_vec()); +} diff --git a/crates/goat/src/transactions/assert.rs b/crates/goat/src/transactions/assert.rs new file mode 100644 index 00000000..cecedcd5 --- /dev/null +++ b/crates/goat/src/transactions/assert.rs @@ -0,0 +1,594 @@ +use std::vec; + +use bitcoin::{ + absolute, consensus, taproot::LeafVersion, Amount, ScriptBuf, TapSighashType, Transaction, + TxIn, TxOut, +}; +use bitvm::{chunk::api::type_conversion_utils::RawWitness, execute_script, treepp::*}; +use musig2::{errors::SigningError, AggNonce, PartialSignature, SecNonce}; +use serde::{Deserialize, Serialize}; + +use crate::{ + assert_scripts::{ + Label, LabelHash, OperatorAssertPublicKey, OperatorAssertSecretKey, + OperatorCommitPubinPublicKey, INPUT_WIRE_NUM, + }, + connectors::{ + assert_connectors::{ProverConnector, VerifierConnector}, + base::TaprootConnector, + connector_c::ConnectorC, + connector_d::{ConnectorD, CONNECTOR_D_PUBIN_DISPROVE_LEAF_INDEX}, + }, + contexts::{base::BaseContext, committee::CommitteeContext}, + error::{Error, TransactionError::InsufficientInputAmount}, + pubin_disprove_scripts::verify_guest_pubin_commitment, + scripts::p2a_output, + transactions::{ + base::*, + pre_signed::PreSignedTransaction, + signing::{ + populate_taproot_input_witness, populate_taproot_txin_witness, + push_taproot_leaf_script_and_control_block_to_witness, + }, + signing_musig2::{ + generate_taproot_aggregated_signature, generate_taproot_partial_signature, + }, + }, +}; + +#[derive(Serialize, Deserialize, Eq, PartialEq, Clone)] +pub struct OperatorAssertTransaction { + #[serde(with = "consensus::serde::With::")] + tx: Transaction, + #[serde(with = "consensus::serde::With::")] + prev_outs: Vec, + prev_scripts: Vec, +} + +impl PreSignedTransaction for OperatorAssertTransaction { + fn tx(&self) -> &Transaction { + &self.tx + } + + fn tx_mut(&mut self) -> &mut Transaction { + &mut self.tx + } + + fn prev_outs(&self) -> &Vec { + &self.prev_outs + } + + fn prev_scripts(&self) -> &Vec { + &self.prev_scripts + } +} + +impl OperatorAssertTransaction { + pub fn new_for_validation( + connector_c: &ConnectorC, + verifier_connectors: &Vec, + connector_d: &ConnectorD, + input_0: Input, + ) -> Result { + let input_0_leaf = 1; + let _input_0 = connector_c.generate_taproot_leaf_tx_in(input_0_leaf, &input_0); + + if input_0.amount + < Amount::from_sat(operator_assert_input_amount(verifier_connectors.len())) + { + return Err(Error::Transaction(InsufficientInputAmount)); + } + + let mut total_output_amount = input_0.amount + - Amount::from_sat(min_relay_fee_operator_assert(verifier_connectors.len())); + let mut txouts = vec![]; + for verifier_connector in verifier_connectors { + let assert_output = TxOut { + value: Amount::from_sat(verifier_assert_input_amount()), + script_pubkey: verifier_connector.generate_taproot_address().script_pubkey(), + }; + total_output_amount -= assert_output.value; + txouts.push(assert_output); + } + let anchor_output = p2a_output(); + total_output_amount -= anchor_output.value; + let connector_d_output = TxOut { + value: total_output_amount, + script_pubkey: connector_d.generate_taproot_address().script_pubkey(), + }; + txouts.push(connector_d_output); + txouts.push(anchor_output); + + Ok(OperatorAssertTransaction { + tx: Transaction { + version: bitcoin::transaction::Version(2), + lock_time: absolute::LockTime::ZERO, + input: vec![_input_0], + output: txouts, + }, + prev_outs: vec![TxOut { + value: input_0.amount, + script_pubkey: connector_c.generate_taproot_address().script_pubkey(), + }], + prev_scripts: vec![connector_c.generate_taproot_leaf_script(input_0_leaf)], + }) + } + + pub fn operator_commit_proof( + &mut self, + wots_sk: &OperatorAssertSecretKey, + connector_c: &ConnectorC, + proof: &[u8; 96], + pi2: &[u8], + pi3: &[u8], + ) -> Result<(), Error> { + let input_index = 0; + let leaf_index = 1; + match connector_c.generate_leaf_1_unlock_data(wots_sk, proof, pi2, pi3) { + Ok(wit) => { + populate_taproot_input_witness( + self.tx_mut(), + input_index, + &connector_c.generate_taproot_spend_info(), + &connector_c.generate_taproot_leaf_script(leaf_index), + wit, + ); + } + Err(e) => return Err(e), + } + Ok(()) + } + + pub fn verifier_connector_input(&self, index: usize) -> Result { + let verifier_num = output_topology::operator_assert::verifier_num(self.tx.output.len()); + if index >= verifier_num { + return Err(Error::Other("verifier connector index out of bounds")); + } + + tx_output_input(&self.tx, output_topology::operator_assert::verifier_connector(index)) + } + + pub fn connector_d_input(&self) -> Result { + let verifier_num = output_topology::operator_assert::verifier_num(self.tx.output.len()); + tx_output_input(&self.tx, output_topology::operator_assert::connector_d(verifier_num)) + } + + pub fn anchor_input(&self) -> Result { + let verifier_num = output_topology::operator_assert::verifier_num(self.tx.output.len()); + tx_output_input(&self.tx, output_topology::operator_assert::anchor(verifier_num)) + } +} + +impl BaseTransaction for OperatorAssertTransaction { + fn finalize(&self) -> Transaction { + self.tx.clone() + } + + fn name(&self) -> &'static str { + "OperatorAssert" + } +} + +#[derive(Serialize, Deserialize, Eq, PartialEq, Clone)] +pub struct VerifierAssertTransaction { + #[serde(with = "consensus::serde::With::")] + tx: Transaction, + #[serde(with = "consensus::serde::With::")] + prev_outs: Vec, + prev_scripts: Vec, +} + +impl PreSignedTransaction for VerifierAssertTransaction { + fn tx(&self) -> &Transaction { + &self.tx + } + + fn tx_mut(&mut self) -> &mut Transaction { + &mut self.tx + } + + fn prev_outs(&self) -> &Vec { + &self.prev_outs + } + + fn prev_scripts(&self) -> &Vec { + &self.prev_scripts + } +} + +impl VerifierAssertTransaction { + pub fn new_for_validation( + verifier_connector: &VerifierConnector, + prover_connector: &ProverConnector, + input_0: Input, + ) -> Result { + let input_0_leaf = 0; + let _input_0 = verifier_connector.generate_taproot_leaf_tx_in(input_0_leaf, &input_0); + + if input_0.amount < Amount::from_sat(verifier_assert_input_amount()) { + return Err(Error::Transaction(InsufficientInputAmount)); + } + + let output_0 = TxOut { + value: Amount::from_sat(verifier_assert_prover_output_amount()), + script_pubkey: prover_connector.generate_taproot_address().script_pubkey(), + }; + + let output_1 = p2a_output(); + + Ok(VerifierAssertTransaction { + tx: Transaction { + version: bitcoin::transaction::Version(2), + lock_time: absolute::LockTime::ZERO, + input: vec![_input_0], + output: vec![output_0, output_1], + }, + prev_outs: vec![TxOut { + value: input_0.amount, + script_pubkey: verifier_connector.generate_taproot_address().script_pubkey(), + }], + prev_scripts: vec![verifier_connector.generate_taproot_leaf_script(input_0_leaf)], + }) + } + + pub fn verifier_publish_labels( + &mut self, + verifier_connector: &VerifierConnector, + labels: [Label; INPUT_WIRE_NUM], + operator_assertion: &RawWitness, + ) -> Result<(), Error> { + let input_index = 0; + let leaf_index = 0; + let wit = verifier_connector.generate_leaf_0_unlock_data(labels, operator_assertion)?; + populate_taproot_input_witness( + self.tx_mut(), + input_index, + &verifier_connector.generate_taproot_spend_info(), + &verifier_connector.generate_taproot_leaf_script(leaf_index), + wit, + ); + Ok(()) + } + + pub fn prover_connector_input(&self) -> Result { + tx_output_input(&self.tx, output_topology::verifier_assert::prover_connector()) + } + + pub fn anchor_input(&self) -> Result { + tx_output_input(&self.tx, output_topology::verifier_assert::anchor()) + } +} + +impl BaseTransaction for VerifierAssertTransaction { + fn finalize(&self) -> Transaction { + self.tx.clone() + } + + fn name(&self) -> &'static str { + "VerifierAssert" + } +} + +#[derive(Serialize, Deserialize, Eq, PartialEq, Clone)] +pub struct DisproveTransaction { + #[serde(with = "consensus::serde::With::")] + tx: Transaction, + #[serde(with = "consensus::serde::With::")] + prev_outs: Vec, + prev_scripts: Vec, +} + +impl PreSignedTransaction for DisproveTransaction { + fn tx(&self) -> &Transaction { + &self.tx + } + + fn tx_mut(&mut self) -> &mut Transaction { + &mut self.tx + } + + fn prev_outs(&self) -> &Vec { + &self.prev_outs + } + + fn prev_scripts(&self) -> &Vec { + &self.prev_scripts + } +} + +impl DisproveTransaction { + pub fn new_for_validation( + prover_connector: &ProverConnector, + connector_d: &ConnectorD, + input_0: Input, + input_1: Input, + _final_msg: Vec, + ) -> Result { + let input_0_leaf = 1; + let _input_0 = prover_connector.generate_taproot_leaf_tx_in(input_0_leaf, &input_0); + + let input_1_leaf = 1; + let _input_1 = connector_d.generate_taproot_leaf_tx_in(input_1_leaf, &input_1); + + if input_0.amount + input_1.amount < Amount::from_sat(disprove_input_amount()) { + return Err(Error::Transaction(InsufficientInputAmount)); + } + + Ok(DisproveTransaction { + tx: Transaction { + version: bitcoin::transaction::Version(2), + lock_time: absolute::LockTime::ZERO, + input: vec![_input_0, _input_1], + output: vec![p2a_output()], + }, + prev_outs: vec![ + TxOut { + value: input_0.amount, + script_pubkey: prover_connector.generate_taproot_address().script_pubkey(), + }, + TxOut { + value: input_1.amount, + script_pubkey: connector_d.generate_taproot_address().script_pubkey(), + }, + ], + prev_scripts: vec![ + prover_connector.generate_taproot_leaf_script(input_0_leaf), + connector_d.generate_taproot_leaf_script(input_1_leaf), + ], + }) + } + + fn sign_input_0_musig2( + &mut self, + context: &CommitteeContext, + sec_nonce: &SecNonce, + agg_nonce: &AggNonce, + ) -> Result { + let input_index = 0; + let sighash_type = TapSighashType::None; + generate_taproot_partial_signature( + context, + self.tx(), + sec_nonce, + agg_nonce, + input_index, + self.prev_outs(), + &self.prev_scripts()[input_index], + sighash_type, + ) + } + + fn push_input_0_signature( + &mut self, + prover_connector: &ProverConnector, + input_0_sig: bitcoin::taproot::Signature, + ) { + let input_index = 0; + let script = self.prev_scripts()[input_index].clone(); + let spend_info = prover_connector.generate_taproot_spend_info(); + let tx_mut = self.tx_mut(); + // Push signature to witness + tx_mut.input[input_index].witness.push(input_0_sig.serialize()); + + // Push script + control block + push_taproot_leaf_script_and_control_block_to_witness( + tx_mut, + input_index, + &spend_info, + &script, + ); + } + + fn sign_input_1_musig2( + &mut self, + context: &CommitteeContext, + sec_nonce: &SecNonce, + agg_nonce: &AggNonce, + ) -> Result { + let input_index = 1; + let sighash_type = TapSighashType::None; + generate_taproot_partial_signature( + context, + self.tx(), + sec_nonce, + agg_nonce, + input_index, + self.prev_outs(), + &self.prev_scripts()[input_index], + sighash_type, + ) + } + + fn push_input_1_signature( + &mut self, + connector_d: &ConnectorD, + input_1_sig: bitcoin::taproot::Signature, + ) { + let input_index = 1; + let script = self.prev_scripts()[input_index].clone(); + let spend_info = connector_d.generate_taproot_spend_info(); + let tx_mut = self.tx_mut(); + // Push signature to witness + tx_mut.input[input_index].witness.push(input_1_sig.serialize()); + + // Push script + control block + push_taproot_leaf_script_and_control_block_to_witness( + tx_mut, + input_index, + &spend_info, + &script, + ); + } + + pub fn pre_sign( + &mut self, + context: &CommitteeContext, + sec_nonces: &[SecNonce; 2], + agg_nonces: &[AggNonce; 2], + ) -> Result<[PartialSignature; 2], SigningError> { + let input_0_sig = self.sign_input_0_musig2(context, &sec_nonces[0], &agg_nonces[0]); + let input_1_sig = self.sign_input_1_musig2(context, &sec_nonces[1], &agg_nonces[1]); + match [input_0_sig, input_1_sig] + .into_iter() + .collect::, SigningError>>() + { + Ok(sigs) => Ok(sigs.try_into().unwrap()), + Err(e) => Err(e), + } + } + + pub fn aggregate_pre_sigs( + &self, + context: &dyn BaseContext, + partial_signatures: &[Vec; 2], + agg_nonces: &[AggNonce; 2], + ) -> Result<[bitcoin::taproot::Signature; 2], Error> { + let (input_0_sig, input_1_sig); + { + let input_index = 0; + let sig_index = 0; + let sighash_type = TapSighashType::None; + input_0_sig = match generate_taproot_aggregated_signature( + context, + self.tx(), + &agg_nonces[sig_index], + input_index, + self.prev_outs(), + &self.prev_scripts()[input_index], + sighash_type, + partial_signatures[sig_index].clone(), + ) { + Ok(sig) => bitcoin::taproot::Signature { signature: sig.into(), sighash_type }, + Err(_) => return Err(Error::Other("Failed to aggregate signatures")), + }; + } + { + let input_index = 1; + let sig_index = 1; + let sighash_type = TapSighashType::None; + input_1_sig = match generate_taproot_aggregated_signature( + context, + self.tx(), + &agg_nonces[sig_index], + input_index, + self.prev_outs(), + &self.prev_scripts()[input_index], + sighash_type, + partial_signatures[sig_index].clone(), + ) { + Ok(sig) => bitcoin::taproot::Signature { signature: sig.into(), sighash_type }, + Err(_) => return Err(Error::Other("Failed to aggregate signatures")), + }; + } + Ok([input_0_sig, input_1_sig]) + } + + pub fn push_pre_sigs( + &mut self, + prover_connector: &ProverConnector, + connector_d: &ConnectorD, + pre_sigs: [bitcoin::taproot::Signature; 2], + ) { + self.push_input_0_signature(prover_connector, pre_sigs[0]); + self.push_input_1_signature(connector_d, pre_sigs[1]); + } +} + +impl BaseTransaction for DisproveTransaction { + fn finalize(&self) -> Transaction { + self.tx.clone() + } + + fn name(&self) -> &'static str { + "Disprove" + } +} + +pub fn pubin_disprove_script( + guest_pubin_wots_pubkey: &OperatorCommitPubinPublicKey, + operator_assert_wots_pubkey: &OperatorAssertPublicKey, + constant_value: &[u8; 32], + watchtower_hashelocks: &[LabelHash], +) -> Script { + verify_guest_pubin_commitment( + guest_pubin_wots_pubkey, + operator_assert_wots_pubkey, + constant_value, + watchtower_hashelocks, + ) +} + +pub fn validate_pubin( + operator_commit_pubin_witness: RawWitness, + operator_assert_witness: RawWitness, + ack_preimages: Vec>, + input_lock_script: ScriptBuf, +) -> Option<(RawWitness, ScriptBuf)> { + let mut unlock_data = operator_assert_witness; + unlock_data.extend(ack_preimages); + unlock_data.extend(operator_commit_pubin_witness); + + let witness_script = script! { + { unlock_data.clone() } + }; + let verification_script = witness_script.push_script(input_lock_script.clone()); + let exec_result = execute_script(verification_script); + if exec_result.success { + Some((unlock_data, input_lock_script)) + } else { + None + } +} + +pub fn pubin_disprove( + connector_d: &ConnectorD, + connector_d_input: &Input, + input_script_witness: RawWitness, +) -> Result { + let input_lock_script = + connector_d.generate_taproot_leaf_script(CONNECTOR_D_PUBIN_DISPROVE_LEAF_INDEX); + let mut txin = connector_d + .generate_taproot_leaf_tx_in(CONNECTOR_D_PUBIN_DISPROVE_LEAF_INDEX, connector_d_input); + input_script_witness.into_iter().for_each(|witness_item| txin.witness.push(witness_item)); + + let prevout_leaf = (input_lock_script, LeafVersion::TapScript); + let control_block = match connector_d.generate_taproot_spend_info().control_block(&prevout_leaf) + { + Some(control_block) => control_block, + None => { + return Err(Error::Other( + "Unable to generate Connector-D control block for pubin-disprove txin.", + )) + } + }; + txin.witness.push(prevout_leaf.0.to_bytes()); + txin.witness.push(control_block.serialize()); + Ok(txin) +} + +pub fn wrongly_challenged( + prover_connector: &ProverConnector, + input_0: &Input, + final_msg: &Label, +) -> Result { + let leaf_index = 0; + let unlock_data = vec![final_msg.clone()]; + let witness_script = script! { + { unlock_data.clone() } + }; + let script = prover_connector.generate_taproot_leaf_script(leaf_index); + let verification_script = witness_script.push_script(script.clone()); + let exec_result = execute_script(verification_script); + if !exec_result.success { + return Err(Error::Other("Invalid hashlock preimage for ProverConnector.")); + } + + let mut txin = prover_connector.generate_taproot_leaf_tx_in(leaf_index, input_0); + populate_taproot_txin_witness( + &mut txin, + &prover_connector.generate_taproot_spend_info(), + &script, + unlock_data, + ); + Ok(txin) +} diff --git a/crates/goat/src/transactions/base.rs b/crates/goat/src/transactions/base.rs new file mode 100644 index 00000000..06438457 --- /dev/null +++ b/crates/goat/src/transactions/base.rs @@ -0,0 +1,538 @@ +use super::pre_signed_musig2::{verify_public_nonce, PreSignedMusig2Transaction}; +use crate::error::Error; +use bitcoin::policy::{DEFAULT_MIN_RELAY_TX_FEE, DUST_RELAY_TX_FEE}; +use bitcoin::{consensus, Amount, OutPoint, PublicKey, Script, Transaction, Txid, XOnlyPublicKey}; +use core::cmp; +use itertools::Itertools; +use musig2::{secp256k1::schnorr::Signature, PubNonce}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +pub const DUST_AMOUNT: u64 = (43 + 67) * DUST_RELAY_FEE_RATE; +pub const MIN_RELAY_FEE_RATE: u64 = (DEFAULT_MIN_RELAY_TX_FEE / 1000) as u64; +pub const DUST_RELAY_FEE_RATE: u64 = (DUST_RELAY_TX_FEE / 1000) as u64; + +pub const fn max(a: u64, b: u64) -> u64 { + [a, b][(a < b) as usize] +} + +// TBD: accurately calculate the relay fee +pub const RELAY_FEE_BUFFER_MULTIPLIER: f32 = 1.2; +pub const ACCELERATE_FEE_MULTIPLIER: u64 = 2; +pub const MIN_RELAY_FEE_KICKOFF: u64 = relay_fee(500); +pub const MIN_RELAY_FEE_TAKE_1: u64 = relay_fee(500); +pub const MIN_RELAY_FEE_TAKE_2: u64 = relay_fee(500); +pub const MIN_RELAY_FEE_VERIFIER_ASSERT: u64 = relay_fee(60000); +pub const MIN_RELAY_FEE_WRONGLY_CHALLENGED: u64 = relay_fee(1500); +pub const MIN_RELAY_FEE_DISPROVE: u64 = relay_fee(800); +pub const MIN_RELAY_FEE_PUBIN_DISPROVE: u64 = relay_fee(800); +pub const MIN_RELAY_FEE_WATCHTOWER_CHALLENGE_TIMEOUT: u64 = relay_fee(500); +pub const MIN_RELAY_FEE_OPERATOR_CHALLENGE_ACK: u64 = relay_fee(500); +pub const MIN_RELAY_FEE_OPERATOR_CHALLENGE_NACK: u64 = relay_fee(800); +pub const MIN_RELAY_FEE_OPERATOR_COMMIT_PUBIN: u64 = relay_fee(500); +pub const MIN_RELAY_FEE_OPERATOR_COMMIT_TIMEOUT: u64 = relay_fee(800); +pub const P2A_AMOUNT: u64 = 240; +pub const fn min_relay_fee_watchtower_challenge_init(watchtower_num: usize) -> u64 { + relay_fee(watchtower_num * 200 + 400) +} +pub const fn min_relay_fee_operator_assert(num_verifier: usize) -> u64 { + relay_fee(num_verifier * 100 + 15000) +} +pub const fn wrongly_challenged_input_amount() -> u64 { + MIN_RELAY_FEE_WRONGLY_CHALLENGED + P2A_AMOUNT +} +pub const fn verifier_assert_prover_output_amount() -> u64 { + max(DUST_AMOUNT, wrongly_challenged_input_amount()) +} +pub const fn verifier_assert_input_amount() -> u64 { + MIN_RELAY_FEE_VERIFIER_ASSERT + verifier_assert_prover_output_amount() + P2A_AMOUNT +} +pub const fn disprove_input_amount() -> u64 { + MIN_RELAY_FEE_DISPROVE + P2A_AMOUNT +} +pub const fn pubin_disprove_input_amount() -> u64 { + MIN_RELAY_FEE_PUBIN_DISPROVE + P2A_AMOUNT +} +pub const fn watchtower_challenge_timeout_input_amount() -> u64 { + MIN_RELAY_FEE_WATCHTOWER_CHALLENGE_TIMEOUT + P2A_AMOUNT +} +pub const fn operator_challenge_nack_input_amount() -> u64 { + MIN_RELAY_FEE_OPERATOR_CHALLENGE_NACK + P2A_AMOUNT +} +pub const fn operator_commit_timeout_input_amount() -> u64 { + MIN_RELAY_FEE_OPERATOR_COMMIT_TIMEOUT + P2A_AMOUNT +} +pub const fn watchtower_challenge_connector_output_amount() -> u64 { + max( + DUST_AMOUNT, + watchtower_challenge_timeout_input_amount().saturating_sub(ack_connector_output_amount()), + ) +} +pub const fn ack_connector_output_amount() -> u64 { + DUST_AMOUNT +} +pub const fn connector_e_output_amount() -> u64 { + DUST_AMOUNT +} +pub const fn connector_f_output_amount() -> u64 { + max( + DUST_AMOUNT, + max( + operator_challenge_nack_input_amount().saturating_sub(ack_connector_output_amount()), + operator_commit_timeout_input_amount().saturating_sub(connector_e_output_amount()), + ), + ) +} +pub const fn connector_d_assert_output_amount() -> u64 { + let disprove_connector_d_amount = + if disprove_input_amount() > verifier_assert_prover_output_amount() { + disprove_input_amount() - verifier_assert_prover_output_amount() + } else { + 0 + }; + + max(max(DUST_AMOUNT, disprove_connector_d_amount), pubin_disprove_input_amount()) +} +pub const fn operator_assert_input_amount(num_verifier: usize) -> u64 { + min_relay_fee_operator_assert(num_verifier) + + num_verifier as u64 * verifier_assert_input_amount() + + connector_d_assert_output_amount() + + P2A_AMOUNT +} +pub const fn max_assert_cost(num_verifier: usize) -> u64 { + operator_assert_input_amount(num_verifier) +} +pub const fn max_watchtower_challenge_cost(num_watchtowers: usize) -> u64 { + min_relay_fee_watchtower_challenge_init(num_watchtowers) + + num_watchtowers as u64 + * (watchtower_challenge_connector_output_amount() + ack_connector_output_amount()) + + connector_e_output_amount() + + connector_f_output_amount() + + P2A_AMOUNT +} +pub const fn max_pegout_cost(num_watchtowers: usize, num_verifier: usize) -> u64 { + max_assert_cost(num_verifier) + + max_watchtower_challenge_cost(num_watchtowers) + + MIN_RELAY_FEE_KICKOFF + + DUST_AMOUNT * 2 + + P2A_AMOUNT +} + +#[derive(Serialize, Deserialize, Eq, PartialEq, Clone)] +pub struct Input { + #[serde(with = "consensus::serde::With::")] + pub outpoint: OutPoint, + #[serde(with = "consensus::serde::With::")] + pub amount: Amount, +} + +pub struct InputWithScript<'a> { + pub outpoint: OutPoint, + pub amount: Amount, + pub script: &'a Script, +} + +pub fn tx_output_input(transaction: &Transaction, vout: usize) -> Result { + let output = transaction + .output + .get(vout) + .ok_or(Error::Other("transaction output index out of bounds"))?; + let vout = vout.try_into().map_err(|_| Error::Other("transaction output index exceeds u32"))?; + + Ok(Input { + outpoint: OutPoint { txid: transaction.compute_txid(), vout }, + amount: output.value, + }) +} + +pub mod output_topology { + pub mod kickoff { + pub const CONNECTOR_A: usize = 0; + pub const CONNECTOR_B: usize = 1; + pub const CONNECTOR_C: usize = 2; + pub const GUARDIAN_CONNECTOR: usize = 3; + pub const ANCHOR: usize = 4; + pub const OUTPUT_NUM: usize = ANCHOR + 1; + + pub const fn connector_a() -> usize { + CONNECTOR_A + } + + pub const fn connector_b() -> usize { + CONNECTOR_B + } + + pub const fn connector_c() -> usize { + CONNECTOR_C + } + + pub const fn guardian_connector() -> usize { + GUARDIAN_CONNECTOR + } + + pub const fn anchor() -> usize { + ANCHOR + } + } + + pub mod prekickoff { + pub const FORCE_SKIP_CONNECTOR: usize = 0; + pub const KICKOFF_CONNECTOR: usize = 1; + pub const PREKICKOFF_CONNECTOR: usize = 2; + pub const ANCHOR: usize = 3; + pub const OUTPUT_NUM: usize = ANCHOR + 1; + + pub const fn force_skip_connector() -> usize { + FORCE_SKIP_CONNECTOR + } + + pub const fn kickoff_connector() -> usize { + KICKOFF_CONNECTOR + } + + pub const fn prekickoff_connector() -> usize { + PREKICKOFF_CONNECTOR + } + + pub const fn anchor() -> usize { + ANCHOR + } + } + + pub mod pegin_deposit { + pub const CONNECTOR_Z: usize = 0; + pub const CHANGE: usize = 1; + + pub const fn connector_z() -> usize { + CONNECTOR_Z + } + + pub const fn change() -> usize { + CHANGE + } + + pub const fn output_num(has_change: bool) -> usize { + if has_change { + CHANGE + 1 + } else { + CONNECTOR_Z + 1 + } + } + } + + pub mod pegin_refund { + pub const REFUND: usize = 0; + pub const OUTPUT_NUM: usize = REFUND + 1; + + pub const fn refund() -> usize { + REFUND + } + } + + pub mod pegin_confirm { + pub const CONNECTOR_0: usize = 0; + pub const OP_RETURN: usize = 1; + pub const OUTPUT_NUM: usize = OP_RETURN + 1; + + pub const fn connector_0() -> usize { + CONNECTOR_0 + } + + pub const fn op_return() -> usize { + OP_RETURN + } + } + + pub mod watchtower_challenge_init { + pub const WATCHTOWER_CONNECTOR_PAIR_START: usize = 0; + + pub const fn watchtower_connector(index: usize) -> usize { + WATCHTOWER_CONNECTOR_PAIR_START + index * 2 + } + + pub const fn ack_connector(index: usize) -> usize { + WATCHTOWER_CONNECTOR_PAIR_START + index * 2 + 1 + } + + pub const fn connector_e(watchtower_num: usize) -> usize { + WATCHTOWER_CONNECTOR_PAIR_START + watchtower_num * 2 + } + + pub const fn connector_f(watchtower_num: usize) -> usize { + connector_e(watchtower_num) + 1 + } + + pub const fn anchor(watchtower_num: usize) -> usize { + connector_f(watchtower_num) + 1 + } + + pub const fn output_num(watchtower_num: usize) -> usize { + anchor(watchtower_num) + 1 + } + + pub const fn watchtower_num(output_num: usize) -> usize { + if output_num < 3 { + 0 + } else { + (output_num - 3) / 2 + } + } + } + + pub mod operator_assert { + pub const VERIFIER_CONNECTOR_START: usize = 0; + + pub const fn verifier_connector(index: usize) -> usize { + VERIFIER_CONNECTOR_START + index + } + + pub const fn connector_d(verifier_num: usize) -> usize { + VERIFIER_CONNECTOR_START + verifier_num + } + + pub const fn anchor(verifier_num: usize) -> usize { + connector_d(verifier_num) + 1 + } + + pub const fn output_num(verifier_num: usize) -> usize { + anchor(verifier_num) + 1 + } + + pub const fn verifier_num(output_num: usize) -> usize { + output_num.saturating_sub(2) + } + } + + pub mod verifier_assert { + pub const PROVER_CONNECTOR: usize = 0; + pub const ANCHOR: usize = 1; + pub const OUTPUT_NUM: usize = ANCHOR + 1; + + pub const fn prover_connector() -> usize { + PROVER_CONNECTOR + } + + pub const fn anchor() -> usize { + ANCHOR + } + } +} + +pub trait BaseTransaction { + fn name(&self) -> &'static str; + // fn initialize(&mut self, context: &dyn BaseContext); + + // TODO: Use musig2 to aggregate signatures + // fn pre_sign(&mut self, context: &dyn BaseContext); + + // TODO: Implement default that goes through all leaves and checks if one of them is executable + // TODO: Return a Result with an Error in case the witness can't be created + fn finalize(&self) -> Transaction; +} + +pub const fn relay_fee(vsize: usize) -> u64 { + (vsize as f32 * RELAY_FEE_BUFFER_MULTIPLIER) as u64 * MIN_RELAY_FEE_RATE +} + +pub fn merge_transactions( + destination_transaction: &mut Transaction, + source_transaction: &Transaction, +) { + for i in destination_transaction.input.len()..source_transaction.input.len() { + destination_transaction.input.push(source_transaction.input[i].clone()); + } + + for i in 0..cmp::min(destination_transaction.input.len(), source_transaction.input.len()) { + // TODO: takes longer witness data but should combine both + // TODO: merge signatures after Musig2 feature is ready + if destination_transaction.input[i].witness.len() + < source_transaction.input[i].witness.len() + { + destination_transaction.input[i].witness = source_transaction.input[i].witness.clone(); + } + } + + for i in destination_transaction.output.len()..source_transaction.output.len() { + destination_transaction.output.push(source_transaction.output[i].clone()); + } +} + +// assumes source_transaction is the latest +pub fn merge_musig2_nonces_and_signatures( + destination_transaction: &mut dyn PreSignedMusig2Transaction, + source_transaction: &dyn PreSignedMusig2Transaction, +) { + let nonces = destination_transaction.musig2_nonces_mut(); + merge_hash_maps(nonces, source_transaction.musig2_nonces().clone()); + + let nonce_signatures = destination_transaction.musig2_nonce_signatures_mut(); + merge_hash_maps(nonce_signatures, source_transaction.musig2_nonce_signatures().clone()); + + let signatures = destination_transaction.musig2_signatures_mut(); + merge_hash_maps(signatures, source_transaction.musig2_signatures().clone()); +} + +// merge the nonce/signature hashmaps. We can't just do a.extend(b) since that would just overwrite the inner +// hashmap rather than merging it +fn merge_hash_maps( + a: &mut HashMap>, + b: HashMap>, +) { + let all_keys = a.keys().chain(b.keys()).unique().cloned().collect::>(); + for key in all_keys { + let q = a.entry(key).or_default(); + let w = b.get(&key).cloned().unwrap_or(HashMap::new()); + q.extend(w.clone()); + } +} + +pub fn validate_transaction( + transaction: &Transaction, + comparison_transaction: &Transaction, +) -> bool { + for i in 0..comparison_transaction.input.len() { + if transaction.input[i].previous_output != comparison_transaction.input[i].previous_output + || transaction.input[i].script_sig != comparison_transaction.input[i].script_sig + || transaction.input[i].sequence != comparison_transaction.input[i].sequence + { + println!( + "Input mismatch on transaction: {} input index: {}", + transaction.compute_txid(), + i + ); + return false; + } + } + + for i in 0..comparison_transaction.output.len() { + if transaction.output[i].value != comparison_transaction.output[i].value + || transaction.output[i].script_pubkey != comparison_transaction.output[i].script_pubkey + { + println!( + "Output mismatch on transaction: {} output index: {}", + transaction.compute_txid(), + i + ); + return false; + } + } + + true +} + +fn verify_public_nonces( + all_nonces: &HashMap>, + all_sigs: &HashMap>, + txid: Txid, +) -> bool { + let mut ret_val = true; + + for (i, nonces) in all_nonces { + for (pubkey, nonce) in nonces { + if !verify_public_nonce(&all_sigs[i][pubkey], nonce, &XOnlyPublicKey::from(*pubkey)) { + eprintln!( + "Failed to verify public nonce for pubkey {pubkey} on tx:input {txid}:{i}." + ); + ret_val = false; + } + } + } + + ret_val +} + +pub fn verify_public_nonces_for_tx(tx: &impl PreSignedMusig2Transaction) -> bool { + verify_public_nonces(tx.musig2_nonces(), tx.musig2_nonce_signatures(), tx.tx().compute_txid()) +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use bitcoin::{ + key::{ + constants::{SCHNORR_SIGNATURE_SIZE, SECRET_KEY_SIZE}, + Keypair, + }, + PublicKey, Txid, + }; + use musig2::{secp256k1::schnorr::Signature, PubNonce}; + + use crate::{ + contexts::base::generate_keys_from_secret, + transactions::{pre_signed_musig2::get_nonce_message, signing_musig2::generate_nonce}, + }; + + use super::verify_public_nonces; + + const DUMMY_TXID: &str = "5df6e0e2761359d30a8275058e299fcc0381534545f55cf43e41983f5d4c9456"; + + type TestNonces = HashMap>; + type TestNonceSignatures = HashMap>; + + fn get_test_nonces() -> (TestNonces, TestNonceSignatures) { + const SIGNERS: usize = 3; + const INPUTS: usize = 4; + + // Generate keys + let mut keypairs: Vec = Vec::new(); + let mut pubkeys: Vec = Vec::new(); + for signer in 0..SIGNERS { + let (keypair, pubkey) = generate_keys_from_secret( + bitcoin::Network::Bitcoin, + &hex::encode([(signer + 1) as u8; SECRET_KEY_SIZE]), + ); + keypairs.push(keypair); + pubkeys.push(pubkey); + } + + // Generate and sign nonces + let mut all_nonces: HashMap> = HashMap::new(); + let mut all_sigs: HashMap> = HashMap::new(); + for input in 0..INPUTS { + let mut nonces: HashMap = HashMap::new(); + let mut sigs: HashMap = HashMap::new(); + for signer in 0..SIGNERS { + let secret_nonce = generate_nonce(); + + nonces.insert(pubkeys[signer], secret_nonce.public_nonce()); + + let nonce_signature = + keypairs[signer].sign_schnorr(get_nonce_message(&secret_nonce.public_nonce())); + sigs.insert(pubkeys[signer], nonce_signature); + } + all_nonces.insert(input, nonces); + all_sigs.insert(input, sigs); + } + (all_nonces, all_sigs) + } + + #[test] + fn test_verify_public_nonces_all_valid_signatures() { + let (all_nonces, all_sigs) = get_test_nonces(); + + assert!( + verify_public_nonces(&all_nonces, &all_sigs, DUMMY_TXID.parse::().unwrap()), + "verify_public_nonces() did not return true on success" + ); + } + + #[test] + fn test_verify_public_nonces_invalid_signature() { + let (all_nonces, mut all_sigs) = get_test_nonces(); + + let input_index = all_sigs.len() / 2; + let pubkey = *all_sigs[&input_index].keys().next().unwrap(); + let mut bad_sig = all_sigs[&input_index][&pubkey].serialize(); + bad_sig[SCHNORR_SIGNATURE_SIZE - 1] += 1; + all_sigs + .get_mut(&input_index) + .unwrap() + .insert(pubkey, Signature::from_slice(&bad_sig).unwrap()); + + assert!( + !verify_public_nonces(&all_nonces, &all_sigs, DUMMY_TXID.parse::().unwrap()), + "verify_public_nonces() did not return false on invalid signature" + ); + } +} diff --git a/crates/goat/src/transactions/challenge.rs b/crates/goat/src/transactions/challenge.rs new file mode 100644 index 00000000..1e7b6ba9 --- /dev/null +++ b/crates/goat/src/transactions/challenge.rs @@ -0,0 +1,183 @@ +use bitcoin::{ + absolute, consensus, Address, Amount, ScriptBuf, TapSighashType, Transaction, TxOut, +}; +use musig2::{errors::SigningError, AggNonce, PartialSignature, SecNonce}; +use serde::{Deserialize, Serialize}; + +use crate::{ + connectors::base::TaprootConnector, + contexts::{base::BaseContext, committee::CommitteeContext}, + error::Error, + transactions::{ + signing::push_taproot_leaf_script_and_control_block_to_witness, + signing_musig2::{ + generate_taproot_aggregated_signature, generate_taproot_partial_signature, + }, + }, +}; + +use super::{super::connectors::connector_a::ConnectorA, base::*, pre_signed::*}; + +#[derive(Serialize, Deserialize, Eq, PartialEq, Clone)] +pub struct ChallengeTransaction { + #[serde(with = "consensus::serde::With::")] + tx: Transaction, + #[serde(with = "consensus::serde::With::")] + prev_outs: Vec, + prev_scripts: Vec, + pub challenge_amount: Amount, +} + +impl PreSignedTransaction for ChallengeTransaction { + fn tx(&self) -> &Transaction { + &self.tx + } + + fn tx_mut(&mut self) -> &mut Transaction { + &mut self.tx + } + + fn prev_outs(&self) -> &Vec { + &self.prev_outs + } + + fn prev_scripts(&self) -> &Vec { + &self.prev_scripts + } +} + +impl ChallengeTransaction { + pub fn new_for_validation( + connector_a: &ConnectorA, + input_0: Input, + challenge_amount: Amount, + operator_address: &Address, + ) -> Self { + let input_0_leaf = 1; + let _input_0 = connector_a.generate_taproot_leaf_tx_in(input_0_leaf, &input_0); + + let _output_0 = + TxOut { value: challenge_amount, script_pubkey: operator_address.script_pubkey() }; + + ChallengeTransaction { + tx: Transaction { + version: bitcoin::transaction::Version(2), + lock_time: absolute::LockTime::ZERO, + input: vec![_input_0], + output: vec![_output_0], + }, + prev_outs: vec![ + TxOut { + value: input_0.amount, + script_pubkey: connector_a.generate_taproot_address().script_pubkey(), + }, + // input 1 will be added later + ], + prev_scripts: vec![ + connector_a.generate_taproot_leaf_script(input_0_leaf), + // input 1's script will be added later + ], + challenge_amount, + } + } + + fn sign_input_0_musig2( + &mut self, + context: &CommitteeContext, + sec_nonce: &SecNonce, + agg_nonce: &AggNonce, + ) -> Result { + let input_index = 0; + let sighash_type = TapSighashType::SinglePlusAnyoneCanPay; + generate_taproot_partial_signature( + context, + self.tx(), + sec_nonce, + agg_nonce, + input_index, + self.prev_outs(), + &self.prev_scripts()[input_index], + sighash_type, + ) + } + + fn push_input_0_signature( + &mut self, + connector_a: &ConnectorA, + input_0_sig: bitcoin::taproot::Signature, + ) { + let input_index = 0; + let script = self.prev_scripts()[input_index].clone(); + let spend_info = connector_a.generate_taproot_spend_info(); + let tx_mut = self.tx_mut(); + // Push signature to witness + tx_mut.input[input_index].witness.push(input_0_sig.serialize()); + + // Push script + control block + push_taproot_leaf_script_and_control_block_to_witness( + tx_mut, + input_index, + &spend_info, + &script, + ); + } + + pub fn pre_sign( + &mut self, + context: &CommitteeContext, + sec_nonces: &[SecNonce; 1], + agg_nonces: &[AggNonce; 1], + ) -> Result<[PartialSignature; 1], SigningError> { + let input_0_sig = self.sign_input_0_musig2(context, &sec_nonces[0], &agg_nonces[0]); + match [input_0_sig].into_iter().collect::, SigningError>>() { + Ok(sigs) => Ok(sigs.try_into().unwrap()), + Err(e) => Err(e), + } + } + + pub fn aggregate_pre_sigs( + &self, + context: &dyn BaseContext, + partial_signatures: &[Vec; 1], + agg_nonces: &[AggNonce; 1], + ) -> Result<[bitcoin::taproot::Signature; 1], Error> { + let input_index = 0; + let sig_index = 0; + let sighash_type = TapSighashType::SinglePlusAnyoneCanPay; + let input_0_sig = match generate_taproot_aggregated_signature( + context, + self.tx(), + &agg_nonces[sig_index], + input_index, + self.prev_outs(), + &self.prev_scripts()[input_index], + sighash_type, + partial_signatures[sig_index].clone(), + ) { + Ok(sig) => bitcoin::taproot::Signature { signature: sig.into(), sighash_type }, + Err(_) => return Err(Error::Other("Failed to aggregate signatures")), + }; + Ok([input_0_sig]) + } + + pub fn push_pre_sigs( + &mut self, + connector_a: &ConnectorA, + pre_sigs: [bitcoin::taproot::Signature; 1], + ) { + self.push_input_0_signature(connector_a, pre_sigs[0]); + } +} + +impl BaseTransaction for ChallengeTransaction { + fn finalize(&self) -> Transaction { + if self.tx.input.len() < 2 { + panic!("Missing input. Call add_inputs_and_output before finalizing"); + } + + self.tx.clone() + } + fn name(&self) -> &'static str { + "Challenge" + } +} diff --git a/crates/goat/src/transactions/kickoff.rs b/crates/goat/src/transactions/kickoff.rs new file mode 100644 index 00000000..95ed161a --- /dev/null +++ b/crates/goat/src/transactions/kickoff.rs @@ -0,0 +1,147 @@ +use bitcoin::{absolute, consensus, Amount, ScriptBuf, TapSighashType, Transaction, TxOut}; +use serde::{Deserialize, Serialize}; + +use crate::{ + connectors::{ + base::*, + connector_a::ConnectorA, + connector_b::ConnectorB, + connector_c::ConnectorC, + kickoff_connectors::{GuardianConnector, KickoffConnector}, + }, + contexts::operator::OperatorContext, + error::{Error, TransactionError::InsufficientInputAmount}, + scripts::p2a_output, + transactions::{ + base::{ + max_pegout_cost, max_watchtower_challenge_cost, output_topology, tx_output_input, + BaseTransaction, Input, DUST_AMOUNT, MIN_RELAY_FEE_KICKOFF, + }, + pre_signed::{pre_sign_taproot_input_default, PreSignedTransaction}, + }, +}; + +#[derive(Serialize, Deserialize, Eq, PartialEq, Clone)] +pub struct KickoffTransaction { + #[serde(with = "consensus::serde::With::")] + tx: Transaction, + #[serde(with = "consensus::serde::With::")] + prev_outs: Vec, + prev_scripts: Vec, +} +impl PreSignedTransaction for KickoffTransaction { + fn tx(&self) -> &Transaction { + &self.tx + } + + fn tx_mut(&mut self) -> &mut Transaction { + &mut self.tx + } + + fn prev_outs(&self) -> &Vec { + &self.prev_outs + } + + fn prev_scripts(&self) -> &Vec { + &self.prev_scripts + } +} +impl KickoffTransaction { + #[allow(clippy::too_many_arguments)] + pub fn new_for_validation( + kickoff_connector: &KickoffConnector, + connector_a: &ConnectorA, + connector_b: &ConnectorB, + connector_c: &ConnectorC, + guardian_connector: &GuardianConnector, + input_0: &Input, + watchtower_num: usize, + verifier_num: usize, + ) -> Result { + let input_0_leaf = 0; + let _input_0 = kickoff_connector.generate_taproot_leaf_tx_in(input_0_leaf, input_0); + + if input_0.amount < Amount::from_sat(max_pegout_cost(watchtower_num, verifier_num)) { + return Err(Error::Transaction(InsufficientInputAmount)); + } + let total_output_amount = input_0.amount - Amount::from_sat(MIN_RELAY_FEE_KICKOFF); + let output_0 = TxOut { + value: Amount::from_sat(DUST_AMOUNT), + script_pubkey: connector_a.generate_taproot_address().script_pubkey(), + }; + let output_1 = TxOut { + value: Amount::from_sat(max_watchtower_challenge_cost(watchtower_num)), + script_pubkey: connector_b.generate_taproot_address().script_pubkey(), + }; + let output_3 = TxOut { + value: Amount::from_sat(DUST_AMOUNT), + script_pubkey: guardian_connector.generate_taproot_address().script_pubkey(), + }; + let anchor_output = p2a_output(); + let output_2 = TxOut { + value: total_output_amount + - output_0.value + - output_1.value + - output_3.value + - anchor_output.value, + script_pubkey: connector_c.generate_taproot_address().script_pubkey(), + }; + + Ok(KickoffTransaction { + tx: Transaction { + version: bitcoin::transaction::Version(2), + lock_time: absolute::LockTime::ZERO, + input: vec![_input_0], + output: vec![output_0, output_1, output_2, output_3, anchor_output], + }, + prev_outs: vec![TxOut { + value: input_0.amount, + script_pubkey: kickoff_connector.generate_taproot_address().script_pubkey(), + }], + prev_scripts: vec![kickoff_connector.generate_taproot_leaf_script(input_0_leaf)], + }) + } + + pub fn sign_input_0( + &mut self, + context: &OperatorContext, + kickoff_connector: &KickoffConnector, + ) { + let input_index = 0; + pre_sign_taproot_input_default( + self, + input_index, + TapSighashType::All, + kickoff_connector.generate_taproot_spend_info(), + &vec![&context.operator_keypair], + ); + } + + pub fn connector_a_input(&self) -> Result { + tx_output_input(&self.tx, output_topology::kickoff::connector_a()) + } + + pub fn connector_b_input(&self) -> Result { + tx_output_input(&self.tx, output_topology::kickoff::connector_b()) + } + + pub fn connector_c_input(&self) -> Result { + tx_output_input(&self.tx, output_topology::kickoff::connector_c()) + } + + pub fn guardian_connector_input(&self) -> Result { + tx_output_input(&self.tx, output_topology::kickoff::guardian_connector()) + } + + pub fn anchor_input(&self) -> Result { + tx_output_input(&self.tx, output_topology::kickoff::anchor()) + } +} +impl BaseTransaction for KickoffTransaction { + fn finalize(&self) -> Transaction { + self.tx.clone() + } + fn name(&self) -> &'static str { + "Kickoff" + } +} diff --git a/crates/goat/src/transactions/mod.rs b/crates/goat/src/transactions/mod.rs new file mode 100644 index 00000000..1b0e0408 --- /dev/null +++ b/crates/goat/src/transactions/mod.rs @@ -0,0 +1,13 @@ +pub mod assert; +pub mod base; +pub mod challenge; +pub mod kickoff; +pub mod pegin; +pub mod pre_signed; +pub mod pre_signed_musig2; +pub mod prekickoff; +pub mod signing; +pub mod signing_musig2; +pub mod take1; +pub mod take2; +pub mod watchtower_challenge; diff --git a/crates/goat/src/transactions/pegin.rs b/crates/goat/src/transactions/pegin.rs new file mode 100644 index 00000000..3765c92a --- /dev/null +++ b/crates/goat/src/transactions/pegin.rs @@ -0,0 +1,358 @@ +use bitcoin::{ + absolute, + consensus::{self}, + Address, Amount, ScriptBuf, TapSighashType, Transaction, TxIn, TxOut, +}; +use musig2::{errors::SigningError, AggNonce, PartialSignature, SecNonce}; +use serde::{Deserialize, Serialize}; + +use crate::{ + connectors::connector_0::Connector0, + contexts::{base::BaseContext, committee::CommitteeContext}, + error::{Error, TransactionError::InsufficientInputAmount}, + scripts::generate_opreturn_script, + transactions::{ + signing::populate_taproot_input_witness_with_signature, + signing_musig2::{ + generate_taproot_aggregated_signature, generate_taproot_partial_signature, + }, + }, +}; + +use super::{ + super::connectors::{base::*, connector_z::ConnectorZ}, + base::*, + pre_signed::*, + signing::*, +}; + +#[derive(Serialize, Deserialize, Eq, PartialEq, Clone)] +pub struct PegInDepositTransaction { + #[serde(with = "consensus::serde::With::")] + tx: Transaction, + #[serde(with = "consensus::serde::With::")] + fee_amount: Amount, + pub input_amounts: Vec, +} +impl PegInDepositTransaction { + pub fn new_unsigned( + connector_z: &ConnectorZ, + inputs: Vec, + deposit_amount: Amount, + fee_amount: Amount, + change_address: Address, + ) -> Result { + let mut total_input_amount = Amount::ZERO; + let input_amounts: Vec = inputs.iter().map(|input| input.amount).collect(); + let txins: Vec = inputs + .iter() + .map(|input| { + total_input_amount += input.amount; + generate_default_tx_in(input) + }) + .collect(); + if total_input_amount < deposit_amount + fee_amount { + return Err(Error::Transaction(InsufficientInputAmount)); + } + + let change_amount = total_input_amount - deposit_amount - fee_amount; + let mut txouts = vec![]; + let output_0 = TxOut { + value: deposit_amount, + script_pubkey: connector_z.generate_taproot_address().script_pubkey(), + }; + txouts.push(output_0); + let mut fee_amount = fee_amount; + if change_amount > Amount::from_sat(DUST_AMOUNT) { + let output_1 = + TxOut { value: change_amount, script_pubkey: change_address.script_pubkey() }; + txouts.push(output_1); + } else { + fee_amount += change_amount; + } + + Ok(PegInDepositTransaction { + tx: Transaction { + version: bitcoin::transaction::Version(2), + lock_time: absolute::LockTime::ZERO, + input: txins, + output: txouts, + }, + fee_amount, + input_amounts, + }) + } + + pub fn tx_mut(&mut self) -> &mut Transaction { + &mut self.tx + } + + pub fn tx(&self) -> &Transaction { + &self.tx + } + + pub fn connector_z_input(&self) -> Result { + tx_output_input(&self.tx, output_topology::pegin_deposit::connector_z()) + } + + pub fn change_input(&self) -> Result { + tx_output_input(&self.tx, output_topology::pegin_deposit::change()) + } +} +impl BaseTransaction for PegInDepositTransaction { + fn finalize(&self) -> Transaction { + self.tx.clone() + } + fn name(&self) -> &'static str { + "PegInDeposit" + } +} + +#[derive(Serialize, Deserialize, Eq, PartialEq, Clone)] +pub struct PegInRefundTransaction { + #[serde(with = "consensus::serde::With::")] + tx: Transaction, + #[serde(with = "consensus::serde::With::")] + prev_outs: Vec, + prev_scripts: Vec, +} +impl PreSignedTransaction for PegInRefundTransaction { + fn tx(&self) -> &Transaction { + &self.tx + } + + fn tx_mut(&mut self) -> &mut Transaction { + &mut self.tx + } + + fn prev_outs(&self) -> &Vec { + &self.prev_outs + } + + fn prev_scripts(&self) -> &Vec { + &self.prev_scripts + } +} +impl PegInRefundTransaction { + pub fn new_with_signature( + connector_z: &ConnectorZ, + input_0: Input, + refund_address: &Address, + fee_amount: Amount, + signature: bitcoin::taproot::Signature, + ) -> Result { + match Self::new_for_validation(connector_z, input_0, refund_address, fee_amount) { + Ok(mut this) => { + this.push_input_0_signature(connector_z, signature); + Ok(this) + } + Err(e) => Err(e), + } + } + + pub fn new_for_validation( + connector_z: &ConnectorZ, + input_0: Input, + refund_address: &Address, + fee_amount: Amount, + ) -> Result { + let input_0_leaf = 1; + let _input_0 = connector_z.generate_taproot_leaf_tx_in(input_0_leaf, &input_0); + + if input_0.amount < fee_amount { + return Err(Error::Transaction(InsufficientInputAmount)); + } + + let total_output_amount = input_0.amount - fee_amount; + let _output_0 = + TxOut { value: total_output_amount, script_pubkey: refund_address.script_pubkey() }; + + Ok(PegInRefundTransaction { + tx: Transaction { + version: bitcoin::transaction::Version(2), + lock_time: absolute::LockTime::ZERO, + input: vec![_input_0], + output: vec![_output_0], + }, + prev_outs: vec![TxOut { + value: input_0.amount, + script_pubkey: connector_z.generate_taproot_address().script_pubkey(), + }], + prev_scripts: vec![connector_z.generate_taproot_leaf_script(input_0_leaf)], + }) + } + + fn push_input_0_signature( + &mut self, + connector_z: &ConnectorZ, + signature: bitcoin::taproot::Signature, + ) { + let input_index = 0; + let script = &self.prev_scripts()[input_index].clone(); + let taproot_spend_info = connector_z.generate_taproot_spend_info(); + + populate_taproot_input_witness_with_signature( + self.tx_mut(), + input_index, + &taproot_spend_info, + script, + &[signature], + ); + } + + pub fn refund_input(&self) -> Result { + tx_output_input(&self.tx, output_topology::pegin_refund::refund()) + } +} +impl BaseTransaction for PegInRefundTransaction { + fn finalize(&self) -> Transaction { + self.tx.clone() + } + fn name(&self) -> &'static str { + "PegInRefund" + } +} + +#[derive(Serialize, Deserialize, Eq, PartialEq, Clone)] +pub struct PegInConfirmTransaction { + #[serde(with = "consensus::serde::With::")] + tx: Transaction, + #[serde(with = "consensus::serde::With::")] + prev_outs: Vec, + prev_scripts: Vec, +} +impl PreSignedTransaction for PegInConfirmTransaction { + fn tx(&self) -> &Transaction { + &self.tx + } + + fn tx_mut(&mut self) -> &mut Transaction { + &mut self.tx + } + + fn prev_outs(&self) -> &Vec { + &self.prev_outs + } + + fn prev_scripts(&self) -> &Vec { + &self.prev_scripts + } +} +impl PegInConfirmTransaction { + pub fn new_for_validation( + connector_0: &Connector0, + connector_z: &ConnectorZ, + input_0: Input, + fee_amount: Amount, + message: Vec, + ) -> Result { + let input_0_leaf = 0; + let _input_0 = connector_z.generate_taproot_leaf_tx_in(input_0_leaf, &input_0); + + if input_0.amount < fee_amount { + return Err(Error::Transaction(InsufficientInputAmount)); + } + + let total_output_amount = input_0.amount - fee_amount; + let output_0 = TxOut { + value: total_output_amount, + script_pubkey: connector_0.generate_taproot_address().script_pubkey(), + }; + let output_1 = + TxOut { value: Amount::ZERO, script_pubkey: generate_opreturn_script(message) }; + + Ok(PegInConfirmTransaction { + tx: Transaction { + version: bitcoin::transaction::Version(2), + lock_time: absolute::LockTime::ZERO, + input: vec![_input_0], + output: vec![output_0, output_1], + }, + prev_outs: vec![TxOut { + value: input_0.amount, + script_pubkey: connector_z.generate_taproot_address().script_pubkey(), + }], + prev_scripts: vec![connector_z.generate_taproot_leaf_script(input_0_leaf)], + }) + } + + pub fn sign_input_0_musig2( + &mut self, + context: &CommitteeContext, + sec_nonce: &SecNonce, + agg_nonce: &AggNonce, + ) -> Result { + let input_index = 0; + let sighash_type = TapSighashType::All; + generate_taproot_partial_signature( + context, + self.tx(), + sec_nonce, + agg_nonce, + input_index, + self.prev_outs(), + &self.prev_scripts()[input_index], + sighash_type, + ) + } + + pub fn aggregate_input_0_musig2_signatures( + &mut self, + context: &dyn BaseContext, + partial_sigs: Vec, + agg_nonce: &AggNonce, + ) -> Result { + let input_index = 0; + let sighash_type = TapSighashType::All; + match generate_taproot_aggregated_signature( + context, + self.tx(), + agg_nonce, + input_index, + self.prev_outs(), + &self.prev_scripts()[input_index], + sighash_type, + partial_sigs, + ) { + Ok(sig) => Ok(bitcoin::taproot::Signature { signature: sig.into(), sighash_type }), + Err(_) => Err(Error::Other("Failed to aggregate signatures")), + } + } + + pub fn push_input_0_signature( + &mut self, + connector_z: &ConnectorZ, + input_0_sig: bitcoin::taproot::Signature, + ) { + let input_index = 0; + let script = self.prev_scripts()[input_index].clone(); + let spend_info = connector_z.generate_taproot_spend_info(); + let tx_mut = self.tx_mut(); + // Push signature to witness + tx_mut.input[input_index].witness.push(input_0_sig.serialize()); + + // Push script + control block + push_taproot_leaf_script_and_control_block_to_witness( + tx_mut, + input_index, + &spend_info, + &script, + ); + } + + pub fn connector_0_input(&self) -> Result { + tx_output_input(&self.tx, output_topology::pegin_confirm::connector_0()) + } + + pub fn op_return_input(&self) -> Result { + tx_output_input(&self.tx, output_topology::pegin_confirm::op_return()) + } +} +impl BaseTransaction for PegInConfirmTransaction { + fn finalize(&self) -> Transaction { + self.tx.clone() + } + fn name(&self) -> &'static str { + "PegInConfirm" + } +} diff --git a/crates/goat/src/transactions/pre_signed.rs b/crates/goat/src/transactions/pre_signed.rs new file mode 100644 index 00000000..dd171d44 --- /dev/null +++ b/crates/goat/src/transactions/pre_signed.rs @@ -0,0 +1,72 @@ +use bitcoin::{ + key::Keypair, taproot::TaprootSpendInfo, EcdsaSighashType, PublicKey, ScriptBuf, + TapSighashType, Transaction, TxOut, +}; + +use super::{ + super::contexts::base::BaseContext, + signing::{ + populate_p2wpkh_witness, populate_p2wsh_witness, populate_taproot_input_witness_default, + }, +}; + +pub trait PreSignedTransaction { + fn tx(&self) -> &Transaction; + fn tx_mut(&mut self) -> &mut Transaction; + fn prev_outs(&self) -> &Vec; + fn prev_scripts(&self) -> &Vec; +} + +pub fn pre_sign_p2wsh_input( + tx: &mut T, + input_index: usize, + sighash_type: EcdsaSighashType, + keypairs: &Vec<&Keypair>, +) { + let script = &tx.prev_scripts()[input_index].clone(); + let value = tx.prev_outs()[input_index].value; + + populate_p2wsh_witness(tx.tx_mut(), input_index, sighash_type, script, value, keypairs); +} + +pub fn pre_sign_p2wpkh_input( + tx: &mut T, + context: &dyn BaseContext, + input_index: usize, + sighash_type: EcdsaSighashType, + public_key: &PublicKey, + keypair: &Keypair, +) { + let value = tx.prev_outs()[input_index].value; + + populate_p2wpkh_witness( + context, + tx.tx_mut(), + input_index, + sighash_type, + value, + public_key, + keypair, + ); +} + +pub fn pre_sign_taproot_input_default( + tx: &mut T, + input_index: usize, + sighash_type: TapSighashType, + taproot_spend_info: TaprootSpendInfo, + keypairs: &Vec<&Keypair>, +) { + let prev_outs = &tx.prev_outs().clone(); + let script = &tx.prev_scripts()[input_index].clone(); + + populate_taproot_input_witness_default( + tx.tx_mut(), + prev_outs, + input_index, + sighash_type, + &taproot_spend_info, + script, + keypairs, + ); +} diff --git a/crates/goat/src/transactions/pre_signed_musig2.rs b/crates/goat/src/transactions/pre_signed_musig2.rs new file mode 100644 index 00000000..a7b02176 --- /dev/null +++ b/crates/goat/src/transactions/pre_signed_musig2.rs @@ -0,0 +1,265 @@ +use bitcoin::{ + hashes::{sha256, Hash}, + key::Keypair, + taproot::TaprootSpendInfo, + PublicKey, TapSighashType, XOnlyPublicKey, +}; +use musig2::{ + secp::MaybeScalar, + secp256k1::{schnorr::Signature, Message}, + BinaryEncoding, PartialSignature, PubNonce, SecNonce, +}; +use std::collections::HashMap; + +use super::{ + super::contexts::{base::BaseContext, committee::CommitteeContext}, + super::error::Error, + pre_signed::PreSignedTransaction, + signing::push_taproot_leaf_script_and_control_block_to_witness, + signing_musig2::{ + generate_aggregated_nonce, generate_nonce, generate_taproot_aggregated_signature, + generate_taproot_partial_signature, + }, +}; + +pub fn generate_nonce_and_sign(signer_keypair: Keypair) -> (SecNonce, PubNonce, Signature) { + let secret_nonce = generate_nonce(); + let public_nonce = secret_nonce.public_nonce(); + let nonce_signature = + signer_keypair.sign_schnorr(get_nonce_message(&secret_nonce.public_nonce())); + (secret_nonce, public_nonce, nonce_signature) +} + +pub trait PreSignedMusig2Transaction: PreSignedTransaction { + fn musig2_nonces(&self) -> &HashMap>; + fn musig2_nonces_mut(&mut self) -> &mut HashMap>; + fn musig2_nonce_signatures(&self) -> &HashMap>; + fn musig2_nonce_signatures_mut(&mut self) + -> &mut HashMap>; + fn musig2_signatures(&self) -> &HashMap>; + fn musig2_signatures_mut( + &mut self, + ) -> &mut HashMap>; + fn verifier_inputs(&self) -> Vec; + fn has_nonces_for(&self, verifier_pubkey: PublicKey) -> bool { + self.has_all_nonces(&[verifier_pubkey]) + } + fn has_all_nonces(&self, verifier_pubkeys: &[PublicKey]) -> bool { + self.verifier_inputs().into_iter().all(|input_index| { + verifier_pubkeys.iter().all(|pubkey| { + self.musig2_nonces().contains_key(&input_index) + && self.musig2_nonces()[&input_index].contains_key(pubkey) + }) + }) + } + fn has_signatures_for(&self, verifier_pubkey: PublicKey) -> bool { + self.has_all_signatures(&[verifier_pubkey]) + } + fn has_all_signatures(&self, verifier_pubkeys: &[PublicKey]) -> bool { + self.verifier_inputs().into_iter().all(|input_index| { + verifier_pubkeys.iter().all(|pubkey| { + self.musig2_signatures().contains_key(&input_index) + && self.musig2_signatures()[&input_index].contains_key(pubkey) + }) + }) + } + + fn push_nonces(&mut self, context: &CommitteeContext) -> HashMap { + self.verifier_inputs() + .iter() + .map(|input_index| (*input_index, self.push_nonce(context, *input_index))) + .collect() + } + + fn push_nonce(&mut self, context: &CommitteeContext, input_index: usize) -> SecNonce { + // Push nonce + let musig2_nonces = self.musig2_nonces_mut(); + if musig2_nonces.get(&input_index).is_none() { + musig2_nonces.insert(input_index, HashMap::new()); + } + + let secret_nonce = generate_nonce(); + musig2_nonces + .get_mut(&input_index) + .unwrap() + .insert(context.committee_public_key, secret_nonce.public_nonce()); + + // Sign the nonce and push the signature + let musig2_nonce_signatures = self.musig2_nonce_signatures_mut(); + if musig2_nonce_signatures.get(&input_index).is_none() { + musig2_nonce_signatures.insert(input_index, HashMap::new()); + } + + let nonce_signature = + context.committee_keypair.sign_schnorr(get_nonce_message(&secret_nonce.public_nonce())); + + musig2_nonce_signatures + .get_mut(&input_index) + .unwrap() + .insert(context.committee_public_key, nonce_signature); + + secret_nonce + } + + fn push_nonces_with_sigs( + &mut self, + verifier_pubkey: &PublicKey, + nonces_with_sigs: &HashMap, + ) -> Option { + for index in self.verifier_inputs() { + let (pub_nonce, nonce_signature) = match nonces_with_sigs.get(&index) { + Some(v) => v, + _ => return Some(Error::Other("PubNonce for {index}'th input not found")), + }; + if !verify_public_nonce( + nonce_signature, + pub_nonce, + &XOnlyPublicKey::from(*verifier_pubkey), + ) { + return Some(Error::Other( + "invalid nonce signature for PubNonce of {index}'th input", + )); + } + + // push pub_nonce + let musig2_nonces = self.musig2_nonces_mut(); + if musig2_nonces.get(&index).is_none() { + musig2_nonces.insert(index, HashMap::new()); + } + musig2_nonces.get_mut(&index).unwrap().insert(*verifier_pubkey, pub_nonce.clone()); + + // push pub_nonce_signature + let musig2_nonce_signatures = self.musig2_nonce_signatures_mut(); + if musig2_nonce_signatures.get(&index).is_none() { + musig2_nonce_signatures.insert(index, HashMap::new()); + } + musig2_nonce_signatures + .get_mut(&index) + .unwrap() + .insert(*verifier_pubkey, *nonce_signature); + } + None + } + + fn push_partial_signatures( + &mut self, + verifier_pubkey: &PublicKey, + partial_sigs: &HashMap, + ) -> Option { + for index in self.verifier_inputs() { + let partial_signature = match partial_sigs.get(&index) { + Some(v) => v, + _ => return Some(Error::Other("partial signature for {index}'th input not found")), + }; + + let musig2_signatures = self.musig2_signatures_mut(); + if musig2_signatures.get(&index).is_none() { + musig2_signatures.insert(index, HashMap::new()); + } + musig2_signatures.get_mut(&index).unwrap().insert(*verifier_pubkey, *partial_signature); + } + None + } +} + +pub fn get_nonce_message(nonce: &PubNonce) -> Message { + let nonce_hash = sha256::Hash::hash(nonce.to_bytes().as_slice()); + Message::from_digest_slice(nonce_hash.as_ref()).expect("Failed to create nonce message") +} + +fn verify_schnorr_signature(sig: &Signature, msg: &Message, pubkey: &XOnlyPublicKey) -> bool { + match sig.verify(msg, pubkey) { + Ok(()) => true, + Err(e) => { + eprintln!("verify_schnorr() failed with: {e}"); + false + } + } +} + +pub fn verify_public_nonce(sig: &Signature, nonce: &PubNonce, pubkey: &XOnlyPublicKey) -> bool { + verify_schnorr_signature(sig, &get_nonce_message(nonce), pubkey) +} + +pub fn pre_sign_musig2_taproot_input( + tx: &mut T, + context: &CommitteeContext, + input_index: usize, + sighash_type: TapSighashType, + secret_nonce: &SecNonce, +) { + // TODO validate nonces first + + let prev_outs = &tx.prev_outs().clone(); + let script = &tx.prev_scripts()[input_index].clone(); + let musig2_nonces = &tx.musig2_nonces()[&input_index].values().cloned().collect(); + + let partial_signature = generate_taproot_partial_signature( + context, + tx.tx_mut(), + secret_nonce, + &generate_aggregated_nonce(musig2_nonces), + input_index, + prev_outs, + script, + sighash_type, + ) + .unwrap(); // TODO: Add error handling. + + let musig2_signatures = tx.musig2_signatures_mut(); + if musig2_signatures.get(&input_index).is_none() { + musig2_signatures.insert(input_index, HashMap::new()); + } + musig2_signatures + .get_mut(&input_index) + .unwrap() + .insert(context.committee_public_key, partial_signature); +} + +pub fn finalize_musig2_taproot_input( + tx: &mut T, + context: &dyn BaseContext, + input_index: usize, + sighash_type: TapSighashType, + taproot_spend_info: TaprootSpendInfo, +) { + // TODO: Verify we have partial signatures from all verifiers. + // TODO: Verify each signature against the signers public key. + // See example here: https://github.com/conduition/musig2/blob/c39bfce58098d337a3ec38b54d93def8306d9953/src/signing.rs#L358C1-L366C65 + + let prev_outs = &tx.prev_outs().clone(); + let script = &tx.prev_scripts()[input_index].clone(); + let musig2_nonces: &Vec = + &tx.musig2_nonces()[&input_index].values().cloned().collect(); + let musig2_signatures: Vec = tx.musig2_signatures()[&input_index] + .values() + .map(|&partial_signature| PartialSignature::from(partial_signature)) + .collect(); + let tx_mut = tx.tx_mut(); + + // Aggregate signature + let signature = generate_taproot_aggregated_signature( + context, + tx_mut, + &generate_aggregated_nonce(musig2_nonces), + input_index, + prev_outs, + script, + sighash_type, + musig2_signatures, // TODO: Is there a more elegant way of doing this? + ) + .unwrap(); // TODO: Add error handling. + + let final_signature = bitcoin::taproot::Signature { signature: signature.into(), sighash_type }; + + // Push signature to witness + tx_mut.input[input_index].witness.push(final_signature.serialize()); + + // Push script + control block + push_taproot_leaf_script_and_control_block_to_witness( + tx_mut, + input_index, + &taproot_spend_info, + script, + ); +} diff --git a/crates/goat/src/transactions/prekickoff.rs b/crates/goat/src/transactions/prekickoff.rs new file mode 100644 index 00000000..398b47d8 --- /dev/null +++ b/crates/goat/src/transactions/prekickoff.rs @@ -0,0 +1,759 @@ +use bitcoin::{ + absolute, consensus, Address, Amount, ScriptBuf, TapSighashType, Transaction, TxIn, TxOut, +}; +use musig2::{errors::SigningError, AggNonce, PartialSignature, SecNonce}; +use serde::{Deserialize, Serialize}; + +use crate::{ + connectors::{ + base::{generate_default_tx_in, TaprootConnector}, + kickoff_connectors::{ + ForceSkipConnector, GuardianConnector, KickoffConnector, PrekickoffConnector, + }, + }, + contexts::{base::BaseContext, committee::CommitteeContext, operator::OperatorContext}, + error::{Error, TransactionError::InsufficientInputAmount}, + scripts::p2a_output, + transactions::{ + signing::{ + populate_taproot_input_witness_default, + push_taproot_leaf_script_and_control_block_to_witness, + }, + signing_musig2::{ + generate_taproot_aggregated_signature, generate_taproot_partial_signature, + }, + }, +}; + +use super::{base::*, pre_signed::*}; + +pub fn operator_skip_kickoff( + context: &OperatorContext, + kickoff_connector: &KickoffConnector, + input_0: Input, + fee_amount: Amount, + receiver_address: Address, +) -> Result { + let input_0_leaf = 0; + let _input_0 = kickoff_connector.generate_taproot_leaf_tx_in(input_0_leaf, &input_0); + + if input_0.amount < fee_amount + Amount::from_sat(DUST_AMOUNT) { + return Err(Error::Transaction(InsufficientInputAmount)); + } + + let output_0 = TxOut { + value: input_0.amount - fee_amount, + script_pubkey: receiver_address.script_pubkey(), + }; + + let mut tx = Transaction { + version: bitcoin::transaction::Version(2), + lock_time: absolute::LockTime::ZERO, + input: vec![_input_0], + output: vec![output_0], + }; + + let prev_out = TxOut { + value: input_0.amount, + script_pubkey: kickoff_connector.generate_taproot_address().script_pubkey(), + }; + let prev_script = kickoff_connector.generate_taproot_leaf_script(input_0_leaf); + populate_taproot_input_witness_default( + &mut tx, + &[prev_out], + 0, + TapSighashType::All, + &kickoff_connector.generate_taproot_spend_info(), + &prev_script, + &vec![&context.operator_keypair], + ); + + Ok(tx) +} + +#[derive(Serialize, Deserialize, Eq, PartialEq, Clone)] +pub struct PrekickoffTransaction { + #[serde(with = "consensus::serde::With::")] + tx: Transaction, + #[serde(with = "consensus::serde::With::")] + prev_outs: Vec, + prev_scripts: Vec, + pub input_amounts: Vec, +} +impl PreSignedTransaction for PrekickoffTransaction { + fn tx(&self) -> &Transaction { + &self.tx + } + + fn tx_mut(&mut self) -> &mut Transaction { + &mut self.tx + } + + fn prev_outs(&self) -> &Vec { + &self.prev_outs + } + + fn prev_scripts(&self) -> &Vec { + &self.prev_scripts + } +} +impl PrekickoffTransaction { + #[allow(clippy::too_many_arguments)] + pub fn new_for_validation( + prev_prekickoff_connector: &PrekickoffConnector, + force_skip_connector: &ForceSkipConnector, + kickoff_connector: &KickoffConnector, + prekickoff_connector: &PrekickoffConnector, + input_0: Input, + replenish_fee_inputs: Vec, + replenish_fee_prev_outs: Vec, + // replenish_fee_prev_scripts: Vec, + fee_amount: u64, + watchtower_num: usize, + verifier_num: usize, + ) -> Result { + let mut input_amounts = vec![input_0.amount]; + let replenish_fee_input_amounts: Vec = + replenish_fee_inputs.iter().map(|input| input.amount).collect(); + input_amounts.extend(replenish_fee_input_amounts); + + let input_0_leaf = 0; + let _input_0 = + prev_prekickoff_connector.generate_taproot_leaf_tx_in(input_0_leaf, &input_0); + let mut txins = vec![_input_0]; + let mut total_input_amount = input_0.amount; + txins.extend( + replenish_fee_inputs + .iter() + .map(|input| { + total_input_amount += input.amount; + generate_default_tx_in(input) + }) + .collect::>(), + ); + + if total_input_amount + < Amount::from_sat( + fee_amount + + 2 * DUST_AMOUNT + + P2A_AMOUNT + + max_pegout_cost(watchtower_num, verifier_num), + ) + { + return Err(Error::Transaction(InsufficientInputAmount)); + } + + let total_output_amount = total_input_amount - Amount::from_sat(fee_amount); + let output_0 = TxOut { + value: Amount::from_sat(DUST_AMOUNT), + script_pubkey: force_skip_connector.generate_taproot_address().script_pubkey(), + }; + let output_1 = TxOut { + value: Amount::from_sat(max_pegout_cost(watchtower_num, verifier_num)), + script_pubkey: kickoff_connector.generate_taproot_address().script_pubkey(), + }; + let output_3 = p2a_output(); + let output_2 = TxOut { + value: total_output_amount - output_0.value - output_1.value - output_3.value, + script_pubkey: prekickoff_connector.generate_taproot_address().script_pubkey(), + }; + + let mut prev_outs = vec![TxOut { + value: input_0.amount, + script_pubkey: prev_prekickoff_connector.generate_taproot_address().script_pubkey(), + }]; + prev_outs.extend(replenish_fee_prev_outs); + + let prev_scripts = + vec![prev_prekickoff_connector.generate_taproot_leaf_script(input_0_leaf)]; + // prev_scripts.extend(replenish_fee_prev_scripts); + + Ok(PrekickoffTransaction { + tx: Transaction { + version: bitcoin::transaction::Version(2), + lock_time: absolute::LockTime::ZERO, + input: txins, + output: vec![output_0, output_1, output_2, output_3], + }, + prev_outs, + prev_scripts, + input_amounts, + }) + } + + pub fn sign_input_0( + &mut self, + context: &OperatorContext, + prev_prekickoff_connector: &PrekickoffConnector, + ) { + let input_index = 0; + pre_sign_taproot_input_default( + self, + input_index, + TapSighashType::All, + prev_prekickoff_connector.generate_taproot_spend_info(), + &vec![&context.operator_keypair], + ); + } + + pub fn force_skip_connector_input(&self) -> Result { + tx_output_input(&self.tx, output_topology::prekickoff::force_skip_connector()) + } + + pub fn kickoff_connector_input(&self) -> Result { + tx_output_input(&self.tx, output_topology::prekickoff::kickoff_connector()) + } + + pub fn prekickoff_connector_input(&self) -> Result { + tx_output_input(&self.tx, output_topology::prekickoff::prekickoff_connector()) + } + + pub fn anchor_input(&self) -> Result { + tx_output_input(&self.tx, output_topology::prekickoff::anchor()) + } +} +impl BaseTransaction for PrekickoffTransaction { + fn finalize(&self) -> Transaction { + self.tx.clone() + } + fn name(&self) -> &'static str { + "Prekickoff" + } +} + +#[derive(Serialize, Deserialize, Eq, PartialEq, Clone)] +pub struct ForceSkipKickoffTransaction { + #[serde(with = "consensus::serde::With::")] + tx: Transaction, + #[serde(with = "consensus::serde::With::")] + prev_outs: Vec, + prev_scripts: Vec, +} +impl PreSignedTransaction for ForceSkipKickoffTransaction { + fn tx(&self) -> &Transaction { + &self.tx + } + + fn tx_mut(&mut self) -> &mut Transaction { + &mut self.tx + } + + fn prev_outs(&self) -> &Vec { + &self.prev_outs + } + + fn prev_scripts(&self) -> &Vec { + &self.prev_scripts + } +} +impl ForceSkipKickoffTransaction { + pub fn new_presigned( + context: &OperatorContext, + kickoff_connector: &KickoffConnector, + next_force_skip_connector: &ForceSkipConnector, + input_0: Input, + input_1: Input, + ) -> Self { + let mut this = Self::new_for_validation( + kickoff_connector, + next_force_skip_connector, + input_0, + input_1, + ); + + this.pre_sign_and_push(context, kickoff_connector); + + this + } + + pub fn new_for_validation( + kickoff_connector: &KickoffConnector, + next_force_skip_connector: &ForceSkipConnector, + input_0: Input, + input_1: Input, + ) -> Self { + let input_0_leaf = 0; + let _input_0 = kickoff_connector.generate_taproot_leaf_tx_in(input_0_leaf, &input_0); + + let input_1_leaf = 0; + let _input_1 = + next_force_skip_connector.generate_taproot_leaf_tx_in(input_1_leaf, &input_1); + + ForceSkipKickoffTransaction { + tx: Transaction { + version: bitcoin::transaction::Version(2), + lock_time: absolute::LockTime::ZERO, + input: vec![_input_0, _input_1], + output: vec![], // output will be added later + }, + prev_outs: vec![ + TxOut { + value: input_0.amount, + script_pubkey: kickoff_connector.generate_taproot_address().script_pubkey(), + }, + TxOut { + value: input_1.amount, + script_pubkey: next_force_skip_connector + .generate_taproot_address() + .script_pubkey(), + }, + ], + prev_scripts: vec![ + kickoff_connector.generate_taproot_leaf_script(input_0_leaf), + next_force_skip_connector.generate_taproot_leaf_script(input_1_leaf), + ], + } + } + + pub fn sign_input_0( + &mut self, + context: &OperatorContext, + kickoff_connector: &KickoffConnector, + ) { + let input_index = 0; + pre_sign_taproot_input_default( + self, + input_index, + TapSighashType::None, + kickoff_connector.generate_taproot_spend_info(), + &vec![&context.operator_keypair], + ); + } + + fn sign_input_1_musig2( + &mut self, + context: &CommitteeContext, + sec_nonce: &SecNonce, + agg_nonce: &AggNonce, + ) -> Result { + let input_index = 1; + let sighash_type = TapSighashType::None; + generate_taproot_partial_signature( + context, + self.tx(), + sec_nonce, + agg_nonce, + input_index, + self.prev_outs(), + &self.prev_scripts()[input_index], + sighash_type, + ) + } + + fn push_input_1_signature( + &mut self, + next_force_skip_connector: &ForceSkipConnector, + input_1_sig: bitcoin::taproot::Signature, + ) { + let input_index = 1; + let script = self.prev_scripts()[input_index].clone(); + let spend_info = next_force_skip_connector.generate_taproot_spend_info(); + let tx_mut = self.tx_mut(); + tx_mut.input[input_index].witness.push(input_1_sig.serialize()); + push_taproot_leaf_script_and_control_block_to_witness( + tx_mut, + input_index, + &spend_info, + &script, + ); + } + + pub fn pre_sign( + &mut self, + context: &CommitteeContext, + sec_nonces: &[SecNonce; 1], + agg_nonces: &[AggNonce; 1], + ) -> Result<[PartialSignature; 1], SigningError> { + let input_1_sig = self.sign_input_1_musig2(context, &sec_nonces[0], &agg_nonces[0]); + match [input_1_sig].into_iter().collect::, SigningError>>() { + Ok(sigs) => Ok(sigs.try_into().unwrap()), + Err(e) => Err(e), + } + } + + pub fn aggregate_pre_sigs( + &self, + context: &dyn BaseContext, + partial_signatures: &[Vec; 1], + agg_nonces: &[AggNonce; 1], + ) -> Result<[bitcoin::taproot::Signature; 1], Error> { + let input_index = 1; + let sig_index = 0; + let sighash_type = TapSighashType::None; + let input_1_sig = match generate_taproot_aggregated_signature( + context, + self.tx(), + &agg_nonces[sig_index], + input_index, + self.prev_outs(), + &self.prev_scripts()[input_index], + sighash_type, + partial_signatures[sig_index].clone(), + ) { + Ok(sig) => bitcoin::taproot::Signature { signature: sig.into(), sighash_type }, + Err(_) => return Err(Error::Other("Failed to aggregate signatures")), + }; + Ok([input_1_sig]) + } + + pub fn push_pre_sigs( + &mut self, + next_force_skip_connector: &ForceSkipConnector, + pre_sigs: [bitcoin::taproot::Signature; 1], + ) { + self.push_input_1_signature(next_force_skip_connector, pre_sigs[0]); + } + + /// Signs input 0. Input 1 spends the force-skip connector and is signed + /// by the committee through `pre_sign`. + pub fn pre_sign_and_push( + &mut self, + context: &OperatorContext, + kickoff_connector: &KickoffConnector, + ) { + self.sign_input_0(context, kickoff_connector); + } +} +impl BaseTransaction for ForceSkipKickoffTransaction { + fn finalize(&self) -> Transaction { + self.tx.clone() + } + fn name(&self) -> &'static str { + "ForceSkipKickoff" + } +} + +#[derive(Serialize, Deserialize, Eq, PartialEq, Clone)] +pub struct QuickChallengeTransaction { + #[serde(with = "consensus::serde::With::")] + tx: Transaction, + #[serde(with = "consensus::serde::With::")] + prev_outs: Vec, + prev_scripts: Vec, +} +impl PreSignedTransaction for QuickChallengeTransaction { + fn tx(&self) -> &Transaction { + &self.tx + } + + fn tx_mut(&mut self) -> &mut Transaction { + &mut self.tx + } + + fn prev_outs(&self) -> &Vec { + &self.prev_outs + } + + fn prev_scripts(&self) -> &Vec { + &self.prev_scripts + } +} +impl QuickChallengeTransaction { + pub fn new_presigned( + context: &OperatorContext, + guardian_connector: &GuardianConnector, + next_force_skip_connector: &ForceSkipConnector, + input_0: Input, + input_1: Input, + ) -> Self { + let mut this = Self::new_for_validation( + guardian_connector, + next_force_skip_connector, + input_0, + input_1, + ); + + this.pre_sign_and_push(context, guardian_connector); + + this + } + + pub fn new_for_validation( + guardian_connector: &GuardianConnector, + next_force_skip_connector: &ForceSkipConnector, + input_0: Input, + input_1: Input, + ) -> Self { + let input_0_leaf = 0; + let _input_0 = guardian_connector.generate_taproot_leaf_tx_in(input_0_leaf, &input_0); + + let input_1_leaf = 0; + let _input_1 = + next_force_skip_connector.generate_taproot_leaf_tx_in(input_1_leaf, &input_1); + + QuickChallengeTransaction { + tx: Transaction { + version: bitcoin::transaction::Version(2), + lock_time: absolute::LockTime::ZERO, + input: vec![_input_0, _input_1], + output: vec![], // output will be added later + }, + prev_outs: vec![ + TxOut { + value: input_0.amount, + script_pubkey: guardian_connector.generate_taproot_address().script_pubkey(), + }, + TxOut { + value: input_1.amount, + script_pubkey: next_force_skip_connector + .generate_taproot_address() + .script_pubkey(), + }, + ], + prev_scripts: vec![ + guardian_connector.generate_taproot_leaf_script(input_0_leaf), + next_force_skip_connector.generate_taproot_leaf_script(input_1_leaf), + ], + } + } + + pub fn sign_input_0( + &mut self, + context: &OperatorContext, + guardian_connector: &GuardianConnector, + ) { + let input_index = 0; + pre_sign_taproot_input_default( + self, + input_index, + TapSighashType::None, + guardian_connector.generate_taproot_spend_info(), + &vec![&context.operator_keypair], + ); + } + + fn sign_input_1_musig2( + &mut self, + context: &CommitteeContext, + sec_nonce: &SecNonce, + agg_nonce: &AggNonce, + ) -> Result { + let input_index = 1; + let sighash_type = TapSighashType::None; + generate_taproot_partial_signature( + context, + self.tx(), + sec_nonce, + agg_nonce, + input_index, + self.prev_outs(), + &self.prev_scripts()[input_index], + sighash_type, + ) + } + + fn push_input_1_signature( + &mut self, + next_force_skip_connector: &ForceSkipConnector, + input_1_sig: bitcoin::taproot::Signature, + ) { + let input_index = 1; + let script = self.prev_scripts()[input_index].clone(); + let spend_info = next_force_skip_connector.generate_taproot_spend_info(); + let tx_mut = self.tx_mut(); + tx_mut.input[input_index].witness.push(input_1_sig.serialize()); + push_taproot_leaf_script_and_control_block_to_witness( + tx_mut, + input_index, + &spend_info, + &script, + ); + } + + pub fn pre_sign( + &mut self, + context: &CommitteeContext, + sec_nonces: &[SecNonce; 1], + agg_nonces: &[AggNonce; 1], + ) -> Result<[PartialSignature; 1], SigningError> { + let input_1_sig = self.sign_input_1_musig2(context, &sec_nonces[0], &agg_nonces[0]); + match [input_1_sig].into_iter().collect::, SigningError>>() { + Ok(sigs) => Ok(sigs.try_into().unwrap()), + Err(e) => Err(e), + } + } + + pub fn aggregate_pre_sigs( + &self, + context: &dyn BaseContext, + partial_signatures: &[Vec; 1], + agg_nonces: &[AggNonce; 1], + ) -> Result<[bitcoin::taproot::Signature; 1], Error> { + let input_index = 1; + let sig_index = 0; + let sighash_type = TapSighashType::None; + let input_1_sig = match generate_taproot_aggregated_signature( + context, + self.tx(), + &agg_nonces[sig_index], + input_index, + self.prev_outs(), + &self.prev_scripts()[input_index], + sighash_type, + partial_signatures[sig_index].clone(), + ) { + Ok(sig) => bitcoin::taproot::Signature { signature: sig.into(), sighash_type }, + Err(_) => return Err(Error::Other("Failed to aggregate signatures")), + }; + Ok([input_1_sig]) + } + + pub fn push_pre_sigs( + &mut self, + next_force_skip_connector: &ForceSkipConnector, + pre_sigs: [bitcoin::taproot::Signature; 1], + ) { + self.push_input_1_signature(next_force_skip_connector, pre_sigs[0]); + } + + /// Signs input 0. Input 1 spends the force-skip connector and is signed + /// by the committee through `pre_sign`. + pub fn pre_sign_and_push( + &mut self, + context: &OperatorContext, + guardian_connector: &GuardianConnector, + ) { + self.sign_input_0(context, guardian_connector); + } +} +impl BaseTransaction for QuickChallengeTransaction { + fn finalize(&self) -> Transaction { + self.tx.clone() + } + fn name(&self) -> &'static str { + "QuickChallenge" + } +} + +#[derive(Serialize, Deserialize, Eq, PartialEq, Clone)] +pub struct ChallengeIncompleteKickoffTransaction { + #[serde(with = "consensus::serde::With::")] + tx: Transaction, + #[serde(with = "consensus::serde::With::")] + prev_outs: Vec, + prev_scripts: Vec, +} +impl PreSignedTransaction for ChallengeIncompleteKickoffTransaction { + fn tx(&self) -> &Transaction { + &self.tx + } + + fn tx_mut(&mut self) -> &mut Transaction { + &mut self.tx + } + + fn prev_outs(&self) -> &Vec { + &self.prev_outs + } + + fn prev_scripts(&self) -> &Vec { + &self.prev_scripts + } +} +impl ChallengeIncompleteKickoffTransaction { + pub fn new_presigned( + context: &OperatorContext, + guardian_connector: &GuardianConnector, + next_prekickoff_connector: &PrekickoffConnector, + input_0: Input, + input_1: Input, + ) -> Self { + let mut this = Self::new_for_validation( + guardian_connector, + next_prekickoff_connector, + input_0, + input_1, + ); + + this.pre_sign_and_push(context, guardian_connector, next_prekickoff_connector); + + this + } + + pub fn new_for_validation( + guardian_connector: &GuardianConnector, + next_prekickoff_connector: &PrekickoffConnector, + input_0: Input, + input_1: Input, + ) -> Self { + let input_0_leaf = 0; + let _input_0 = guardian_connector.generate_taproot_leaf_tx_in(input_0_leaf, &input_0); + + let input_1_leaf = 0; + let _input_1 = + next_prekickoff_connector.generate_taproot_leaf_tx_in(input_1_leaf, &input_1); + + ChallengeIncompleteKickoffTransaction { + tx: Transaction { + version: bitcoin::transaction::Version(2), + lock_time: absolute::LockTime::ZERO, + input: vec![_input_0, _input_1], + output: vec![], // output will be added later + }, + prev_outs: vec![ + TxOut { + value: input_0.amount, + script_pubkey: guardian_connector.generate_taproot_address().script_pubkey(), + }, + TxOut { + value: input_1.amount, + script_pubkey: next_prekickoff_connector + .generate_taproot_address() + .script_pubkey(), + }, + ], + prev_scripts: vec![ + guardian_connector.generate_taproot_leaf_script(input_0_leaf), + next_prekickoff_connector.generate_taproot_leaf_script(input_1_leaf), + ], + } + } + + pub fn sign_input_0( + &mut self, + context: &OperatorContext, + guardian_connector: &GuardianConnector, + ) { + let input_index = 0; + pre_sign_taproot_input_default( + self, + input_index, + TapSighashType::None, + guardian_connector.generate_taproot_spend_info(), + &vec![&context.operator_keypair], + ); + } + + pub fn sign_input_1( + &mut self, + context: &OperatorContext, + next_prekickoff_connector: &PrekickoffConnector, + ) { + let input_index = 1; + pre_sign_taproot_input_default( + self, + input_index, + TapSighashType::None, + next_prekickoff_connector.generate_taproot_spend_info(), + &vec![&context.operator_keypair], + ); + } + + pub fn pre_sign_and_push( + &mut self, + context: &OperatorContext, + guardian_connector: &GuardianConnector, + next_prekickoff_connector: &PrekickoffConnector, + ) { + self.sign_input_0(context, guardian_connector); + self.sign_input_1(context, next_prekickoff_connector); + } +} +impl BaseTransaction for ChallengeIncompleteKickoffTransaction { + fn finalize(&self) -> Transaction { + self.tx.clone() + } + fn name(&self) -> &'static str { + "ChallengeIncompleteKickoff" + } +} diff --git a/crates/goat/src/transactions/signing.rs b/crates/goat/src/transactions/signing.rs new file mode 100644 index 00000000..69b86213 --- /dev/null +++ b/crates/goat/src/transactions/signing.rs @@ -0,0 +1,360 @@ +use bitcoin::{ + key::{Keypair, TapTweak}, + secp256k1::Message, + sighash::{Prevouts, SighashCache}, + taproot::{LeafVersion, TaprootSpendInfo}, + Amount, EcdsaSighashType, PublicKey, Script, ScriptBuf, TapLeafHash, TapNodeHash, + TapSighashType, Transaction, TxIn, TxOut, +}; +use secp256k1::SECP256K1; + +use super::super::{contexts::base::BaseContext, scripts::generate_p2wpkh_address}; + +pub fn generate_p2wsh_schnorr_signature( + tx: &mut Transaction, + input_index: usize, + sighash_type: EcdsaSighashType, + script: &Script, + value: Amount, + keypair: &Keypair, +) -> bitcoin::ecdsa::Signature { + let mut sighash_cache = SighashCache::new(tx); + + let sighash = sighash_cache + .p2wsh_signature_hash(input_index, script, value, sighash_type) + .expect("Failed to construct sighash"); + + let signature = keypair.secret_key().sign_ecdsa(Message::from(sighash)); + + bitcoin::ecdsa::Signature { signature, sighash_type } +} + +pub fn push_p2wsh_signature_to_witness( + tx: &mut Transaction, + input_index: usize, + sighash_type: EcdsaSighashType, + script: &Script, + value: Amount, + keypair: &Keypair, +) { + let signature = + generate_p2wsh_schnorr_signature(tx, input_index, sighash_type, script, value, keypair); + + tx.input[input_index].witness.push_ecdsa_signature(&signature); +} + +pub fn push_p2wsh_script_to_witness(tx: &mut Transaction, input_index: usize, script: &Script) { + tx.input[input_index].witness.push(script); +} + +pub fn populate_p2wsh_witness( + tx: &mut Transaction, + input_index: usize, + sighash_type: EcdsaSighashType, + script: &Script, + value: Amount, + keypairs: &Vec<&Keypair>, +) { + for keypair in keypairs { + push_p2wsh_signature_to_witness(tx, input_index, sighash_type, script, value, keypair); + } + push_p2wsh_script_to_witness(tx, input_index, script); +} + +pub fn populate_p2wsh_witness_with_signatures( + tx: &mut Transaction, + input_index: usize, + script: &Script, + signatures: &Vec, +) { + for signature in signatures { + tx.input[input_index].witness.push_ecdsa_signature(signature); + } + push_p2wsh_script_to_witness(tx, input_index, script); +} + +pub fn generate_p2wpkh_schnorr_signature( + context: &dyn BaseContext, + tx: &mut Transaction, + input_index: usize, + sighash_type: EcdsaSighashType, + value: Amount, + public_key: &PublicKey, + keypair: &Keypair, +) -> bitcoin::ecdsa::Signature { + let mut sighash_cache = SighashCache::new(tx); + + let sighash = sighash_cache + .p2wpkh_signature_hash( + input_index, + &generate_p2wpkh_address(context.network(), public_key).script_pubkey(), + value, + sighash_type, + ) + .expect("Failed to construct sighash"); + + let signature = keypair.secret_key().sign_ecdsa(Message::from(sighash)); + + bitcoin::ecdsa::Signature { signature, sighash_type } +} + +pub fn push_p2wpkh_signature_to_witness( + context: &dyn BaseContext, + tx: &mut Transaction, + input_index: usize, + sighash_type: EcdsaSighashType, + value: Amount, + public_key: &PublicKey, + keypair: &Keypair, +) { + let signature = generate_p2wpkh_schnorr_signature( + context, + tx, + input_index, + sighash_type, + value, + public_key, + keypair, + ); + + tx.input[input_index].witness.push_ecdsa_signature(&signature); +} + +pub fn push_p2wpkh_public_key_to_witness( + tx: &mut Transaction, + input_index: usize, + public_key: &PublicKey, +) { + tx.input[input_index].witness.push(public_key.to_bytes()); +} + +pub fn populate_p2wpkh_witness( + context: &dyn BaseContext, + tx: &mut Transaction, + input_index: usize, + sighash_type: EcdsaSighashType, + value: Amount, + public_key: &PublicKey, + keypair: &Keypair, +) { + push_p2wpkh_signature_to_witness( + context, + tx, + input_index, + sighash_type, + value, + public_key, + keypair, + ); + push_p2wpkh_public_key_to_witness(tx, input_index, public_key); +} + +pub fn generate_taproot_leaf_schnorr_signature( + tx: &mut Transaction, + prev_outs: &[TxOut], + input_index: usize, + sighash_type: TapSighashType, + script: &Script, + keypair: &Keypair, +) -> bitcoin::taproot::Signature { + let leaf_hash = TapLeafHash::from_script(script, LeafVersion::TapScript); + + let sighash = if sighash_type == TapSighashType::AllPlusAnyoneCanPay + || sighash_type == TapSighashType::SinglePlusAnyoneCanPay + || sighash_type == TapSighashType::NonePlusAnyoneCanPay + { + SighashCache::new(tx) + .taproot_script_spend_signature_hash( + input_index, + &Prevouts::One(input_index, &prev_outs[input_index]), + leaf_hash, + sighash_type, + ) + .expect("Failed to construct sighash") + } else { + SighashCache::new(tx) + .taproot_script_spend_signature_hash( + input_index, + &Prevouts::All(prev_outs), + leaf_hash, + sighash_type, + ) + .expect("Failed to construct sighash") + }; + + // If secp256k1 is updated to 0.30.0, the following line can be replaced with + // let signature = keypair.sign_schnorr_no_aux_rand(&Message::from(sighash)); + let signature = SECP256K1.sign_schnorr_no_aux_rand(&Message::from(sighash), keypair); + + bitcoin::taproot::Signature { signature, sighash_type } +} + +pub fn push_taproot_leaf_unlock_data_to_witness( + tx: &mut Transaction, + input_index: usize, + unlock_data: Vec>, +) { + for element in unlock_data.iter() { + if element.len() == 1 && element[0] == 0 { + tx.input[input_index].witness.push(vec![]); // minimal encoding of 0 is an empty vector + } else { + tx.input[input_index].witness.push(element); + } + } +} + +pub fn push_taproot_leaf_script_and_control_block_to_witness( + tx: &mut Transaction, + input_index: usize, + taproot_spend_info: &TaprootSpendInfo, + script: &Script, +) { + let prevout_leaf = (ScriptBuf::from(script), LeafVersion::TapScript); + + let control_block = + taproot_spend_info.control_block(&prevout_leaf).expect("Unable to create Control block"); + + tx.input[input_index].witness.push(prevout_leaf.0.to_bytes()); + + tx.input[input_index].witness.push(control_block.serialize()); +} + +/// Use this function to populate taproot input witness for +/// scripts containing any logic. +pub fn populate_taproot_input_witness( + tx: &mut Transaction, + input_index: usize, + taproot_spend_info: &TaprootSpendInfo, + script: &Script, + unlock_data: Vec>, +) { + push_taproot_leaf_unlock_data_to_witness(tx, input_index, unlock_data); + push_taproot_leaf_script_and_control_block_to_witness( + tx, + input_index, + taproot_spend_info, + script, + ); +} + +pub fn populate_taproot_txin_witness( + txin: &mut TxIn, + taproot_spend_info: &TaprootSpendInfo, + script: &Script, + unlock_data: Vec>, +) { + // push unlock data + for element in unlock_data.iter() { + if element.len() == 1 && element[0] == 0 { + txin.witness.push(vec![]); // minimal encoding of 0 is an empty vector + } else { + txin.witness.push(element); + } + } + + // push script and control block + let prevout_leaf = (ScriptBuf::from(script), LeafVersion::TapScript); + let control_block = + taproot_spend_info.control_block(&prevout_leaf).expect("Unable to create Control block"); + txin.witness.push(prevout_leaf.0.to_bytes()); + txin.witness.push(control_block.serialize()); +} + +/// Use this function to populate taproot input witness for +/// scripts containing only OP_CHECKSIG verification. +#[allow(clippy::too_many_arguments)] +pub fn populate_taproot_input_witness_default( + tx: &mut Transaction, + prevouts: &[TxOut], + input_index: usize, + sighash_type: TapSighashType, + taproot_spend_info: &TaprootSpendInfo, + script: &Script, + keypairs: &Vec<&Keypair>, +) { + let mut unlock_data: Vec> = Vec::new(); + for keypair in keypairs { + let schnorr_signature = generate_taproot_leaf_schnorr_signature( + tx, + prevouts, + input_index, + sighash_type, + script, + keypair, + ); + unlock_data.push(schnorr_signature.to_vec()); + } + + push_taproot_leaf_unlock_data_to_witness(tx, input_index, unlock_data); + push_taproot_leaf_script_and_control_block_to_witness( + tx, + input_index, + taproot_spend_info, + script, + ); +} + +pub fn populate_taproot_input_witness_with_signature( + tx: &mut Transaction, + input_index: usize, + taproot_spend_info: &TaprootSpendInfo, + script: &Script, + signatures: &[bitcoin::taproot::Signature], +) { + let unlock_data = signatures.iter().map(|sig| sig.to_vec()).collect(); + populate_taproot_input_witness(tx, input_index, taproot_spend_info, script, unlock_data); +} + +fn generate_p2tr_key_spend_schnorr_signature( + tx: &mut Transaction, + input_index: usize, + prev_outs: &[TxOut], + sighash_type: TapSighashType, + merkle_root: Option, + keypair: &Keypair, +) -> bitcoin::taproot::Signature { + let sighash = if sighash_type == TapSighashType::AllPlusAnyoneCanPay + || sighash_type == TapSighashType::SinglePlusAnyoneCanPay + || sighash_type == TapSighashType::NonePlusAnyoneCanPay + { + SighashCache::new(tx) + .taproot_key_spend_signature_hash( + input_index, + &Prevouts::One(input_index, &prev_outs[input_index]), + sighash_type, + ) + .expect("Failed to construct sighash") + } else { + SighashCache::new(tx) + .taproot_key_spend_signature_hash(input_index, &Prevouts::All(prev_outs), sighash_type) + .expect("Failed to construct sighash") + }; + + let tweak_keypair = keypair.tap_tweak(SECP256K1, merkle_root); + + // If secp256k1 is updated to 0.30.0, the following line can be replaced with + // let signature = keypair.sign_schnorr_no_aux_rand(&Message::from(sighash)); + let signature = + SECP256K1.sign_schnorr_no_aux_rand(&Message::from(sighash), &tweak_keypair.to_keypair()); + + bitcoin::taproot::Signature { signature, sighash_type } +} + +pub fn populate_p2tr_key_spend_witness( + tx: &mut Transaction, + input_index: usize, + prev_outs: &[TxOut], + sighash_type: TapSighashType, + merkle_root: Option, + keypair: &Keypair, +) { + let signature = generate_p2tr_key_spend_schnorr_signature( + tx, + input_index, + prev_outs, + sighash_type, + merkle_root, + keypair, + ); + tx.input[input_index].witness.push(signature.to_vec()); +} diff --git a/crates/goat/src/transactions/signing_musig2.rs b/crates/goat/src/transactions/signing_musig2.rs new file mode 100644 index 00000000..f2cff3db --- /dev/null +++ b/crates/goat/src/transactions/signing_musig2.rs @@ -0,0 +1,102 @@ +#![allow(clippy::too_many_arguments)] +use bitcoin::{ + sighash::{Prevouts, SighashCache}, + taproot::LeafVersion, + Script, TapLeafHash, TapSighash, TapSighashType, Transaction, TxOut, +}; +use musig2::{ + aggregate_partial_signatures, + errors::{SigningError, VerifyError}, + secp::{MaybeScalar, Point}, + sign_partial, AggNonce, KeyAggContext, LiftedSignature, PartialSignature, PubNonce, SecNonce, +}; + +use super::super::contexts::{base::BaseContext, committee::CommitteeContext}; + +fn taproot_script_spend_sighash( + tx: &Transaction, + input_index: usize, + prevouts: &[TxOut], + leaf_hash: TapLeafHash, + sighash_type: TapSighashType, +) -> TapSighash { + if sighash_type == TapSighashType::AllPlusAnyoneCanPay + || sighash_type == TapSighashType::SinglePlusAnyoneCanPay + || sighash_type == TapSighashType::NonePlusAnyoneCanPay + { + SighashCache::new(tx) + .taproot_script_spend_signature_hash( + input_index, + &Prevouts::One(input_index, &prevouts[input_index]), + leaf_hash, + sighash_type, + ) + .expect("Failed to construct sighash") + } else { + SighashCache::new(tx) + .taproot_script_spend_signature_hash( + input_index, + &Prevouts::All(prevouts), + leaf_hash, + sighash_type, + ) + .expect("Failed to construct sighash") + } +} + +pub fn generate_nonce() -> SecNonce { + SecNonce::build(&mut rand::rngs::OsRng).build() +} + +pub fn generate_aggregated_nonce(nonces: &Vec) -> AggNonce { + AggNonce::sum(nonces) +} + +pub fn generate_taproot_partial_signature( + context: &CommitteeContext, + tx: &Transaction, + secret_nonce: &SecNonce, + aggregated_nonce: &AggNonce, + input_index: usize, + prevouts: &[TxOut], + script: &Script, + sighash_type: TapSighashType, +) -> Result { + let pubkeys: Vec = Vec::from_iter( + context.n_of_n_public_keys.iter().map(|&public_key| public_key.inner.into()), + ); // TODO: The tests will reveal whether this conversion works as expected. + let key_agg_ctx = KeyAggContext::new(pubkeys).unwrap(); + + let leaf_hash = TapLeafHash::from_script(script, LeafVersion::TapScript); + let sighash = taproot_script_spend_sighash(tx, input_index, prevouts, leaf_hash, sighash_type); + + sign_partial( + &key_agg_ctx, + context.committee_keypair.secret_key(), + secret_nonce.clone(), + aggregated_nonce, + sighash, + ) +} + +pub fn generate_taproot_aggregated_signature( + context: &dyn BaseContext, + tx: &Transaction, + aggregated_nonce: &AggNonce, + input_index: usize, + prevouts: &[TxOut], + script: &Script, + sighash_type: TapSighashType, + partial_signatures: Vec, +) -> Result { + let pubkeys: Vec = Vec::from_iter( + context.n_of_n_public_keys().iter().map(|&public_key| public_key.inner.into()), + ); + let key_agg_ctx = KeyAggContext::new(pubkeys).unwrap(); + + let leaf_hash = TapLeafHash::from_script(script, LeafVersion::TapScript); + let sighash_cache = + taproot_script_spend_sighash(tx, input_index, prevouts, leaf_hash, sighash_type); + + aggregate_partial_signatures(&key_agg_ctx, aggregated_nonce, partial_signatures, sighash_cache) +} diff --git a/crates/goat/src/transactions/take1.rs b/crates/goat/src/transactions/take1.rs new file mode 100644 index 00000000..c1a96720 --- /dev/null +++ b/crates/goat/src/transactions/take1.rs @@ -0,0 +1,340 @@ +use bitcoin::{ + absolute, consensus, Address, Amount, ScriptBuf, TapSighashType, Transaction, TxOut, +}; +use musig2::{errors::SigningError, AggNonce, PartialSignature, SecNonce}; +use serde::{Deserialize, Serialize}; + +use crate::{ + connectors::{connector_b::ConnectorB, connector_c::ConnectorC}, + contexts::base::BaseContext, + error::{Error, TransactionError::InsufficientInputAmount}, + transactions::signing_musig2::{ + generate_taproot_aggregated_signature, generate_taproot_partial_signature, + }, +}; + +use super::{ + super::{ + connectors::{ + base::*, connector_0::Connector0, connector_a::ConnectorA, + kickoff_connectors::GuardianConnector, + }, + contexts::{committee::CommitteeContext, operator::OperatorContext}, + scripts::*, + }, + base::*, + pre_signed::*, + signing::*, +}; + +#[derive(Serialize, Deserialize, Eq, PartialEq, Clone)] +pub struct Take1Transaction { + #[serde(with = "consensus::serde::With::")] + tx: Transaction, + #[serde(with = "consensus::serde::With::")] + prev_outs: Vec, + prev_scripts: Vec, +} +impl PreSignedTransaction for Take1Transaction { + fn tx(&self) -> &Transaction { + &self.tx + } + + fn tx_mut(&mut self) -> &mut Transaction { + &mut self.tx + } + + fn prev_outs(&self) -> &Vec { + &self.prev_outs + } + + fn prev_scripts(&self) -> &Vec { + &self.prev_scripts + } +} +impl Take1Transaction { + #[allow(clippy::too_many_arguments)] + pub fn new_for_validation( + connector_0: &Connector0, + connector_a: &ConnectorA, + connector_b: &ConnectorB, + connector_c: &ConnectorC, + guardian_connector: &GuardianConnector, + input_0: Input, + input_1: Input, + input_2: Input, + input_3: Input, + input_4: Input, + operator_address: &Address, + ) -> Result { + let input_0_leaf = 0; + let _input_0 = connector_0.generate_taproot_leaf_tx_in(input_0_leaf, &input_0); + + let input_1_leaf = 0; + let _input_1 = connector_a.generate_taproot_leaf_tx_in(input_1_leaf, &input_1); + + let input_2_leaf = 0; + let _input_2 = connector_b.generate_taproot_leaf_tx_in(input_2_leaf, &input_2); + + let input_3_leaf = 0; + let _input_3 = connector_c.generate_taproot_leaf_tx_in(input_3_leaf, &input_3); + + let input_4_leaf = 0; + let _input_4 = guardian_connector.generate_taproot_leaf_tx_in(input_4_leaf, &input_4); + + let total_input_amount = + input_0.amount + input_1.amount + input_2.amount + input_3.amount + input_4.amount; + + if total_input_amount < Amount::from_sat(MIN_RELAY_FEE_TAKE_1 + DUST_AMOUNT + P2A_AMOUNT) { + return Err(Error::Transaction(InsufficientInputAmount)); + } + + let total_output_amount = total_input_amount - Amount::from_sat(MIN_RELAY_FEE_TAKE_1); + + let output_1 = p2a_output(); + + let output_0 = TxOut { + value: total_output_amount - output_1.value, + script_pubkey: operator_address.script_pubkey(), + }; + + Ok(Take1Transaction { + tx: Transaction { + version: bitcoin::transaction::Version(2), + lock_time: absolute::LockTime::ZERO, + input: vec![_input_0, _input_1, _input_2, _input_3, _input_4], + output: vec![output_0, output_1], + }, + prev_outs: vec![ + TxOut { + value: input_0.amount, + script_pubkey: connector_0.generate_taproot_address().script_pubkey(), + }, + TxOut { + value: input_1.amount, + script_pubkey: connector_a.generate_taproot_address().script_pubkey(), + }, + TxOut { + value: input_2.amount, + script_pubkey: connector_b.generate_taproot_address().script_pubkey(), + }, + TxOut { + value: input_3.amount, + script_pubkey: connector_c.generate_taproot_address().script_pubkey(), + }, + TxOut { + value: input_4.amount, + script_pubkey: guardian_connector.generate_taproot_address().script_pubkey(), + }, + ], + prev_scripts: vec![ + connector_0.generate_taproot_leaf_script(input_0_leaf), + connector_a.generate_taproot_leaf_script(input_1_leaf), + connector_b.generate_taproot_leaf_script(input_2_leaf), + connector_c.generate_taproot_leaf_script(input_3_leaf), + guardian_connector.generate_taproot_leaf_script(input_4_leaf), + ], + }) + } + + fn sign_input_0_musig2( + &mut self, + context: &CommitteeContext, + sec_nonce: &SecNonce, + agg_nonce: &AggNonce, + ) -> Result { + let input_index = 0; + let sighash_type = TapSighashType::All; + generate_taproot_partial_signature( + context, + self.tx(), + sec_nonce, + agg_nonce, + input_index, + self.prev_outs(), + &self.prev_scripts()[input_index], + sighash_type, + ) + } + + fn push_input_0_signature( + &mut self, + connector_0: &Connector0, + input_0_sig: bitcoin::taproot::Signature, + ) { + let input_index = 0; + let script = self.prev_scripts()[input_index].clone(); + let spend_info = connector_0.generate_taproot_spend_info(); + let tx_mut = self.tx_mut(); + // Push signature to witness + tx_mut.input[input_index].witness.push(input_0_sig.serialize()); + + // Push script + control block + push_taproot_leaf_script_and_control_block_to_witness( + tx_mut, + input_index, + &spend_info, + &script, + ); + } + + fn sign_input_3_musig2( + &mut self, + context: &CommitteeContext, + sec_nonce: &SecNonce, + agg_nonce: &AggNonce, + ) -> Result { + let input_index = 3; + let sighash_type = TapSighashType::All; + generate_taproot_partial_signature( + context, + self.tx(), + sec_nonce, + agg_nonce, + input_index, + self.prev_outs(), + &self.prev_scripts()[input_index], + sighash_type, + ) + } + + fn push_input_3_signature( + &mut self, + connector_c: &ConnectorC, + input_3_sig: bitcoin::taproot::Signature, + ) { + let input_index = 3; + let script = self.prev_scripts()[input_index].clone(); + let spend_info = connector_c.generate_taproot_spend_info(); + let tx_mut = self.tx_mut(); + // Push signature to witness + tx_mut.input[input_index].witness.push(input_3_sig.serialize()); + + // Push script + control block + push_taproot_leaf_script_and_control_block_to_witness( + tx_mut, + input_index, + &spend_info, + &script, + ); + } + + pub fn pre_sign( + &mut self, + context: &CommitteeContext, + sec_nonces: &[SecNonce; 2], + agg_nonces: &[AggNonce; 2], + ) -> Result<[PartialSignature; 2], SigningError> { + let input_0_sig = self.sign_input_0_musig2(context, &sec_nonces[0], &agg_nonces[0]); + let input_3_sig = self.sign_input_3_musig2(context, &sec_nonces[1], &agg_nonces[1]); + match [input_0_sig, input_3_sig] + .into_iter() + .collect::, SigningError>>() + { + Ok(sigs) => Ok(sigs.try_into().unwrap()), + Err(e) => Err(e), + } + } + + pub fn aggregate_pre_sigs( + &self, + context: &dyn BaseContext, + partial_signatures: &[Vec; 2], + agg_nonces: &[AggNonce; 2], + ) -> Result<[bitcoin::taproot::Signature; 2], Error> { + let (input_0_sig, input_3_sig); + { + let input_index = 0; + let sig_index = 0; + let sighash_type = TapSighashType::All; + input_0_sig = match generate_taproot_aggregated_signature( + context, + self.tx(), + &agg_nonces[sig_index], + input_index, + self.prev_outs(), + &self.prev_scripts()[input_index], + sighash_type, + partial_signatures[sig_index].clone(), + ) { + Ok(sig) => bitcoin::taproot::Signature { signature: sig.into(), sighash_type }, + Err(_) => return Err(Error::Other("Failed to aggregate signatures")), + }; + } + { + let input_index = 3; + let sig_index = 1; + let sighash_type = TapSighashType::All; + input_3_sig = match generate_taproot_aggregated_signature( + context, + self.tx(), + &agg_nonces[sig_index], + input_index, + self.prev_outs(), + &self.prev_scripts()[input_index], + sighash_type, + partial_signatures[sig_index].clone(), + ) { + Ok(sig) => bitcoin::taproot::Signature { signature: sig.into(), sighash_type }, + Err(_) => return Err(Error::Other("Failed to aggregate signatures")), + }; + } + Ok([input_0_sig, input_3_sig]) + } + + pub fn push_pre_sigs( + &mut self, + connector_0: &Connector0, + connector_c: &ConnectorC, + pre_sigs: [bitcoin::taproot::Signature; 2], + ) { + self.push_input_0_signature(connector_0, pre_sigs[0]); + self.push_input_3_signature(connector_c, pre_sigs[1]); + } + + pub fn sign_input_1(&mut self, context: &OperatorContext, connector_a: &ConnectorA) { + let input_index = 1; + pre_sign_taproot_input_default( + self, + input_index, + TapSighashType::All, + connector_a.generate_taproot_spend_info(), + &vec![&context.operator_keypair], + ); + } + + pub fn sign_input_2(&mut self, context: &OperatorContext, connector_b: &ConnectorB) { + let input_index = 2; + pre_sign_taproot_input_default( + self, + input_index, + TapSighashType::All, + connector_b.generate_taproot_spend_info(), + &vec![&context.operator_keypair], + ); + } + + pub fn sign_input_4( + &mut self, + context: &OperatorContext, + guardian_connector: &GuardianConnector, + ) { + let input_index = 4; + pre_sign_taproot_input_default( + self, + input_index, + TapSighashType::All, + guardian_connector.generate_taproot_spend_info(), + &vec![&context.operator_keypair], + ); + } +} + +impl BaseTransaction for Take1Transaction { + fn finalize(&self) -> Transaction { + self.tx.clone() + } + fn name(&self) -> &'static str { + "Take1" + } +} diff --git a/crates/goat/src/transactions/take2.rs b/crates/goat/src/transactions/take2.rs new file mode 100644 index 00000000..4d6e3f08 --- /dev/null +++ b/crates/goat/src/transactions/take2.rs @@ -0,0 +1,261 @@ +use bitcoin::{ + absolute, consensus, Address, Amount, ScriptBuf, TapSighashType, Transaction, TxOut, +}; +use musig2::{errors::SigningError, AggNonce, PartialSignature, SecNonce}; +use serde::{Deserialize, Serialize}; + +use crate::{ + contexts::base::BaseContext, + transactions::signing_musig2::generate_taproot_aggregated_signature, +}; + +use super::{ + super::{ + connectors::{ + base::*, connector_0::Connector0, connector_d::ConnectorD, connector_f::ConnectorF, + kickoff_connectors::GuardianConnector, + }, + contexts::{committee::CommitteeContext, operator::OperatorContext}, + error::{Error, TransactionError::InsufficientInputAmount}, + scripts::*, + transactions::signing_musig2::generate_taproot_partial_signature, + }, + base::*, + pre_signed::*, + signing::*, +}; + +#[derive(Serialize, Deserialize, Eq, PartialEq, Clone)] +pub struct Take2Transaction { + #[serde(with = "consensus::serde::With::")] + tx: Transaction, + #[serde(with = "consensus::serde::With::")] + prev_outs: Vec, + prev_scripts: Vec, +} + +impl PreSignedTransaction for Take2Transaction { + fn tx(&self) -> &Transaction { + &self.tx + } + + fn tx_mut(&mut self) -> &mut Transaction { + &mut self.tx + } + + fn prev_outs(&self) -> &Vec { + &self.prev_outs + } + + fn prev_scripts(&self) -> &Vec { + &self.prev_scripts + } +} + +impl Take2Transaction { + #[allow(clippy::too_many_arguments)] + pub fn new_for_validation( + connector_0: &Connector0, + connector_d: &ConnectorD, + connector_f: &ConnectorF, + guardian_connector: &GuardianConnector, + input_0: Input, + input_1: Input, + input_2: Input, + input_3: Input, + operator_address: &Address, + ) -> Result { + let input_0_leaf = 0; + let _input_0 = connector_0.generate_taproot_leaf_tx_in(input_0_leaf, &input_0); + + let input_1_leaf = 0; + let _input_1 = connector_d.generate_taproot_leaf_tx_in(input_1_leaf, &input_1); + + let input_2_leaf = 0; + let _input_2 = connector_f.generate_taproot_leaf_tx_in(input_2_leaf, &input_2); + + let input_3_leaf = 0; + let _input_3 = guardian_connector.generate_taproot_leaf_tx_in(input_3_leaf, &input_3); + + let total_input_amount = input_0.amount + input_1.amount + input_2.amount + input_3.amount; + + if total_input_amount < Amount::from_sat(MIN_RELAY_FEE_TAKE_2 + DUST_AMOUNT + P2A_AMOUNT) { + return Err(Error::Transaction(InsufficientInputAmount)); + } + + let total_output_amount = total_input_amount - Amount::from_sat(MIN_RELAY_FEE_TAKE_2); + + let output_1 = p2a_output(); + + let output_0 = TxOut { + value: total_output_amount - output_1.value, + script_pubkey: operator_address.script_pubkey(), + }; + + Ok(Take2Transaction { + tx: Transaction { + version: bitcoin::transaction::Version(2), + lock_time: absolute::LockTime::ZERO, + input: vec![_input_0, _input_1, _input_2, _input_3], + output: vec![output_0, output_1], + }, + prev_outs: vec![ + TxOut { + value: input_0.amount, + script_pubkey: connector_0.generate_taproot_address().script_pubkey(), + }, + TxOut { + value: input_1.amount, + script_pubkey: connector_d.generate_taproot_address().script_pubkey(), + }, + TxOut { + value: input_2.amount, + script_pubkey: connector_f.generate_taproot_address().script_pubkey(), + }, + TxOut { + value: input_3.amount, + script_pubkey: guardian_connector.generate_taproot_address().script_pubkey(), + }, + ], + prev_scripts: vec![ + connector_0.generate_taproot_leaf_script(input_0_leaf), + connector_d.generate_taproot_leaf_script(input_1_leaf), + connector_f.generate_taproot_leaf_script(input_2_leaf), + guardian_connector.generate_taproot_leaf_script(input_3_leaf), + ], + }) + } + + fn sign_input_0_musig2( + &mut self, + context: &CommitteeContext, + sec_nonce: &SecNonce, + agg_nonce: &AggNonce, + ) -> Result { + let input_index = 0; + let sighash_type = TapSighashType::All; + generate_taproot_partial_signature( + context, + self.tx(), + sec_nonce, + agg_nonce, + input_index, + self.prev_outs(), + &self.prev_scripts()[input_index], + sighash_type, + ) + } + + fn push_input_0_signature( + &mut self, + connector_0: &Connector0, + input_0_sig: bitcoin::taproot::Signature, + ) { + let input_index = 0; + let script = self.prev_scripts()[input_index].clone(); + let spend_info = connector_0.generate_taproot_spend_info(); + let tx_mut = self.tx_mut(); + // Push signature to witness + tx_mut.input[input_index].witness.push(input_0_sig.serialize()); + + // Push script + control block + push_taproot_leaf_script_and_control_block_to_witness( + tx_mut, + input_index, + &spend_info, + &script, + ); + } + + pub fn pre_sign( + &mut self, + context: &CommitteeContext, + sec_nonces: &[SecNonce; 1], + agg_nonces: &[AggNonce; 1], + ) -> Result<[PartialSignature; 1], SigningError> { + let input_0_sig = self.sign_input_0_musig2(context, &sec_nonces[0], &agg_nonces[0]); + match [input_0_sig].into_iter().collect::, SigningError>>() { + Ok(sigs) => Ok(sigs.try_into().unwrap()), + Err(e) => Err(e), + } + } + + pub fn aggregate_pre_sigs( + &self, + context: &dyn BaseContext, + partial_signatures: &[Vec; 1], + agg_nonces: &[AggNonce; 1], + ) -> Result<[bitcoin::taproot::Signature; 1], Error> { + let input_index = 0; + let sig_index = 0; + let sighash_type = TapSighashType::All; + let input_0_sig = match generate_taproot_aggregated_signature( + context, + self.tx(), + &agg_nonces[sig_index], + input_index, + self.prev_outs(), + &self.prev_scripts()[input_index], + sighash_type, + partial_signatures[sig_index].clone(), + ) { + Ok(sig) => bitcoin::taproot::Signature { signature: sig.into(), sighash_type }, + Err(_) => return Err(Error::Other("Failed to aggregate signatures")), + }; + Ok([input_0_sig]) + } + + pub fn push_pre_sigs( + &mut self, + connector_0: &Connector0, + pre_sigs: [bitcoin::taproot::Signature; 1], + ) { + self.push_input_0_signature(connector_0, pre_sigs[0]); + } + + pub fn sign_input_1(&mut self, context: &OperatorContext, connector_d: &ConnectorD) { + let input_index = 1; + pre_sign_taproot_input_default( + self, + input_index, + TapSighashType::All, + connector_d.generate_taproot_spend_info(), + &vec![&context.operator_keypair], + ); + } + + pub fn sign_input_2(&mut self, context: &OperatorContext, connector_f: &ConnectorF) { + let input_index = 2; + pre_sign_taproot_input_default( + self, + input_index, + TapSighashType::All, + connector_f.generate_taproot_spend_info(), + &vec![&context.operator_keypair], + ); + } + + pub fn sign_input_3( + &mut self, + context: &OperatorContext, + guardian_connector: &GuardianConnector, + ) { + let input_index = 3; + pre_sign_taproot_input_default( + self, + input_index, + TapSighashType::All, + guardian_connector.generate_taproot_spend_info(), + &vec![&context.operator_keypair], + ); + } +} + +impl BaseTransaction for Take2Transaction { + fn finalize(&self) -> Transaction { + self.tx.clone() + } + fn name(&self) -> &'static str { + "Take2" + } +} diff --git a/crates/goat/src/transactions/watchtower_challenge.rs b/crates/goat/src/transactions/watchtower_challenge.rs new file mode 100644 index 00000000..08bfdbf0 --- /dev/null +++ b/crates/goat/src/transactions/watchtower_challenge.rs @@ -0,0 +1,820 @@ +use bitcoin::{ + absolute, consensus, key::Keypair, Address, Amount, ScriptBuf, TapSighashType, Transaction, + TxIn, TxOut, +}; +use musig2::{errors::SigningError, AggNonce, PartialSignature, SecNonce}; +use serde::{Deserialize, Serialize}; + +use crate::{ + assert_scripts::OperatorCommitPubinSecretKey, + connectors::{ + base::{generate_default_tx_in, TaprootConnector}, + connector_b::ConnectorB, + connector_e::ConnectorE, + connector_f::ConnectorF, + watchtower_connectors::{AckConnector, WatchtowerChallengeConnector}, + }, + contexts::{base::BaseContext, committee::CommitteeContext, operator::OperatorContext}, + error::{Error, TransactionError::InsufficientInputAmount}, + scripts::{generate_data_commitment_outputs, p2a_output}, + transactions::{ + signing::{ + populate_taproot_input_witness_default, populate_taproot_txin_witness, + push_taproot_leaf_script_and_control_block_to_witness, + }, + signing_musig2::{ + generate_taproot_aggregated_signature, generate_taproot_partial_signature, + }, + }, +}; + +use super::{base::*, pre_signed::*}; + +pub fn operator_commit_pubin( + connector_e: &ConnectorE, + pubin_commitment: &[u8; 96], + wots_sk: &OperatorCommitPubinSecretKey, + input_0: Input, +) -> Result { + let input_0_leaf = 0; + let mut _input_0 = connector_e.generate_taproot_leaf_tx_in(input_0_leaf, &input_0); + let unlock_data = connector_e.generate_leaf_0_unlock_data(wots_sk, pubin_commitment)?; + populate_taproot_txin_witness( + &mut _input_0, + &connector_e.generate_taproot_spend_info(), + &connector_e.generate_taproot_leaf_script(input_0_leaf), + unlock_data, + ); + Ok(_input_0) +} + +pub fn operator_challenge_ack( + ack_connector: &AckConnector, + preimage: &[u8], + input_0: Input, +) -> Result { + let input_0_leaf = 1; + let mut _input_0 = ack_connector.generate_taproot_leaf_tx_in(input_0_leaf, &input_0); + let unlock_data = ack_connector.generate_leaf_1_unlock_data(preimage)?; + populate_taproot_txin_witness( + &mut _input_0, + &ack_connector.generate_taproot_spend_info(), + &ack_connector.generate_taproot_leaf_script(input_0_leaf), + unlock_data, + ); + Ok(_input_0) +} + +pub fn extract_operator_preimage_from_ack_txin(ack_txin: &TxIn) -> Result, Error> { + ack_txin + .witness + .nth(0) + .map(|w| w.to_vec()) + .ok_or(Error::Other("Unable to extract preimage from ACK txin witness")) +} + +pub fn watchtower_challenge( + watchtower_keypair: &Keypair, + watchtower_challenge_connector: &WatchtowerChallengeConnector, + commitment: &[u8], + input_0: Input, + payer_inputs: Vec, + change_address: &Address, + fee_amount: Amount, +) -> Result { + let input_0_leaf = 0; + let _input_0 = + watchtower_challenge_connector.generate_taproot_leaf_tx_in(input_0_leaf, &input_0); + + let mut txins = vec![_input_0]; + let mut total_input_amount = input_0.amount; + txins.extend( + payer_inputs + .iter() + .map(|input| { + total_input_amount += input.amount; + generate_default_tx_in(input) + }) + .collect::>(), + ); + + let commitment_outputs = generate_data_commitment_outputs(commitment); + let commitment_amounts = commitment_outputs.iter().map(|out| out.value).sum::(); + + if total_input_amount < fee_amount + commitment_amounts { + return Err(Error::Transaction(InsufficientInputAmount)); + } + + let total_output_amount = total_input_amount - fee_amount; + let change_amount = total_output_amount - commitment_amounts; + let mut txouts = commitment_outputs; + if change_amount >= Amount::from_sat(DUST_AMOUNT) { + txouts.push(TxOut { value: change_amount, script_pubkey: change_address.script_pubkey() }); + }; + + let mut tx = Transaction { + version: bitcoin::transaction::Version(2), + lock_time: absolute::LockTime::ZERO, + input: txins, + output: txouts, + }; + + let input_index = 0; + let prev_outs = vec![TxOut { + value: input_0.amount, + script_pubkey: watchtower_challenge_connector.generate_taproot_address().script_pubkey(), + }]; + let script = watchtower_challenge_connector.generate_taproot_leaf_script(input_0_leaf); + populate_taproot_input_witness_default( + &mut tx, + &prev_outs, + input_index, + TapSighashType::AllPlusAnyoneCanPay, + &watchtower_challenge_connector.generate_taproot_spend_info(), + &script, + &vec![watchtower_keypair], + ); + + Ok(tx) +} + +#[derive(Serialize, Deserialize, Eq, PartialEq, Clone)] +pub struct WatchtowerChallengeInitTransaction { + #[serde(with = "consensus::serde::With::")] + tx: Transaction, + #[serde(with = "consensus::serde::With::")] + prev_outs: Vec, + prev_scripts: Vec, +} + +impl PreSignedTransaction for WatchtowerChallengeInitTransaction { + fn tx(&self) -> &Transaction { + &self.tx + } + + fn tx_mut(&mut self) -> &mut Transaction { + &mut self.tx + } + + fn prev_outs(&self) -> &Vec { + &self.prev_outs + } + + fn prev_scripts(&self) -> &Vec { + &self.prev_scripts + } +} + +impl WatchtowerChallengeInitTransaction { + pub fn new_for_validation( + connector_b: &ConnectorB, + connector_e: &ConnectorE, + connector_f: &ConnectorF, + watchtower_challenge_connectors: &[WatchtowerChallengeConnector], + ack_connectors: &[AckConnector], + input_0: Input, + ) -> Result { + if watchtower_challenge_connectors.len() != ack_connectors.len() { + return Err(Error::Other("watchtower challenge and ACK connector counts differ")); + } + + let input_0_leaf = 0; + let _input_0 = connector_b.generate_taproot_leaf_tx_in(input_0_leaf, &input_0); + + if input_0.amount + < Amount::from_sat(max_watchtower_challenge_cost(watchtower_challenge_connectors.len())) + { + return Err(Error::Transaction(InsufficientInputAmount)); + } + + let mut total_output_amount = input_0.amount + - Amount::from_sat(min_relay_fee_watchtower_challenge_init( + watchtower_challenge_connectors.len(), + )); + let mut txouts = vec![]; + for (watchtower_challenge_connector, ack_connector) in + watchtower_challenge_connectors.iter().zip(ack_connectors.iter()) + { + let challenge_output = TxOut { + value: Amount::from_sat(watchtower_challenge_connector_output_amount()), + script_pubkey: watchtower_challenge_connector + .generate_taproot_address() + .script_pubkey(), + }; + let ack_output = TxOut { + value: Amount::from_sat(ack_connector_output_amount()), + script_pubkey: ack_connector.generate_taproot_address().script_pubkey(), + }; + total_output_amount = total_output_amount - challenge_output.value - ack_output.value; + txouts.push(challenge_output); + txouts.push(ack_output); + } + + let connector_e_output = TxOut { + value: Amount::from_sat(connector_e_output_amount()), + script_pubkey: connector_e.generate_taproot_address().script_pubkey(), + }; + let anchor_output = p2a_output(); + total_output_amount = total_output_amount - connector_e_output.value - anchor_output.value; + let connector_f_output = TxOut { + value: total_output_amount, + script_pubkey: connector_f.generate_taproot_address().script_pubkey(), + }; + + txouts.push(connector_e_output); + txouts.push(connector_f_output); + txouts.push(anchor_output); + + Ok(WatchtowerChallengeInitTransaction { + tx: Transaction { + version: bitcoin::transaction::Version(2), + lock_time: absolute::LockTime::ZERO, + input: vec![_input_0], + output: txouts, + }, + prev_outs: vec![TxOut { + value: input_0.amount, + script_pubkey: connector_b.generate_taproot_address().script_pubkey(), + }], + prev_scripts: vec![connector_b.generate_taproot_leaf_script(input_0_leaf)], + }) + } + + pub fn sign_input_0(&mut self, context: &OperatorContext, connector_b: &ConnectorB) { + let input_index = 0; + pre_sign_taproot_input_default( + self, + input_index, + TapSighashType::All, + connector_b.generate_taproot_spend_info(), + &vec![&context.operator_keypair], + ); + } + + pub fn watchtower_connector_input(&self, index: usize) -> Result { + let watchtower_num = + output_topology::watchtower_challenge_init::watchtower_num(self.tx.output.len()); + if index >= watchtower_num { + return Err(Error::Other("watchtower connector index out of bounds")); + } + tx_output_input( + &self.tx, + output_topology::watchtower_challenge_init::watchtower_connector(index), + ) + } + + pub fn ack_connector_input(&self, index: usize) -> Result { + let watchtower_num = + output_topology::watchtower_challenge_init::watchtower_num(self.tx.output.len()); + if index >= watchtower_num { + return Err(Error::Other("ACK connector index out of bounds")); + } + tx_output_input(&self.tx, output_topology::watchtower_challenge_init::ack_connector(index)) + } + + pub fn connector_e_input(&self) -> Result { + let watchtower_num = + output_topology::watchtower_challenge_init::watchtower_num(self.tx.output.len()); + tx_output_input( + &self.tx, + output_topology::watchtower_challenge_init::connector_e(watchtower_num), + ) + } + + pub fn connector_f_input(&self) -> Result { + let watchtower_num = + output_topology::watchtower_challenge_init::watchtower_num(self.tx.output.len()); + tx_output_input( + &self.tx, + output_topology::watchtower_challenge_init::connector_f(watchtower_num), + ) + } + + pub fn anchor_input(&self) -> Result { + let watchtower_num = + output_topology::watchtower_challenge_init::watchtower_num(self.tx.output.len()); + tx_output_input( + &self.tx, + output_topology::watchtower_challenge_init::anchor(watchtower_num), + ) + } +} + +impl BaseTransaction for WatchtowerChallengeInitTransaction { + fn finalize(&self) -> Transaction { + self.tx.clone() + } + + fn name(&self) -> &'static str { + "WatchtowerChallengeInit" + } +} + +#[derive(Serialize, Deserialize, Eq, PartialEq, Clone)] +pub struct WatchtowerChallengeTimeoutTransaction { + #[serde(with = "consensus::serde::With::")] + tx: Transaction, + #[serde(with = "consensus::serde::With::")] + prev_outs: Vec, + prev_scripts: Vec, +} + +impl PreSignedTransaction for WatchtowerChallengeTimeoutTransaction { + fn tx(&self) -> &Transaction { + &self.tx + } + + fn tx_mut(&mut self) -> &mut Transaction { + &mut self.tx + } + + fn prev_outs(&self) -> &Vec { + &self.prev_outs + } + + fn prev_scripts(&self) -> &Vec { + &self.prev_scripts + } +} + +impl WatchtowerChallengeTimeoutTransaction { + pub fn new_for_validation( + watchtower_challenge_connector: &WatchtowerChallengeConnector, + ack_connector: &AckConnector, + input_0: Input, + input_1: Input, + ) -> Self { + let input_0_leaf = 1; + let _input_0 = + watchtower_challenge_connector.generate_taproot_leaf_tx_in(input_0_leaf, &input_0); + + let input_1_leaf = 0; + let _input_1 = ack_connector.generate_taproot_leaf_tx_in(input_1_leaf, &input_1); + + WatchtowerChallengeTimeoutTransaction { + tx: Transaction { + version: bitcoin::transaction::Version(2), + lock_time: absolute::LockTime::ZERO, + input: vec![_input_0, _input_1], + output: vec![p2a_output()], + }, + prev_outs: vec![ + TxOut { + value: input_0.amount, + script_pubkey: watchtower_challenge_connector + .generate_taproot_address() + .script_pubkey(), + }, + TxOut { + value: input_1.amount, + script_pubkey: ack_connector.generate_taproot_address().script_pubkey(), + }, + ], + prev_scripts: vec![ + watchtower_challenge_connector.generate_taproot_leaf_script(input_0_leaf), + ack_connector.generate_taproot_leaf_script(input_1_leaf), + ], + } + } + + pub fn sign_input_0( + &mut self, + context: &OperatorContext, + watchtower_challenge_connector: &WatchtowerChallengeConnector, + ) { + let input_index = 0; + pre_sign_taproot_input_default( + self, + input_index, + TapSighashType::All, + watchtower_challenge_connector.generate_taproot_spend_info(), + &vec![&context.operator_keypair], + ); + } + + fn sign_input_1_musig2( + &mut self, + context: &CommitteeContext, + sec_nonce: &SecNonce, + agg_nonce: &AggNonce, + ) -> Result { + let input_index = 1; + let sighash_type = TapSighashType::None; + generate_taproot_partial_signature( + context, + self.tx(), + sec_nonce, + agg_nonce, + input_index, + self.prev_outs(), + &self.prev_scripts()[input_index], + sighash_type, + ) + } + + fn push_input_1_signature( + &mut self, + ack_connector: &AckConnector, + input_1_sig: bitcoin::taproot::Signature, + ) { + let input_index = 1; + let script = self.prev_scripts()[input_index].clone(); + let spend_info = ack_connector.generate_taproot_spend_info(); + let tx_mut = self.tx_mut(); + tx_mut.input[input_index].witness.push(input_1_sig.serialize()); + push_taproot_leaf_script_and_control_block_to_witness( + tx_mut, + input_index, + &spend_info, + &script, + ); + } + + pub fn pre_sign( + &mut self, + context: &CommitteeContext, + sec_nonces: &[SecNonce; 1], + agg_nonces: &[AggNonce; 1], + ) -> Result<[PartialSignature; 1], SigningError> { + let input_1_sig = self.sign_input_1_musig2(context, &sec_nonces[0], &agg_nonces[0]); + match [input_1_sig].into_iter().collect::, SigningError>>() { + Ok(sigs) => Ok(sigs.try_into().unwrap()), + Err(e) => Err(e), + } + } + + pub fn aggregate_pre_sigs( + &self, + context: &dyn BaseContext, + partial_signatures: &[Vec; 1], + agg_nonces: &[AggNonce; 1], + ) -> Result<[bitcoin::taproot::Signature; 1], Error> { + let input_index = 1; + let sig_index = 0; + let sighash_type = TapSighashType::None; + let input_1_sig = match generate_taproot_aggregated_signature( + context, + self.tx(), + &agg_nonces[sig_index], + input_index, + self.prev_outs(), + &self.prev_scripts()[input_index], + sighash_type, + partial_signatures[sig_index].clone(), + ) { + Ok(sig) => bitcoin::taproot::Signature { signature: sig.into(), sighash_type }, + Err(_) => return Err(Error::Other("Failed to aggregate signatures")), + }; + Ok([input_1_sig]) + } + + pub fn push_pre_sigs( + &mut self, + ack_connector: &AckConnector, + pre_sigs: [bitcoin::taproot::Signature; 1], + ) { + self.push_input_1_signature(ack_connector, pre_sigs[0]); + } +} + +impl BaseTransaction for WatchtowerChallengeTimeoutTransaction { + fn finalize(&self) -> Transaction { + self.tx.clone() + } + + fn name(&self) -> &'static str { + "WatchtowerChallengeTimeout" + } +} + +#[derive(Serialize, Deserialize, Eq, PartialEq, Clone)] +pub struct OperatorChallengeNackTransaction { + #[serde(with = "consensus::serde::With::")] + tx: Transaction, + #[serde(with = "consensus::serde::With::")] + prev_outs: Vec, + prev_scripts: Vec, +} + +impl PreSignedTransaction for OperatorChallengeNackTransaction { + fn tx(&self) -> &Transaction { + &self.tx + } + + fn tx_mut(&mut self) -> &mut Transaction { + &mut self.tx + } + + fn prev_outs(&self) -> &Vec { + &self.prev_outs + } + + fn prev_scripts(&self) -> &Vec { + &self.prev_scripts + } +} + +impl OperatorChallengeNackTransaction { + pub fn new_for_validation( + ack_connector: &AckConnector, + connector_f: &ConnectorF, + input_0: Input, + input_1: Input, + ) -> Self { + let input_0_leaf = 2; + let _input_0 = ack_connector.generate_taproot_leaf_tx_in(input_0_leaf, &input_0); + + let input_1_leaf = 1; + let _input_1 = connector_f.generate_taproot_leaf_tx_in(input_1_leaf, &input_1); + + OperatorChallengeNackTransaction { + tx: Transaction { + version: bitcoin::transaction::Version(2), + lock_time: absolute::LockTime::ZERO, + input: vec![_input_0, _input_1], + output: vec![p2a_output()], + }, + prev_outs: vec![ + TxOut { + value: input_0.amount, + script_pubkey: ack_connector.generate_taproot_address().script_pubkey(), + }, + TxOut { + value: input_1.amount, + script_pubkey: connector_f.generate_taproot_address().script_pubkey(), + }, + ], + prev_scripts: vec![ + ack_connector.generate_taproot_leaf_script(input_0_leaf), + connector_f.generate_taproot_leaf_script(input_1_leaf), + ], + } + } + + fn sign_input_musig2( + &mut self, + context: &CommitteeContext, + sec_nonce: &SecNonce, + agg_nonce: &AggNonce, + input_index: usize, + ) -> Result { + let sighash_type = TapSighashType::All; + generate_taproot_partial_signature( + context, + self.tx(), + sec_nonce, + agg_nonce, + input_index, + self.prev_outs(), + &self.prev_scripts()[input_index], + sighash_type, + ) + } + + fn push_input_signature( + &mut self, + input_index: usize, + spend_info: bitcoin::taproot::TaprootSpendInfo, + input_sig: bitcoin::taproot::Signature, + ) { + let script = self.prev_scripts()[input_index].clone(); + let tx_mut = self.tx_mut(); + tx_mut.input[input_index].witness.push(input_sig.serialize()); + push_taproot_leaf_script_and_control_block_to_witness( + tx_mut, + input_index, + &spend_info, + &script, + ); + } + + pub fn pre_sign( + &mut self, + context: &CommitteeContext, + sec_nonces: &[SecNonce; 2], + agg_nonces: &[AggNonce; 2], + ) -> Result<[PartialSignature; 2], SigningError> { + let input_0_sig = self.sign_input_musig2(context, &sec_nonces[0], &agg_nonces[0], 0); + let input_1_sig = self.sign_input_musig2(context, &sec_nonces[1], &agg_nonces[1], 1); + match [input_0_sig, input_1_sig] + .into_iter() + .collect::, SigningError>>() + { + Ok(sigs) => Ok(sigs.try_into().unwrap()), + Err(e) => Err(e), + } + } + + pub fn aggregate_pre_sigs( + &self, + context: &dyn BaseContext, + partial_signatures: &[Vec; 2], + agg_nonces: &[AggNonce; 2], + ) -> Result<[bitcoin::taproot::Signature; 2], Error> { + let mut pre_sigs = Vec::with_capacity(2); + for input_index in 0..2 { + let sighash_type = TapSighashType::All; + let sig = match generate_taproot_aggregated_signature( + context, + self.tx(), + &agg_nonces[input_index], + input_index, + self.prev_outs(), + &self.prev_scripts()[input_index], + sighash_type, + partial_signatures[input_index].clone(), + ) { + Ok(sig) => bitcoin::taproot::Signature { signature: sig.into(), sighash_type }, + Err(_) => return Err(Error::Other("Failed to aggregate signatures")), + }; + pre_sigs.push(sig); + } + Ok(pre_sigs.try_into().unwrap()) + } + + pub fn push_pre_sigs( + &mut self, + ack_connector: &AckConnector, + connector_f: &ConnectorF, + pre_sigs: [bitcoin::taproot::Signature; 2], + ) { + self.push_input_signature(0, ack_connector.generate_taproot_spend_info(), pre_sigs[0]); + self.push_input_signature(1, connector_f.generate_taproot_spend_info(), pre_sigs[1]); + } +} + +impl BaseTransaction for OperatorChallengeNackTransaction { + fn finalize(&self) -> Transaction { + self.tx.clone() + } + + fn name(&self) -> &'static str { + "OperatorChallengeNack" + } +} + +#[derive(Serialize, Deserialize, Eq, PartialEq, Clone)] +pub struct OperatorCommitTimeoutTransaction { + #[serde(with = "consensus::serde::With::")] + tx: Transaction, + #[serde(with = "consensus::serde::With::")] + prev_outs: Vec, + prev_scripts: Vec, +} + +impl PreSignedTransaction for OperatorCommitTimeoutTransaction { + fn tx(&self) -> &Transaction { + &self.tx + } + + fn tx_mut(&mut self) -> &mut Transaction { + &mut self.tx + } + + fn prev_outs(&self) -> &Vec { + &self.prev_outs + } + + fn prev_scripts(&self) -> &Vec { + &self.prev_scripts + } +} + +impl OperatorCommitTimeoutTransaction { + pub fn new_for_validation( + connector_e: &ConnectorE, + connector_f: &ConnectorF, + input_0: Input, + input_1: Input, + ) -> Self { + let input_0_leaf = 1; + let _input_0 = connector_e.generate_taproot_leaf_tx_in(input_0_leaf, &input_0); + + let input_1_leaf = 1; + let _input_1 = connector_f.generate_taproot_leaf_tx_in(input_1_leaf, &input_1); + + OperatorCommitTimeoutTransaction { + tx: Transaction { + version: bitcoin::transaction::Version(2), + lock_time: absolute::LockTime::ZERO, + input: vec![_input_0, _input_1], + output: vec![p2a_output()], + }, + prev_outs: vec![ + TxOut { + value: input_0.amount, + script_pubkey: connector_e.generate_taproot_address().script_pubkey(), + }, + TxOut { + value: input_1.amount, + script_pubkey: connector_f.generate_taproot_address().script_pubkey(), + }, + ], + prev_scripts: vec![ + connector_e.generate_taproot_leaf_script(input_0_leaf), + connector_f.generate_taproot_leaf_script(input_1_leaf), + ], + } + } + + fn sign_input_musig2( + &mut self, + context: &CommitteeContext, + sec_nonce: &SecNonce, + agg_nonce: &AggNonce, + input_index: usize, + ) -> Result { + let sighash_type = TapSighashType::All; + generate_taproot_partial_signature( + context, + self.tx(), + sec_nonce, + agg_nonce, + input_index, + self.prev_outs(), + &self.prev_scripts()[input_index], + sighash_type, + ) + } + + fn push_input_signature( + &mut self, + input_index: usize, + spend_info: bitcoin::taproot::TaprootSpendInfo, + input_sig: bitcoin::taproot::Signature, + ) { + let script = self.prev_scripts()[input_index].clone(); + let tx_mut = self.tx_mut(); + tx_mut.input[input_index].witness.push(input_sig.serialize()); + push_taproot_leaf_script_and_control_block_to_witness( + tx_mut, + input_index, + &spend_info, + &script, + ); + } + + pub fn pre_sign( + &mut self, + context: &CommitteeContext, + sec_nonces: &[SecNonce; 2], + agg_nonces: &[AggNonce; 2], + ) -> Result<[PartialSignature; 2], SigningError> { + let input_0_sig = self.sign_input_musig2(context, &sec_nonces[0], &agg_nonces[0], 0); + let input_1_sig = self.sign_input_musig2(context, &sec_nonces[1], &agg_nonces[1], 1); + match [input_0_sig, input_1_sig] + .into_iter() + .collect::, SigningError>>() + { + Ok(sigs) => Ok(sigs.try_into().unwrap()), + Err(e) => Err(e), + } + } + + pub fn aggregate_pre_sigs( + &self, + context: &dyn BaseContext, + partial_signatures: &[Vec; 2], + agg_nonces: &[AggNonce; 2], + ) -> Result<[bitcoin::taproot::Signature; 2], Error> { + let mut pre_sigs = Vec::with_capacity(2); + for input_index in 0..2 { + let sighash_type = TapSighashType::All; + let sig = match generate_taproot_aggregated_signature( + context, + self.tx(), + &agg_nonces[input_index], + input_index, + self.prev_outs(), + &self.prev_scripts()[input_index], + sighash_type, + partial_signatures[input_index].clone(), + ) { + Ok(sig) => bitcoin::taproot::Signature { signature: sig.into(), sighash_type }, + Err(_) => return Err(Error::Other("Failed to aggregate signatures")), + }; + pre_sigs.push(sig); + } + Ok(pre_sigs.try_into().unwrap()) + } + + pub fn push_pre_sigs( + &mut self, + connector_e: &ConnectorE, + connector_f: &ConnectorF, + pre_sigs: [bitcoin::taproot::Signature; 2], + ) { + self.push_input_signature(0, connector_e.generate_taproot_spend_info(), pre_sigs[0]); + self.push_input_signature(1, connector_f.generate_taproot_spend_info(), pre_sigs[1]); + } +} + +impl BaseTransaction for OperatorCommitTimeoutTransaction { + fn finalize(&self) -> Transaction { + self.tx.clone() + } + + fn name(&self) -> &'static str { + "OperatorCommitTimeout" + } +} diff --git a/crates/goat/src/utils.rs b/crates/goat/src/utils.rs new file mode 100644 index 00000000..48ec21f2 --- /dev/null +++ b/crates/goat/src/utils.rs @@ -0,0 +1,75 @@ +use std::path::{Path, PathBuf}; + +use bitcode::{Decode, Encode}; +use bitcoin::Network; + +const NUM_BLOCKS_REGTEST: u32 = 1; +const NUM_BLOCKS_TESTNET: u32 = 7; + +pub fn num_blocks_per_network(network: Network, mainnet_num_blocks: u32) -> u32 { + match network { + Network::Bitcoin => mainnet_num_blocks, + Network::Regtest => NUM_BLOCKS_REGTEST, + _ => NUM_BLOCKS_TESTNET, // Testnet, Signet + } +} + +pub fn remove_script_and_control_block_from_witness(mut witness: Vec>) -> Vec> { + witness.truncate(witness.len() - 2); + + witness +} + +pub fn write_cache(file_path: &Path, data: &impl Encode) -> std::io::Result<()> { + println!("Writing cache to {}...", file_path.display()); + if let Some(parent) = file_path.parent() { + if !parent.exists() { + std::fs::create_dir_all(parent)?; + } + } + let now = std::time::Instant::now(); + let encoded_data = bitcode::encode(data); + let compressed_data = zstd::stream::encode_all(encoded_data.as_slice(), 5)?; + let elapsed = now.elapsed(); + println!("Encoded cache to in {} ms", elapsed.as_millis()); + std::fs::write(file_path, compressed_data) +} + +pub fn read_cache(file_path: &Path) -> std::io::Result +where + T: for<'de> Decode<'de>, +{ + println!("Reading cache from {}...", file_path.display()); + let compressed_data = std::fs::read(file_path)?; + let now = std::time::Instant::now(); + let encoded_data: Vec = zstd::stream::decode_all(compressed_data.as_slice())?; + let decoded = bitcode::decode(&encoded_data).map_err(|e| { + std::io::Error::new(std::io::ErrorKind::InvalidData, format!("bitcode error: {}", e)) + })?; + let elapsed = now.elapsed(); + println!("Decoded cache in {} ms", elapsed.as_millis()); + + Ok(decoded) +} + +pub fn cleanup_cache_files(prefix: &str, cache_location: &Path, max_cache_files: u32) { + let mut paths: Vec = std::fs::read_dir(cache_location) + .unwrap() + .filter_map(Result::ok) + .filter(|entry| entry.file_name().to_str().unwrap_or("").starts_with(prefix)) + .map(|entry| entry.path()) + .collect(); + + paths.sort_by_key(|path| { + std::fs::metadata(path) + .and_then(|m| m.modified()) + .unwrap_or_else(|_| std::time::SystemTime::now()) + }); + + if paths.len() >= max_cache_files as usize { + if let Some(oldest) = paths.first() { + std::fs::remove_file(oldest).expect("Failed to delete the old cache file"); + println!("Old cache file deleted: {:?}", oldest); + } + } +} diff --git a/crates/goat/src/wots.rs b/crates/goat/src/wots.rs new file mode 100644 index 00000000..2bb2d4c5 --- /dev/null +++ b/crates/goat/src/wots.rs @@ -0,0 +1,105 @@ +use bitcoin::script::read_scriptint; +use bitcoin::Witness; +use bitvm::signatures::utils::bitcoin_representation; +pub use bitvm::signatures::winternitz::{Parameters, VoidConverter}; +pub use bitvm::signatures::{ + CompactWots, GenericWinternitzPublicKey, WinternitzSecret, WinternitzSigningInputs, Wots, + Wots16, Wots32, Wots4, Wots64, Wots80, +}; + +pub const WOTS96_LOG2_BASE: u32 = 8; +pub const WOTS96_BASE: u32 = 1 << WOTS96_LOG2_BASE; + +pub struct Wots96; + +impl Wots for Wots96 { + type Converter = VoidConverter; + type PublicKey = [[u8; 20]; Self::TOTAL_DIGIT_LEN as usize]; + type Message = [u8; Self::MSG_BYTE_LEN as usize]; + type Signature = [[u8; 21]; Self::TOTAL_DIGIT_LEN as usize]; + + const MSG_BYTE_LEN: u32 = 96; + const PARAMETERS: Parameters = + Parameters::new_by_bit_length(Self::MSG_BYTE_LEN * 8, WOTS96_LOG2_BASE); + + fn raw_witness_to_signature(witness: &Witness) -> Self::Signature { + assert_eq!(witness.len(), 2 * Self::TOTAL_DIGIT_LEN as usize); + + let mut digit_signatures = Vec::with_capacity(Self::TOTAL_DIGIT_LEN as usize); + for i in (0..witness.len()).step_by(2) { + assert_eq!(witness[i].len(), 20, "the digit signature should be constant 20 bytes"); + assert!( + witness[i + 1].len() <= 2, + "the base256 digit should fit in minimally encoded script bytes" + ); + + let digit_value = read_scriptint(&witness[i + 1]).unwrap(); + assert!( + (0..WOTS96_BASE as i64).contains(&digit_value), + "the digit should be in the valid Wots96 base range" + ); + + let mut digit_signature = [0u8; 21]; + digit_signature[..20].copy_from_slice(&witness[i]); + digit_signature[20] = digit_value as u8; + digit_signatures.push(digit_signature); + } + + Self::Signature::try_from(digit_signatures).unwrap() + } + + fn signature_to_raw_witness(signature: &Self::Signature) -> Witness { + let mut witness = Witness::new(); + + for digit_signature in signature.as_ref() { + witness.push(&digit_signature[..20]); + witness.push(bitcoin_representation(i32::from(digit_signature[20]))); + } + + witness + } + + fn signature_to_message(signature: &Self::Signature) -> Self::Message { + if WOTS96_LOG2_BASE == 8 { + let bytes = signature + .as_ref() + .iter() + .map(|digit_sig| digit_sig[20]) + .take(Self::MSG_BYTE_LEN as usize) + .rev() + .collect::>(); + + return Self::Message::try_from(bytes).unwrap(); + } + + let digits = signature + .as_ref() + .iter() + .map(|digit_sig| digit_sig[20]) + .take(((Self::MSG_BYTE_LEN * 8).div_ceil(WOTS96_LOG2_BASE)) as usize) + .rev() + .collect::>(); + + let mut bytes = Vec::with_capacity(Self::MSG_BYTE_LEN as usize); + let mut byte = 0u8; + let mut used_bits = 0u32; + for digit in digits { + byte |= digit << used_bits; + used_bits += WOTS96_LOG2_BASE; + if used_bits >= 8 { + bytes.push(byte); + byte = digit >> (8 - (used_bits - WOTS96_LOG2_BASE)); + used_bits -= 8; + } + } + bytes.truncate(Self::MSG_BYTE_LEN as usize); + + Self::Message::try_from(bytes).unwrap() + } +} + +impl CompactWots for Wots96 { + type CompactSignature = [[u8; 20]; Self::TOTAL_DIGIT_LEN as usize]; +} + +pub type Wots96Secret = WinternitzSecret;