diff --git a/Cargo.toml b/Cargo.toml index b4cbe2d..f1d3ff1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,9 +5,9 @@ members = [ "bitcoin-winternitz", "bitcoin-scriptexec", "bitcoin-testscripts", - "core", + "nero-core", "bitcoin-utils", - "nero-cli", + # "nero-cli", ] resolver = "2" diff --git a/README.md b/README.md index 81cf491..798f6b9 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ Compile `nero-cli`: ```shell cargo install --path ./nero-cli ``` - + Generate random pair of keys for payout path spending: ```shell diff --git a/bitcoin-splitter/src/split/intermediate_state.rs b/bitcoin-splitter/src/split/intermediate_state.rs index 9c01f0e..c6d053b 100644 --- a/bitcoin-splitter/src/split/intermediate_state.rs +++ b/bitcoin-splitter/src/split/intermediate_state.rs @@ -54,8 +54,8 @@ impl IntermediateState { /// Creates a new instance of the IntermediateState based on the given /// script and the stack and altstack after the execution of the script - pub fn from_inject_script(inject_script: &Script) -> Self { - let result = execute_script(inject_script.clone()); + pub fn from_inject_script(inject_script: Script) -> Self { + let result = execute_script(inject_script); Self { stack: result.main_stack.clone(), diff --git a/bitcoin-testscripts/Cargo.toml b/bitcoin-testscripts/Cargo.toml index e582413..4a5deeb 100644 --- a/bitcoin-testscripts/Cargo.toml +++ b/bitcoin-testscripts/Cargo.toml @@ -9,13 +9,14 @@ bitcoin = { workspace = true, features = ["rand-std"]} bitcoin-script = { git = "https://github.com/BitVM/rust-bitcoin-script" } # BitVM scripts -bitcoin-window-mul = { git = "https://github.com/distributed-lab/bitcoin-window-mul.git" } +bitcoin-window-mul = { workspace = true } # bitcoin-window-mul = { path = "../../../alpen/bitcoin-window-mul" } bitcoin-splitter = { path = "../bitcoin-splitter" } bitcoin-utils = { path = "../bitcoin-utils" } # General-purpose libraries -paste = "1.0.15" +paste = "1.0.15" +seq-macro = "0.3.5" # For constant for loops # Crypto libraries hex = "0.4.3" diff --git a/bitcoin-testscripts/src/friendly/bigint_mul.rs b/bitcoin-testscripts/src/friendly/bigint_mul.rs index 8ebc935..6d60c17 100644 --- a/bitcoin-testscripts/src/friendly/bigint_mul.rs +++ b/bitcoin-testscripts/src/friendly/bigint_mul.rs @@ -381,6 +381,7 @@ mod tests { bigint::{window::NonNativeWindowedBigIntImpl, U254Windowed}, traits::{comparable::Comparable, integer::NonNativeInteger}, }; + use seq_macro::seq; #[test] fn test_u254_verify() { @@ -493,6 +494,8 @@ mod tests { println!("Shard {i} length: {} bytes", shard.len()); } + println!("Complexity index is {}", split_result.complexity_index()); + // Checking the last state (which must be equal to the result of the multiplication) let last_state = split_result.must_last_state(); @@ -666,4 +669,14 @@ mod tests { assert!(result.success, "verification has failed"); } } + + /// Executes the given script and builds a table with disprove + /// script sizes for different number of bits and window sizes + #[test] + fn print_performance_table() { + let mut prng = ChaCha20Rng::seed_from_u64(0); + let num_1: BigUint = prng.sample(RandomBits::new(254)); + println!("{:?}", num_1.to_str_radix(16)); + println!("{:?}", U254::OP_PUSH_U32LESLICE(&num_1.to_u32_digits()).to_asm_string()) + } } diff --git a/bitcoin-utils/src/debug.rs b/bitcoin-utils/src/debug.rs index 7e382ef..aadfcfb 100644 --- a/bitcoin-utils/src/debug.rs +++ b/bitcoin-utils/src/debug.rs @@ -45,6 +45,10 @@ impl fmt::Display for ExecuteInfo { /// Executes the given script and returns the result of the execution /// (success, error, stack, etc.) pub fn execute_script(script: ScriptBuf) -> ExecuteInfo { + execute_script_with_leaf(script, TapLeafHash::all_zeros()) +} + +pub fn execute_script_with_leaf(script: ScriptBuf, leaf_hash: TapLeafHash) -> ExecuteInfo { let mut exec = Exec::new( ExecCtx::Tapscript, Options { @@ -63,7 +67,7 @@ pub fn execute_script(script: ScriptBuf) -> ExecuteInfo { }, prevouts: vec![], input_idx: 0, - taproot_annex_scriptleaf: Some((TapLeafHash::all_zeros(), None)), + taproot_annex_scriptleaf: Some((leaf_hash, None)), }, script, vec![], diff --git a/bitcoin-utils/src/lib.rs b/bitcoin-utils/src/lib.rs index bf8cd82..5a3db91 100644 --- a/bitcoin-utils/src/lib.rs +++ b/bitcoin-utils/src/lib.rs @@ -1,3 +1,10 @@ +use bitcoin::{ + key::{Keypair, Parity, Secp256k1}, + secp256k1::{All, SecretKey}, + sighash::{Prevouts, SighashCache}, + taproot::{LeafVersion, Signature}, + TapLeafHash, TapSighashType, Transaction, TxOut, +}; use bitcoin_scriptexec::Stack; use treepp::*; @@ -23,3 +30,39 @@ pub fn stack_to_script(stack: &Stack) -> Script { } } } + +pub fn comittee_signature( + disprove_script: &Script, + secp_ctx: &Secp256k1, + mut seckey: SecretKey, +) -> Signature { + let tx = Transaction { + version: bitcoin::transaction::Version::TWO, + lock_time: bitcoin::locktime::absolute::LockTime::ZERO, + input: vec![], + output: vec![], + }; + + let sighash = SighashCache::new(tx) + .taproot_script_spend_signature_hash( + 0, + &Prevouts::<&TxOut>::All(&[]), + TapLeafHash::from_script(disprove_script, LeafVersion::TapScript), + TapSighashType::All, + ) + .unwrap(); + + if seckey.public_key(secp_ctx).x_only_public_key().1 == Parity::Even { + seckey = seckey.negate(); + } + + let signature = secp_ctx.sign_schnorr( + &sighash.into(), + &Keypair::from_secret_key(secp_ctx, &seckey), + ); + + Signature { + signature, + sighash_type: TapSighashType::All, + } +} diff --git a/bitcoin-winternitz/src/u32.rs b/bitcoin-winternitz/src/u32.rs index 7ff7a72..fc9ec77 100644 --- a/bitcoin-winternitz/src/u32.rs +++ b/bitcoin-winternitz/src/u32.rs @@ -27,7 +27,7 @@ pub const N: usize = N0 + N1; /// Secret key is array of $N$ chunks by $D$ bits, where the whole number /// of bits is equal to $v$. -#[derive(Clone, Debug, Copy)] +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] pub struct SecretKey([Hash160; N]); impl SecretKey { @@ -94,7 +94,7 @@ impl SecretKey { /// Public key is a hashed $D$ times each of the $n$ parts of the /// [`SecretKey`]. -#[derive(Clone, Copy, Debug)] +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] pub struct PublicKey([Hash160; N]); impl PublicKey { @@ -121,9 +121,15 @@ impl PublicKey { let skip = msg.count_zero_limbs_from_left(); checksig_verify_script_compact(skip, self) } + + /// Construct `script_pubkey` signature verification which uses + /// full encoding. + pub fn checksig_verify_script(&self) -> Script { + checksig_verify_script(self) + } } -#[derive(Clone, Copy, Debug, Default)] +#[derive(Debug, Clone, Hash, PartialEq, Eq, Default, Copy)] pub struct Message([u8; N]); impl Message { @@ -243,7 +249,7 @@ impl Message { } /// Winternitz signature. The array of intermidiate hashes of secret key. -#[derive(Clone, Copy, Debug)] +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] pub struct Signature { sig: [Hash160; N], msg: Message, @@ -256,6 +262,38 @@ impl Signature { self.to_script_sig_skipping(0) } + /// The same as [`Signature::to_script_sig`], but converts signature to + /// witness stack elements instead of script. + pub fn to_witness_stack_elements(self) -> Vec> { + self.to_witness_stack_elements_skipping(0) + } + + fn to_witness_stack_elements_skipping(self, skipping: usize) -> Vec> { + let mut elements = Vec::new(); + + // TODO(Velnbur): later let's move it somewhere else + fn push_element(idx: usize, elements: &mut Vec>, sig: &Signature) { + elements.push(sig.sig[idx].to_byte_array().to_vec()); + let msg_byte = sig.msg.0[idx]; + if msg_byte == 0 { + elements.push(Vec::new()); + } else { + elements.push(msg_byte.to_le_bytes().to_vec()); + } + } + + // Keep all the elements of the checksum. + for idx in (N0..(N0 + N1)).rev() { + push_element(idx, &mut elements, &self); + } + // Push the stack element limbs skipping some of them. + for idx in (0..N0).rev().skip(skipping) { + push_element(idx, &mut elements, &self); + } + + elements + } + /// The same as [`Self::to_script_sig`], but skips `skipping` number of /// limbs and sigs for zero limbs. fn to_script_sig_skipping(self, skipping: usize) -> Script { diff --git a/core/src/assert/mod.rs b/core/src/assert/mod.rs deleted file mode 100644 index 8ce62d7..0000000 --- a/core/src/assert/mod.rs +++ /dev/null @@ -1,343 +0,0 @@ -use std::{collections::HashMap, iter, marker::PhantomData}; - -use bitcoin::{ - absolute::LockTime, - ecdsa, - key::{Keypair, Secp256k1}, - psbt::Input, - relative::Height, - secp256k1::{All, SecretKey}, - sighash::{Prevouts, SighashCache}, - taproot::{LeafVersion, TaprootBuilder, TaprootSpendInfo}, - transaction::Version, - Amount, EcdsaSighashType, OutPoint, Psbt, Sequence, TapSighashType, Transaction, TxIn, TxOut, - Witness, XOnlyPublicKey, -}; -use bitcoin_splitter::split::script::SplitableScript; - -use crate::{ - assert::payout_script::PayoutScript, disprove::form_disprove_scripts_distorted_with_seed, - treepp::*, UNSPENDABLE_KEY, -}; - -use crate::disprove::{form_disprove_scripts, DisproveScript}; - -pub mod payout_script; - -const DISPROVE_SCRIPT_WEIGHT: u32 = 1; -const PAYOUT_SCRIPT_WEIGHT: u32 = 5; - -pub struct AssertTransaction { - /// Operator's public key. - pub operator_pubkey: XOnlyPublicKey, - - /// Amount staked for assertion. - pub amount: Amount, - - pub disprove_scripts: Vec, - pub payout_script: PayoutScript, - - /// Program this transaction asserts. - __program: PhantomData, -} - -impl Clone for AssertTransaction { - fn clone(&self) -> Self { - Self { - operator_pubkey: self.operator_pubkey, - amount: self.amount, - disprove_scripts: self.disprove_scripts.clone(), - payout_script: self.payout_script.clone(), - __program: self.__program, - } - } -} - -pub struct Options { - pub payout_locktime: Height, -} - -impl Default for Options { - fn default() -> Self { - Self { - payout_locktime: payout_script::LOCKTIME.into(), - } - } -} - -impl AssertTransaction { - pub fn from_scripts( - operator_pubkey: XOnlyPublicKey, - payout: PayoutScript, - disprove_scripts: Vec, - amount: Amount, - ) -> Self { - Self { - operator_pubkey, - amount, - disprove_scripts, - payout_script: payout, - __program: PhantomData, - } - } - - /// Construct new Assert transaction. - pub fn new(input: Script, operator_pubkey: XOnlyPublicKey, amount: Amount) -> Self { - Self::with_options(input, operator_pubkey, amount, Default::default()) - } - - pub fn with_options( - input: Script, - operator_pubkey: XOnlyPublicKey, - amount: Amount, - options: Options, - ) -> Self { - let disprove_scripts = form_disprove_scripts::(input.clone()); - let payout_script = PayoutScript::with_locktime(operator_pubkey, options.payout_locktime); - Self { - operator_pubkey, - amount, - disprove_scripts, - payout_script, - __program: PhantomData, - } - } - - pub fn with_options_distorted< - Seed: Sized + Default + AsMut<[u8]> + Copy, - Rng: rand::SeedableRng + rand::Rng, - >( - input: Script, - operator_pubkey: XOnlyPublicKey, - amount: Amount, - options: Options, - seed: Seed, - ) -> (Self, usize) { - let (disprove_scripts, idx) = - form_disprove_scripts_distorted_with_seed::(input.clone(), seed); - let payout_script = PayoutScript::with_locktime(operator_pubkey, options.payout_locktime); - ( - Self { - operator_pubkey, - amount, - disprove_scripts, - payout_script, - __program: PhantomData, - }, - idx, - ) - } - - /// Return partially signed transaction with P2TR output with all disprove - /// scripts and payout script. - pub fn into_psbt(self, ctx: &Secp256k1) -> Psbt { - let txout = self.txout(ctx); - - let tx = Transaction { - version: Version::ONE, - lock_time: LockTime::ZERO, - input: vec![], - output: vec![txout], - }; - - Psbt::from_unsigned_tx(tx) - .expect("witness and script_sigs are not filled, so this should never panic") - } - - pub fn txout(&self, ctx: &Secp256k1) -> TxOut { - let taptree = - Self::form_taptree(ctx, self.payout_script.to_script(), &self.disprove_scripts); - - self.assert_taproot_output(&taptree) - } - - fn assert_taproot_output(&self, taptree: &TaprootSpendInfo) -> TxOut { - let script_pubkey = Script::new_p2tr_tweaked(taptree.output_key()); - - TxOut { - value: self.amount, - script_pubkey, - } - } - - pub fn form_taptree( - ctx: &Secp256k1, - payout_script: Script, - disprove_scripts: &[DisproveScript], - ) -> TaprootSpendInfo { - let scripts_with_weights = iter::once((PAYOUT_SCRIPT_WEIGHT, payout_script)).chain( - disprove_scripts - .iter() - .map(|script| (DISPROVE_SCRIPT_WEIGHT, script.script_pubkey.clone())), - ); - - TaprootBuilder::with_huffman_tree(scripts_with_weights) - .expect("Weights are low, and number of scripts shoudn't create the tree greater than 128 in depth (I believe)") - .finalize(ctx, *UNSPENDABLE_KEY) - .expect("Scripts and keys should be valid") - } - - /// Create Payout transaction which spends first output of Assert - /// transaction using Payout script path. - pub fn payout_transaction( - &self, - ctx: &Secp256k1, - txout: TxOut, - prev_out: OutPoint, - operator_seckey: &SecretKey, - ) -> eyre::Result { - let taptree = - Self::form_taptree(ctx, self.payout_script.to_script(), &self.disprove_scripts); - - let script = self.payout_script.to_script(); - - let mut tx = Transaction { - version: Version::TWO, - lock_time: LockTime::ZERO, - input: vec![TxIn { - previous_output: prev_out, - script_sig: Script::new(), - sequence: Sequence::from_height(self.payout_script.locktime.value()), - witness: Witness::new(), - }], - output: vec![txout], - }; - - let leaf_hash = script.tapscript_leaf_hash(); - let prev_txout = self.assert_taproot_output(&taptree); - - let sighash = SighashCache::new(&tx).taproot_script_spend_signature_hash( - 0, - &Prevouts::All(&[prev_txout]), - leaf_hash, - TapSighashType::Default, - )?; - - let signature = ctx.sign_schnorr( - &sighash.into(), - &Keypair::from_secret_key(ctx, operator_seckey), - ); - - let control_block = &taptree - .control_block(&(script.clone(), LeafVersion::TapScript)) - .unwrap(); - - let mut witness = Witness::new(); - witness.push(signature.as_ref()); - witness.push(self.operator_pubkey.serialize()); - witness.push(script.as_bytes()); - witness.push(control_block.serialize()); - - tx.input[0].witness = witness; - - Ok(tx) - } - - /// Create Payout transaction which spends first output of Assert - /// transaction using Payout script path. - pub fn disprove_transactions( - &self, - ctx: &Secp256k1, - txout: TxOut, - prev_out: OutPoint, - ) -> eyre::Result> { - Self::form_disprove_transactions( - self.payout_script.to_script(), - &self.disprove_scripts, - ctx, - txout, - prev_out, - ) - } - - /// Create Disprove transaction which spends first output of Assert - /// transaction using Payout script path. - pub fn form_disprove_transactions( - payout_script: Script, - disprove_scripts: &[DisproveScript], - ctx: &Secp256k1, - txout: TxOut, - prev_out: OutPoint, - ) -> eyre::Result> { - let taptree = Self::form_taptree(ctx, payout_script, disprove_scripts); - let mut map = HashMap::with_capacity(disprove_scripts.len()); - - for disprove in disprove_scripts { - let script = disprove.script_pubkey.clone(); - let mut witness = Witness::new(); - - for elem in disprove.witness_elements() { - witness.push(elem); - } - - let control_block = &taptree - .control_block(&(script.clone(), LeafVersion::TapScript)) - .unwrap(); - - witness.push(script.as_bytes()); - witness.push(control_block.serialize()); - - let tx = Transaction { - version: Version::ONE, - lock_time: LockTime::ZERO, - input: vec![TxIn { - previous_output: prev_out, - script_sig: Script::new(), - sequence: Sequence::ZERO, - witness, - }], - output: vec![txout.clone()], - }; - - map.insert(disprove.clone(), tx); - } - - Ok(map) - } - - /// Create transaction which spends provided utxo (assuming that it's the - /// P2WPKH one) signed with provided key. - pub fn spend_p2wpkh_input_tx( - self, - ctx: &Secp256k1, - secret_key: &SecretKey, - txout: TxOut, - outpoint: OutPoint, - ) -> eyre::Result { - let mut psbt = self.into_psbt(ctx); - - psbt.unsigned_tx.input.push(TxIn { - previous_output: outpoint, - script_sig: Script::new(), - sequence: Sequence::ZERO, - witness: Witness::new(), - }); - - let sighash = SighashCache::new(&psbt.unsigned_tx).p2wpkh_signature_hash( - 0, - &txout.script_pubkey, - txout.value, - EcdsaSighashType::All, - )?; - - let signature = ctx.sign_ecdsa(&sighash.into(), secret_key); - - let mut witness = Witness::new(); - - // for disprove in disprove_scripts { - // for elem in disprove.witness_elements() { - // witness.push(elem); - // } - // } - witness.push_ecdsa_signature(&ecdsa::Signature::sighash_all(signature)); - witness.push(secret_key.public_key(ctx).serialize()); - - psbt.inputs.push(Input { - witness_utxo: Some(txout), - final_script_witness: Some(witness), - ..Default::default() - }); - - psbt.extract_tx().map_err(Into::into) - } -} diff --git a/core/src/assert/payout_script.rs b/core/src/assert/payout_script.rs deleted file mode 100644 index 69efa54..0000000 --- a/core/src/assert/payout_script.rs +++ /dev/null @@ -1,56 +0,0 @@ -use bitcoin::{ - hashes::{hash160::Hash as Hash160, Hash}, - relative::Height, - XOnlyPublicKey, -}; - -use crate::treepp::*; - -/// Assuming that mean block mining time is 10 minutes: -pub const LOCKTIME: u16 = 6 /* hour */ * 24 /* day */ * 14 /* two weeks */; - -/// Script by which Operator spends the Assert transaction after timelock. -#[derive(Debug, Clone)] -pub struct PayoutScript { - // TODO(Velnbur): add mutisig of the comittee. - // pub comittee_pubkeys: Vec, - /// Public key of the operator - pub operator_pubkey: XOnlyPublicKey, - - /// Specified locktime after which assert transaction is spendable - /// by payout script, default value is [`LOCKTIME`]. - pub locktime: Height, -} - -impl PayoutScript { - pub fn new(operator_pubkey: XOnlyPublicKey) -> Self { - Self { - operator_pubkey, - locktime: Height::from(LOCKTIME), - } - } - - pub fn with_locktime(operator_pubkey: XOnlyPublicKey, locktime: Height) -> Self { - Self { - operator_pubkey, - locktime, - } - } - - pub fn to_script(&self) -> Script { - script! { - { self.locktime.value() as u32 } - OP_CSV - OP_DROP - OP_DUP - OP_HASH160 - { - Hash160::hash( - &self.operator_pubkey.serialize() - ).as_byte_array().to_vec() - } - OP_EQUALVERIFY - OP_CHECKSIG - } - } -} diff --git a/core/src/disprove/mod.rs b/core/src/disprove/mod.rs deleted file mode 100644 index 456850f..0000000 --- a/core/src/disprove/mod.rs +++ /dev/null @@ -1,288 +0,0 @@ -use bitcoin::{opcodes::ClassifyContext, script::Instruction}; -use bitcoin_utils::{comparison::OP_LONGNOTEQUAL, pseudo::OP_LONGFROMALTSTACK, treepp::*}; - -use signing::SignedIntermediateState; - -use bitcoin_splitter::split::{ - core::SplitType, - intermediate_state::IntermediateState, - script::{SplitResult, SplitableScript}, -}; - -pub mod signing; - -#[cfg(test)] -mod tests; - -/// Script letting challengers spend the **Assert** transaction -/// output if the operator computated substates incorrectly. -/// -/// This a typed version of [`Script`] can be easily converted into it. -/// -/// The script structure in general is simple: -/// ## Witness: -/// ```bitcoin_script -/// { Enc(z[i+1]) and Sig[i+1] } // Zipped -/// { Enc(z[i]) and Sig[i] } // Zipped -/// ``` -/// -/// ## Script: -/// ```bitcoin_script -/// { pk[i] } // { Zip(Enc(z[i+1]), Sig[i+1]), Zip(Enc(z[i]), Sig[i]), pk[i] } -/// { OP_WINTERNITZVERIFY } // { Zip(Enc(z[i+1]), Sig[i+1]), Enc(z[i]) } -/// { OP_RESTORE } // { Zip(Enc(z[i+1]), Sig[i+1]), z[i] } -/// { OP_TOALTSTACK } // { Zip(Enc(z[i+1]), Sig[i+1]) } -/// { pk[i+1] } // { Zip(Enc(z[i+1]), Sig[i+1]), pk[i+1] } -/// { OP_WINTERNITZVERIFY } // { Enc(z[i+1]) } -/// { OP_RESTORE } // { z[i+1] } -/// { OP_FROMALTSTACK } // { z[i+1] z[i] } -/// { fn[i] } // { z[i+1] fn[i](z[i]) } -/// { OP_EQUAL } // { z[i+1] == fn[i](z[i]) } -/// { OP_NOT } // { z[i+1] != fn[i](z[i]) } -/// ``` -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -pub struct DisproveScript { - pub script_witness: Script, - pub script_pubkey: Script, -} - -impl DisproveScript { - /// Given the previous and current states, and the function that was executed, - /// creates a new DisproveScript according to the BitVM2 protocol. - pub fn new(from: &IntermediateState, to: &IntermediateState, function: &Script) -> Self { - // Sign the states with the regular entropy randomness - let from_signed = SignedIntermediateState::sign(from); - let to_signed = SignedIntermediateState::sign(to); - - Self::new_from_signed_states(&from_signed, &to_signed, function) - } - - /// Given the previous and current states, and the function that was executed, - /// creates a new DisproveScript according to the BitVM2 protocol. - /// - /// The randomness is derived from the `seed`. - pub fn new_with_seed( - from: &IntermediateState, - to: &IntermediateState, - function: &Script, - seed: Seed, - ) -> Self - where - Seed: Sized + Default + AsMut<[u8]> + Copy, - Rng: rand::SeedableRng + rand::Rng, - { - // Sign the states with the seed randomness - let from_signed = SignedIntermediateState::sign_with_seed::(from, seed); - let to_signed = SignedIntermediateState::sign_with_seed::(to, seed); - - Self::new_from_signed_states(&from_signed, &to_signed, function) - } - - /// Given the previous and current states signed, and the function that was executed, - /// creates a new DisproveScript according to the BitVM2 protocol. - fn new_from_signed_states( - from: &SignedIntermediateState, - to: &SignedIntermediateState, - function: &Script, - ) -> Self { - // Step 1. - // We form the witness script. Just pushing all - // signatures + messages to the witness script - let script_witness = script! { - { from.witness_script() } // Zipped Enc(z[i]) and Sig[i] - { to.witness_script() } // Zipped Enc(z[i+1]) and Sig[i+1] - }; - - // Step 3. - // Now, we form the script pubkey - let script_pubkey = script! { - // Step 3.1. Public key + verification of "to" state - { to.verification_script_toaltstack() } // This leaves z[i+1] in the altstack - { from.verification_script() } // This leaves z[i].mainstack in the mainstack, while (z[i+1], z[i].altstack) is still in the altstack - - // Step 3.2. Applying function and popping "to" state - { function.clone() } // This leaves f[i](z[i]).mainstack in the mainstack and { z[i+1].altstack, f[i](z[i]).altstack } in the altstack - { OP_LONGFROMALTSTACK(to.altstack.len()) } - { to.verification_script_fromaltstack() } // This leaves z[i+1].mainstack and f[i](z[i]).mainstack in the mainstack, while f[i](z[i]).altstack and z[i+1].alstack is in the altstack - - // Step 3.3. - // At this point, our stack consists of: - // { f[i](z[i]).mainstack, f[i](z[i]).altstack, z[i+1].mainstack } - // while the altstack has z[i+1].altstack. - // Thus, we have to pick f[i](z[i]).mainstack to the top of the stack - for _ in (0..to.stack.len()).rev() { - { to.total_len() + to.stack.len() - 1 } OP_ROLL - } - - // At this point, we should have - // { f[i](z[i]).altstack, z[i+1].mainstack, f[i](z[i]).mainstack } - - // Step 3.4. Checking if z[i+1] == f(z[i]) - // a) Mainstack verification - { OP_LONGNOTEQUAL(to.stack.len()) } - - // b) Altstack verification - { OP_LONGFROMALTSTACK(to.altstack.len()) } - - // Since currently our stack looks like: - // { f[i](z[i]).altstack, {bit}, z[i+1].altstack, }, - // we need to push f[i](z[i]).altstack to the top of the stack - for _ in 0..to.altstack.len() { - { 2*to.altstack.len() } OP_ROLL - } - - { OP_LONGNOTEQUAL(to.altstack.len()) } - OP_BOOLOR - }; - - Self { - script_witness, - script_pubkey, - } - } - - /// Returns the elements of the witness script - pub fn witness_elements(&self) -> Vec> { - let mut elements = Vec::with_capacity(self.script_witness.len()); - - for instruction in self.script_witness.instructions() { - match instruction.unwrap() { - Instruction::PushBytes(bytes) => { - elements.push(bytes.as_bytes().to_vec()); - } - Instruction::Op(opcode) => { - match opcode.classify(ClassifyContext::TapScript) { - bitcoin::opcodes::Class::PushNum(num) => { - let buf = num.to_le_bytes().into_iter().filter(|b| *b != 0).collect(); - elements.push(buf); - } - _ => { - unreachable!("script witness shouldn't have opcodes, got {opcode}") - } - }; - } - } - } - - elements - } -} - -/// Given the `input` script, [`SplitResult`] and `constructor`, does the following: -/// - For each shard, creates a DisproveScript using `constructor` -/// - Returns the list of [`DisproveScript`]s. -fn disprove_scripts_with_constructor( - input: Script, - split_result: SplitResult, - constructor: F, -) -> Vec -where - F: Fn(&IntermediateState, &IntermediateState, &Script) -> DisproveScript + Clone, -{ - assert_eq!( - split_result.shards.len(), - split_result.intermediate_states.len(), - "Shards and intermediate states must have the same length" - ); - - (0..split_result.shards.len()) - .map(|i| { - let from_state = match i { - 0 => IntermediateState::from_inject_script(&input.clone()), - _ => split_result.intermediate_states[i - 1].clone(), - }; - - constructor( - &from_state, - &split_result.intermediate_states[i], - &split_result.shards[i], - ) - }) - .collect() -} - -/// Given the script and its input, does the following: -/// - Splits the script into shards -/// - For each shard, creates a [`DisproveScript`] -/// - Returns the list of [`DisproveScript`]s -pub fn form_disprove_scripts(input: Script) -> Vec { - let split_result = S::default_split(input.clone(), SplitType::default()); - disprove_scripts_with_constructor(input, split_result, DisproveScript::new) -} - -/// Given the script and its input, does the following: -/// - Splits the script into shards -/// - Distorts the random intermediate state, making -/// two state transitions incorrect -/// - For each shard, creates a [`DisproveScript`] -/// - Returns the list of [`DisproveScript`]s and the index of distorted shard -pub fn form_disprove_scripts_distorted( - input: Script, -) -> (Vec, usize) { - // Splitting the script into shards - let split_result = S::default_split(input.clone(), SplitType::default()); - - // Distorting the output of the random shard - let (distorted_split_result, distorted_shard_id) = split_result.distort(); - - // Creating the disprove scripts - let disprove_scripts = - disprove_scripts_with_constructor(input, distorted_split_result, DisproveScript::new); - - // Returning the result - (disprove_scripts, distorted_shard_id) -} - -/// Given the script and its input, does the following: -/// - Splits the script into shards -/// - For each shard, creates a [`DisproveScript`] -/// - Returns the list of [`DisproveScript`]s -/// -/// The randomness is derived from the `seed`. -pub fn form_disprove_scripts_with_seed( - input: Script, - seed: Seed, -) -> Vec -where - S: SplitableScript, - Seed: Sized + Default + AsMut<[u8]> + Copy, - Rng: rand::SeedableRng + rand::Rng, -{ - let split_result = S::default_split(input.clone(), SplitType::default()); - disprove_scripts_with_constructor(input, split_result, |from, to, shard| { - DisproveScript::new_with_seed::(from, to, shard, seed) - }) -} - -/// Given the script and its input, does the following: -/// - Splits the script into shards -/// - Distorts the random intermediate state, making -/// two state transitions incorrect -/// - For each shard, creates a [`DisproveScript`] -/// - Returns the list of [`DisproveScript`]s and the index of distorted shard -/// -/// The randomness is derived from the `seed`. -pub fn form_disprove_scripts_distorted_with_seed( - input: Script, - seed: Seed, -) -> (Vec, usize) -where - S: SplitableScript, - Seed: Sized + Default + AsMut<[u8]> + Copy, - Rng: rand::SeedableRng + rand::Rng, -{ - // Splitting the script into shards - let split_result = S::default_split(input.clone(), SplitType::default()); - - // Distorting the output of the random shard - let (distorted_split_result, distorted_shard_id) = split_result.distort(); - - // Creating the disprove scripts - let disprove_scripts = - disprove_scripts_with_constructor(input, distorted_split_result, |from, to, shard| { - DisproveScript::new_with_seed::(from, to, shard, seed) - }); - - // Returning the result - (disprove_scripts, distorted_shard_id) -} diff --git a/core/src/lib.rs b/core/src/lib.rs deleted file mode 100644 index e91fd76..0000000 --- a/core/src/lib.rs +++ /dev/null @@ -1,33 +0,0 @@ -use bitcoin::XOnlyPublicKey; -use once_cell::sync::Lazy; - -pub mod assert; -pub mod disprove; - -#[allow(dead_code)] -// Re-export what is needed to write treepp scripts -pub mod treepp { - pub use bitcoin_script::{define_pushable, script}; - pub use bitcoin_utils::debug::{execute_script, run}; - - define_pushable!(); - pub use bitcoin::ScriptBuf as Script; -} - -// FIXME(Velnbur): Use really non spendable key. For example checkout: -// 1. https://github.com/nomic-io/nomic/blob/5ba8b661e6d9ffb6b9eb39c13247cccefa5342a9/src/babylon/mod.rs#L451 -pub static UNSPENDABLE_KEY: Lazy = Lazy::new(|| { - "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0" - .parse() - .unwrap() -}); - -#[cfg(test)] -mod tests { - use crate::UNSPENDABLE_KEY; - - #[test] - fn test_unspendable_key() { - let _ = *UNSPENDABLE_KEY; - } -} diff --git a/docker-compose.yaml b/docker-compose.yaml index cff89d5..66bb2ab 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,7 +1,7 @@ services: bitcoind: - image: lncm/bitcoind:v26.0 + image: bitcoin/bitcoin:28 container_name: bitcoind restart: on-failure stop_grace_period: 30s @@ -11,7 +11,6 @@ services: interval: 2s volumes: - ./configs/bitcoind.conf:/root/.bitcoin/bitcoin.conf - - bitcoind:/root/.bitcoin entrypoint: - "sh" - "-c" @@ -20,5 +19,3 @@ services: - 18443:18443 - 18444:18444 -volumes: - bitcoind: diff --git a/docs/paper/iacrtrans.cls b/docs/paper/iacrtrans.cls index a3d0369..e84b548 100644 --- a/docs/paper/iacrtrans.cls +++ b/docs/paper/iacrtrans.cls @@ -650,6 +650,8 @@ \RequirePackage[most]{tcolorbox} \tcolorboxenvironment{example}{breakable,boxrule=0pt,boxsep=0pt,colback={gray!3},left=8pt,right=8pt,enhanced jigsaw, borderline west={1.5pt}{0pt}{green!60!black},sharp corners,before skip=10pt,after skip=10pt} \tcolorboxenvironment{definition}{breakable,boxrule=0pt,boxsep=0pt,colback={gray!3},left=8pt,right=8pt,enhanced jigsaw, borderline west={1.5pt}{0pt}{blue},sharp corners,before skip=10pt,after skip=10pt} +\tcolorboxenvironment{proposition}{breakable,boxrule=0pt,boxsep=0pt,colback={gray!3},left=8pt,right=8pt,enhanced jigsaw, borderline west={1.5pt}{0pt}{orange!60!black},sharp corners,before skip=10pt,after skip=10pt} +\tcolorboxenvironment{theorem}{breakable,boxrule=0pt,boxsep=0pt,colback={gray!3},left=8pt,right=8pt,enhanced jigsaw, borderline west={1.5pt}{0pt}{purple!90!white},sharp corners,before skip=10pt,after skip=10pt} \endinput -%end of file iacrtrans.cls \ No newline at end of file +%end of file iacrtrans.cls diff --git a/docs/paper/nero.pdf b/docs/paper/nero.pdf index 9f85cd0..13384fa 100644 Binary files a/docs/paper/nero.pdf and b/docs/paper/nero.pdf differ diff --git a/docs/paper/nero.tex b/docs/paper/nero.tex index a110a2f..dfac952 100644 --- a/docs/paper/nero.tex +++ b/docs/paper/nero.tex @@ -22,8 +22,9 @@ \usepackage[nameinlink]{cleveref} \crefname{algocf}{alg.}{algs.} \Crefname{algocf}{Algorithm}{Algorithms} +\usepackage{xstring} \usepackage{tikz} -\usetikzlibrary{shapes.geometric, arrows.meta, positioning} +\usetikzlibrary{shapes.geometric, arrows.meta, positioning, calc} \newtcolorbox{empheqboxed}{ enhanced, @@ -49,12 +50,13 @@ \newcommand{\elem}[1]{\, \langle #1 \rangle \,} \newcommand{\opcode}[1]{\, \texttt{#1} \,} \newcommand{\script}[1]{ $\big\{ #1 \big\}$ } +\newcommand{\nero}{$\mathcal{N}\mathfrak{e}\mathcal{R}O$} % -- Algorithms -- \usepackage[ - titlenumbered, - linesnumbered, - ruled + titlenumbered, + linesnumbered, + ruled ]{algorithm2e} \SetKwInOut{Input}{Input} \SetKwInOut{Output}{Output} @@ -65,11 +67,15 @@ \DeclareMathOperator*{\argmax}{arg\,max} \DeclareMathOperator*{\argmin}{arg\,min} -\author{Oleksandr Kurbatov\inst{1} \and Dmytro Zakharov\inst{1} +\author{Oleksandr Kurbatov\inst{1} \and Dmytro Zakharov\inst{1} \and Kyrylo Baibula \inst{1}} \institute{Distributed Lab -\email{ok@distributedlab.com}, \email{dmytro.zakharov@distributedlab.com}, \email{kyrylo.baybula@distributedlab.com}} -\title[Verifiable Computation on Bitcoin]{$\mathcal{N}\mathfrak{e}\mathcal{R}O$: BitVM2-Based Generic Optimistic Verifiable Computation on Bitcoin} + \email{ok@distributedlab.com}, + \email{dmytro.zakharov@distributedlab.com}, +\email{kyrylo.baybula@distributedlab.com}} +\title[Verifiable Computation on +Bitcoin]{$\mathcal{N}\mathfrak{e}\mathcal{R}O$: BitVM2-Based Generic +Optimistic Verifiable Computation on Bitcoin} \hypersetup{ pdfauthor={Distributed Lab}, @@ -80,6 +86,15 @@ pdflang={English} } +\def\bitcoin{% + \leavevmode + \vtop{\offinterlineskip + \setbox0=\hbox{B}% + \setbox2=\hbox to\wd0{\hfil\hskip-.03em + \vrule height .3ex width .15ex\hskip .08em + \vrule height .3ex width .15ex\hfil} +\vbox{\copy2\box0}\box2}} + \usepackage{biblatex} \addbibresource{refs.bib} @@ -87,7 +102,8 @@ \maketitle -\keywords[]{Bitcoin, Bitcoin Script, BitVM2, Verifiable Computation, Optimistic Verification, L2 Layer, Zero-Knowledge Proofs} +\keywords[]{Bitcoin, Bitcoin Script, BitVM2, Verifiable Computation, +Optimistic Verification, L2 Layer, Zero-Knowledge Proofs} \begin{abstract} One of Bitcoin's biggest unresolved challenges is the ability to execute a @@ -98,11 +114,13 @@ narrow down the problem to the verifiable computation which is more feasible given the current state of Bitcoin. - One of the ways to do it is the \textit{BitVM2} protocol. Based on it, we are aiming to + One of the ways to do it is the \textit{BitVM2} protocol. Based on + it, we are aiming to create a generic library for the on-chain verifiable computations. This document is designated to state our progress, pitfalls, and challenges - encountered during the development. While most of the current efforts are put into - transferring the \textit{Groth16} verifier on-chain with the + encountered during the development. While most of the current + efforts are put into + transferring the \textit{Groth16} verifier on-chain with the main focus on implementing bridge, we try to solve a broader problem, enabling a more significant number of potential use cases (including zero-knowledge proofs verification). @@ -111,7 +129,8 @@ \setcounter{tocdepth}{2} \tableofcontents -\begin{tcolorbox}[colback=green!15!white, halign title=flush center, colframe=green!70!black, fonttitle=\bfseries\large, title=Note, sharp corners] +\begin{tcolorbox}[colback=green!15!white, halign title=flush center, + colframe=green!70!black, fonttitle=\bfseries\large, title=Note, sharp corners] \centering This is a very early version of the paper, development is still in active progress! \end{tcolorbox} @@ -123,9 +142,10 @@ \section{Introduction}\label{sec:intro} limits on transactions --- only 4 MB are allowed, making it challenging to implement any advanced cryptographic (and not only) primitives, among which highly desirable zero-knowledge proofs verification on-chain. To address this -limitation, the \textit{BitVM2} \autocite{bitvm2} proposal introduces an innovative +limitation, the \textit{BitVM2} \autocite{bitvm2} proposal introduces +an innovative approach that enables the optimistic execution of large programs on the Bitcoin -chain. +chain. The proposed method suggests that the executor (which is called an \textbf{operator}) splits the large program into smaller chunks (which we @@ -135,7 +155,8 @@ \section{Introduction}\label{sec:intro} This document provides a concise overview of our progress in implementing the library for generic, optimistic, verifiable computation on Bitcoin. Currently, -we are focusing on reproducing the \textit{BitVM2} paper approach while not limiting the +we are focusing on reproducing the \textit{BitVM2} paper approach +while not limiting the function and input/output format as much as possible. \section{Program Split}\label{sec:program-splitting} @@ -148,52 +169,61 @@ \subsection{Public Verifiable Computation} computationally limited verifier $\mathcal{V}$ outsource the evaluation of some function $f$ on input $x$ to the prover (worker) $\mathcal{P}$. Then, $\mathcal{V}$ can verify the correctness of the provided output $y$ by -performing significantly less work than $f$ requires. +performing significantly less work than $f$ requires. In the context of Bitcoin on-chain verification, $\mathcal{V}$ can be viewed as the Bitcoin smart contract which is heavily limited in computational resources (due to the inherit Bitcoin Script inexpressiveness). The prover $\mathcal{P}$ is the operator who executes the program on-chain. The program $f$ is the -Bitcoin Script, and the input $x$ is the data provided by the operator. +Bitcoin Script, and the input $x$ is the data provided by the operator. Now, we define the \textit{public verifiable computation scheme} as follows: \begin{definition} - A public verifiable computation (VC) scheme $\Pi_{\text{VC}}$ + A public verifiable computation (VC) scheme $\Pi_{\text{VC}}$ consists of three probabilistic polynomial-time algorithms: \begin{itemize} \item $\textsc{Gen}(f,1^{\lambda})$: randomized algorithm, taking the - security parameter $\lambda \in \mathbb{N}$ and the function $f$ as input, - and outputting the prover and verifier parameters $\mathsf{pp}$ and - $\mathsf{vp}$. + security parameter $\lambda \in \mathbb{N}$ and the function $f$ as input, + and outputting the prover and verifier parameters $\mathsf{pp}$ and + $\mathsf{vp}$. \item $\textsc{Compute}(\mathsf{pp}, x)$: deterministic algorithm, taking - the prover parameters $\mathsf{pp}$ and the input $x$, and outputting $y=f(x)$ - together with a ``proof of computation'' $\pi$. + the prover parameters $\mathsf{pp}$ and the input $x$, and + outputting $y=f(x)$ + together with a ``proof of computation'' $\pi$. \item $\textsc{Verify}(\mathsf{vp}, x, y, \pi)$: given the verifier - parameters $\mathsf{vp}$, the input $x$, the output $y$, and the proof - $\pi$, the algorithm outputs $\mathsf{accept}$ or $\mathsf{reject}$ based on - the correctness of the computation. + parameters $\mathsf{vp}$, the input $x$, the output $y$, and the proof + $\pi$, the algorithm outputs $\mathsf{accept}$ or + $\mathsf{reject}$ based on + the correctness of the computation. \end{itemize} Such scheme should satisfy the following properties (informally): \begin{itemize} \item \textbf{Correctness}. Given any function $f$ and input $x$, - \begin{equation*} - \text{Pr}\left[\mathsf{Verify}(\mathsf{vp}, x, y, \pi) = \mathsf{accept}\; \Big| \; \begin{matrix} - (\mathsf{pp},\mathsf{vp}) \gets \mathsf{Gen}(f,1^{\lambda}) \\ - (y,\pi) \gets \mathsf{Compute}(\mathsf{pp},x) - \end{matrix}\right] = 1 - \end{equation*} + \begin{equation*} + \text{Pr}\left[\mathsf{Verify}(\mathsf{vp}, x, y, \pi) = + \mathsf{accept}\; \Big| \; + \begin{matrix} + (\mathsf{pp},\mathsf{vp}) \gets \mathsf{Gen}(f,1^{\lambda}) \\ + (y,\pi) \gets \mathsf{Compute}(\mathsf{pp},x) + \end{matrix}\right] = 1 + \end{equation*} \item \textbf{Security}. For any $f$ and any probabilistic - polynomial-time adversary $\mathcal{A}$, - \begin{equation*} - \text{Pr}\left[\mathsf{Verify}(\mathsf{vp}, \widetilde{x}, \widetilde{y}, \widetilde{\pi}) = \mathsf{accept}\; \Big| \; \begin{matrix} - (\mathsf{pp},\mathsf{vp}) \gets \mathsf{Gen}(f,1^{\lambda}) \\ - (\widetilde{x}, \widetilde{y}, \widetilde{\pi}) \gets \mathcal{A}(\mathsf{pp}, \mathsf{vp}), \; f(\widetilde{x}) \neq \widetilde{y} - \end{matrix}\right] \leq \mathsf{negl}(\lambda) - \end{equation*} + polynomial-time adversary $\mathcal{A}$, + \begin{equation*} + \text{Pr}\left[\mathsf{Verify}(\mathsf{vp}, \widetilde{x}, + \widetilde{y}, \widetilde{\pi}) = \mathsf{accept}\; \Big| \; + \begin{matrix} + (\mathsf{pp},\mathsf{vp}) \gets \mathsf{Gen}(f,1^{\lambda}) \\ + (\widetilde{x}, \widetilde{y}, \widetilde{\pi}) \gets + \mathcal{A}(\mathsf{pp}, \mathsf{vp}), \; + f(\widetilde{x}) \neq \widetilde{y} + \end{matrix}\right] \leq \mathsf{negl}(\lambda) + \end{equation*} \item \textbf{Efficiency}. $\mathsf{Verify}$ should be much cheaper than the - evaluation of $f$. For example, if the evaluation of $f$ takes $T_f$, - then the verification could take $T_{\mathcal{V}} = \mathcal{O}(\log T_f)$. + evaluation of $f$. For example, if the evaluation of $f$ takes $T_f$, + then the verification could take $T_{\mathcal{V}} = + \mathcal{O}(\log T_f)$. \end{itemize} \end{definition} @@ -202,28 +232,29 @@ \subsection{Motivation for Verifiable Computation on Bitcoin} want to verify its execution on-chain. Suppose the prover $\mathcal{P}$ claims that ${y} = f({x})$ for published ${x}$ and ${y}$. Some of the examples include: \begin{itemize} - \item \textbf{Field Multiplication}: $f(a,b) = a \times b$ for $a,b \in + \item \textbf{Field Multiplication}: $f(a,b) = a \times b$ for $a,b \in \mathbb{F}_p$. Here, the input ${x}=(a,b) \in \mathbb{F}_p^2$ is a tuple of two field elements, while the output $y \in \mathbb{F}_p$ is a single field element. - \item \textbf{EC Points Addition}: $f(x_1,y_1,x_2,y_2) = (x_1,y_1) \oplus + \item \textbf{EC Points Addition}: $f(x_1,y_1,x_2,y_2) = (x_1,y_1) \oplus (x_2,y_2) = (x_3,y_3)$. Input is a tuple $(x_1,y_1,x_2,y_2)$ of four field elements, representing the coordinates of two elliptic curve points. The output is a point $(x_3,y_3)$, represented by two field elements $\mathbb{F}_p$. - \item \textbf{Groth16 Verifier}: $f(\pi_1,\pi_2,\pi_3) = b$ for $b \in + \item \textbf{Groth16 Verifier}: $f(\pi_1,\pi_2,\pi_3) = b$ for $b \in \{\mathsf{accept}, \mathsf{reject}\}$. Based on three provided points $\pi_1$,$\pi_2$,$\pi_3$, representing the proof, decide whether the proof is valid. \end{itemize} As mentioned before, publishing $f$ entirely on-chain is not an option. Instead, -the \textit{BitVM2} paper suggests splitting the program into shards (subprograms) $f_1,\dots,f_n$ such +the \textit{BitVM2} paper suggests splitting the program into shards +(subprograms) $f_1,\dots,f_n$ such that $f=f_n \circ f_{n-1} \circ \dots \circ f_1$, where $\circ$ denotes the function composition. This way, both the prover $\mathcal{P}$ and verifier $\mathcal{V}$ can calculate all intermediate results as follows: \begin{equation*} - {z}_j = f_j({z}_{j-1}), \; \text{for each $j \in \{1,\dots,n\}$} + {z}_j = f_j({z}_{j-1}), \; \text{for each $j \in \{1,\dots,n\}$} \end{equation*} Of course, we additionally set ${z}_0 := {x}$. If everything was computed @@ -231,7 +262,8 @@ \subsection{Motivation for Verifiable Computation on Bitcoin} have ${z}_n = {y}$. We will give a practical example of this in the further sections. -So recall that $\mathcal{P}$ (referred to in \textit{BitVM2} as the \textit{operator}) +So recall that $\mathcal{P}$ (referred to in \textit{BitVM2} as the +\textit{operator}) only needs to prove that the given program $f$ indeed returns ${y}$ for \({x}\), otherwise \textbf{anyone can disprove this fact}. In our case, this means giving challengers (essentially, being verifiers $\mathcal{V}$) the ability to prove @@ -240,17 +272,17 @@ \subsection{Motivation for Verifiable Computation on Bitcoin} So overall, the idea of \textit{BitVM2} can be described as follows: \begin{enumerate} - \item The program $f$ is decomposed into shards $f_1,\dots,f_n$ of + \item The program $f$ is decomposed into shards $f_1,\dots,f_n$ of reasonable size\footnote{By ``size'' we mean the number of \texttt{OP\_CODES} needed to represent the logic.}. - \item $\mathcal{P}$ executes $f$ on input ${x}$ shard by shard, obtaining + \item $\mathcal{P}$ executes $f$ on input ${x}$ shard by shard, obtaining intermediate steps ${z}_1,\dots,{z}_n$. - \item $\mathcal{P}$ commits to the given intermediate steps and publishes + \item $\mathcal{P}$ commits to the given intermediate steps and publishes commitments on-chain. - \item $\mathcal{V}$, knowing ${x}$ published by $\mathcal{P}$, executes the + \item $\mathcal{V}$, knowing ${x}$ published by $\mathcal{P}$, executes the same program, obtaining his own states $\widetilde{z}_1,\dots,\widetilde{z}_n$. - \item $\mathcal{V}$ checks whether $\widetilde{z}_j = z_j$. If this does not + \item $\mathcal{V}$ checks whether $\widetilde{z}_j = z_j$. If this does not hold, the verifier publishes transactions corresponding to the disprove statement $z_j \neq f_j(z_{j-1})$ and claims funds. \end{enumerate} @@ -265,80 +297,158 @@ \subsection{Implementation on Bitcoin} In other words, executing the script \script{\elem{x} \elem{f_1} \elem{f_2}} is the same as calculating composition $f_2 \circ f_1(x)$. So all what remains is finding \textit{valid} $f_1,\dots,f_n$ such that $f = f_1 \parallel f_{2} -\parallel \dots \parallel f_n$. All the intermediate steps $\{z_j\}_{0 \leq j +\parallel \dots \parallel f_n$. All the intermediate steps ${\{z_j\}}_{0 \leq j \leq n}$ can be calculated as specified in \Cref{alg:intermediate_steps}. \begin{algorithm}[H] -\caption{Calculating intermediate steps from script shard decomposition} -\Input{Script $f$} -\Output{Intermediate steps $z_1,\dots,z_n$} + \caption{Calculating intermediate steps from script shard decomposition} + \Input{Script $f$} + \Output{Intermediate steps $z_1,\dots,z_n$} -Decompose $f$ into shards: $(f_1,\dots,f_n) \gets \mathsf{Decompose}(f)$; + Decompose $f$ into shards: $(f_1,\dots,f_n) \gets \mathsf{Decompose}(f)$; -\For{$i \in \{1,\dots,n\}$}{ + \For{$i \in \{1,\dots,n\}$}{ $z_i \gets \mathsf{Exec}(\{\elem{z_{i-1}} \elem{f_i}\})$; -} + } -\Return{$z_1,\dots,z_n$} -\label{alg:intermediate_steps} -\end{algorithm} + \Return{$z_1,\dots,z_n$} +\end{algorithm}\label{alg:intermediate_steps} + +\begin{example} + Consider a fairly simple program $f$: + \begin{equation*} + f(a,b) = 25a^2b^2{(a+b)}^2 + \end{equation*} + + Additionally, assume that we can abstract the multiplication operation + via the opcode \texttt{OP\_MUL} (which, in fact, is already natively + implemented in Bitcoin Script, although the size of such script + for two 254-bit numbers exceeds 60kB). + Then, the implementation of $f$ in Bitcoin Script could be: + \begin{empheqboxed} + \begin{align*} + &\elem{a} \elem{b} \opcode{\texttt{OP\_2DUP}} + \opcode{\texttt{OP\_ADD}} \opcode{\texttt{OP\_MUL}} + \opcode{\texttt{OP\_MUL}} + \opcode{\texttt{OP\_DUP}} \opcode{\texttt{OP\_DUP}} \\& + \opcode{\texttt{OP\_ADD}} \opcode{\texttt{OP\_DUP}} + \opcode{\texttt{OP\_ADD}} \opcode{\texttt{OP\_ADD}} + \opcode{\texttt{OP\_DUP}} \opcode{\texttt{OP\_MUL}} + \end{align*} + \end{empheqboxed} + + Fairly complex, right? Let us split the function into three shards + $\textcolor{red!80!white}{f_1}$, $\textcolor{blue}{f_2}$, and + $\textcolor{green!60!black}{f_3}$: + \begin{align*} + \textcolor{red!80!white}{f_1}(x,y) = xy(x+y), \quad + \textcolor{blue}{f_2}(z) = 5z, \quad + \textcolor{green!60!black}{f_3}(w) = w^2 + \end{align*} + + This way, it is fairly easy to see that $f(a,b) = + \textcolor{green!60!black}{f_3} \circ \textcolor{blue}{f_2} \circ + \textcolor{red!80!white}{f_1}(a,b)$. In turn, in Bitcoin + script we can represent $f$ as $\textcolor{red!80!white}{f_1} + \parallel \textcolor{blue}{f_2} \parallel \textcolor{green!60!black}{f_3}$: + \begin{empheqboxed} + \begin{align*} + &\elem{a} \elem{b} + \textcolor{red!80!white}{\opcode{\texttt{OP\_2DUP}} + \opcode{\texttt{OP\_ADD}} \opcode{\texttt{OP\_MUL}} + \opcode{\texttt{OP\_MUL}}} && + \textcolor{gray!80!black}{\text{// $xy(x+y)$}} \\ + &\textcolor{blue}{\opcode{\texttt{OP\_DUP}} + \opcode{\texttt{OP\_DUP}} \opcode{\texttt{OP\_ADD}} + \opcode{\texttt{OP\_DUP}} \opcode{\texttt{OP\_ADD}} + \opcode{\texttt{OP\_ADD}}} && \textcolor{gray!80!black}{\text{// $5z$}} \\ + &\textcolor{green!60!black}{\opcode{\texttt{OP\_DUP}} + \opcode{\texttt{OP\_MUL}}}&&\textcolor{gray!80!black}{\text{// $w^2$}} + \end{align*} + \end{empheqboxed} +\end{example} \begin{example} Consider a fairly simple program $f$: \begin{equation*} - f(a,b) = 25a^2b^2(a+b)^2 + f(a,b) = 25a^2b^2(a+b)^2 \end{equation*} - Additionally, assume that we can abstract the multiplication operation - via the opcode \texttt{OP\_MUL} (which, in fact, is already natively - implemented in Bitcoin Script, although the size of such script - for two 254-bit numbers exceeds 60kB). + Additionally, assume that we can abstract the multiplication operation + via the opcode \texttt{OP\_MUL} (which, in fact, is already natively + implemented in Bitcoin Script, although the size of such script + for two 254-bit numbers exceeds 60kB). Then, the implementation of $f$ in Bitcoin Script could be: \begin{empheqboxed} \begin{align*} - &\elem{a} \elem{b} \opcode{\texttt{OP\_2DUP}} \opcode{\texttt{OP\_ADD}} \opcode{\texttt{OP\_MUL}} \opcode{\texttt{OP\_MUL}} - \opcode{\texttt{OP\_DUP}} \opcode{\texttt{OP\_DUP}} \\& \opcode{\texttt{OP\_ADD}} \opcode{\texttt{OP\_DUP}} \opcode{\texttt{OP\_ADD}} \opcode{\texttt{OP\_ADD}} \opcode{\texttt{OP\_DUP}} \opcode{\texttt{OP\_MUL}} + &\elem{a} \elem{b} \opcode{\texttt{OP\_2DUP}} + \opcode{\texttt{OP\_ADD}} \opcode{\texttt{OP\_MUL}} + \opcode{\texttt{OP\_MUL}} + \opcode{\texttt{OP\_DUP}} \opcode{\texttt{OP\_DUP}} \\& + \opcode{\texttt{OP\_ADD}} \opcode{\texttt{OP\_DUP}} + \opcode{\texttt{OP\_ADD}} \opcode{\texttt{OP\_ADD}} + \opcode{\texttt{OP\_DUP}} \opcode{\texttt{OP\_MUL}} \end{align*} \end{empheqboxed} - Fairly complex, right? Let us split the function into three shards $\textcolor{red!80!white}{f_1}$, $\textcolor{blue}{f_2}$, and $\textcolor{green!60!black}{f_3}$: + Fairly complex, right? Let us split the function into three shards + $\textcolor{red!80!white}{f_1}$, $\textcolor{blue}{f_2}$, and + $\textcolor{green!60!black}{f_3}$: \begin{align*} - \textcolor{red!80!white}{f_1}(x,y) = xy(x+y), \quad \textcolor{blue}{f_2}(z) = 5z, \quad \textcolor{green!60!black}{f_3}(w) = w^2 + \textcolor{red!80!white}{f_1}(x,y) = xy(x+y), \quad + \textcolor{blue}{f_2}(z) = 5z, \quad + \textcolor{green!60!black}{f_3}(w) = w^2 \end{align*} - This way, it is fairly easy to see that $f(a,b) = \textcolor{green!60!black}{f_3} \circ \textcolor{blue}{f_2} \circ \textcolor{red!80!white}{f_1}(a,b)$. In turn, in Bitcoin - script we can represent $f$ as $\textcolor{red!80!white}{f_1} \parallel \textcolor{blue}{f_2} \parallel \textcolor{green!60!black}{f_3}$: + This way, it is fairly easy to see that $f(a,b) = + \textcolor{green!60!black}{f_3} \circ \textcolor{blue}{f_2} \circ + \textcolor{red!80!white}{f_1}(a,b)$. In turn, in Bitcoin + script we can represent $f$ as $\textcolor{red!80!white}{f_1} + \parallel \textcolor{blue}{f_2} \parallel \textcolor{green!60!black}{f_3}$: \begin{empheqboxed} \begin{align*} - &\elem{a} \elem{b} \textcolor{red!80!white}{\opcode{\texttt{OP\_2DUP}} \opcode{\texttt{OP\_ADD}} \opcode{\texttt{OP\_MUL}} \opcode{\texttt{OP\_MUL}}} && \textcolor{gray!80!black}{\text{// $xy(x+y)$}} \\ - &\textcolor{blue}{\opcode{\texttt{OP\_DUP}} \opcode{\texttt{OP\_DUP}} \opcode{\texttt{OP\_ADD}} \opcode{\texttt{OP\_DUP}} \opcode{\texttt{OP\_ADD}} \opcode{\texttt{OP\_ADD}}} && \textcolor{gray!80!black}{\text{// $5z$}} \\ - &\textcolor{green!60!black}{\opcode{\texttt{OP\_DUP}} \opcode{\texttt{OP\_MUL}}}&&\textcolor{gray!80!black}{\text{// $w^2$}} + &\elem{a} \elem{b} + \textcolor{red!80!white}{\opcode{\texttt{OP\_2DUP}} + \opcode{\texttt{OP\_ADD}} \opcode{\texttt{OP\_MUL}} + \opcode{\texttt{OP\_MUL}}} && + \textcolor{gray!80!black}{\text{// $xy(x+y)$}} \\ + &\textcolor{blue}{\opcode{\texttt{OP\_DUP}} + \opcode{\texttt{OP\_DUP}} \opcode{\texttt{OP\_ADD}} + \opcode{\texttt{OP\_DUP}} \opcode{\texttt{OP\_ADD}} + \opcode{\texttt{OP\_ADD}}} && \textcolor{gray!80!black}{\text{// $5z$}} \\ + &\textcolor{green!60!black}{\opcode{\texttt{OP\_DUP}} + \opcode{\texttt{OP\_MUL}}}&&\textcolor{gray!80!black}{\text{// $w^2$}} \end{align*} \end{empheqboxed} \end{example} Bad news is that $\mathsf{Decompose}$ function is quite tricky to implement. -Namely, we believe that there are several issues: +Namely, there are several issues: \begin{itemize} - \item Decomposition must be valid, meaning each $f_j$ must be valid itself. + \item \textit{Easy one:} Decomposition must be valid, meaning each + $f_j$ must be valid itself. For example, $f_j$ cannot contain unclosed \texttt{OP\_IF}'s. This issue is easily fixed through a careful implementation of the splitting mechanism: for instance, whenever the number of \texttt{OP\_IF}'s and $\texttt{OP\_NOTIF}$'s is not equal to the number of \texttt{OP\_ENDIF}'s, we continue the current shard until the balance is restored. - \item Despite that each $f_j$ might be small, not necessarily $z_j$ is. In + \item \textit{Fundamental one:} Despite that each $f_j$ might be + small, not necessarily $z_j$ is. In other words, optimizing the size of each $f_j$ does not result in optimizing the size of $z_j$. Moreover, in the further sections, we show that optimizing the size of intermediate states is, in fact, a much more tricky and fundamental issue than optimizing the shards' sizes. In other words, we should find a balance between the size of $f_j$ and the size of $z_j$. - \item Some of $z_j$'s might contain the same repetitive pieces: for example, + \item Some of $z_j$'s might contain the same repetitive pieces: for example, the lookup table for certain algorithms or the number binary/$w$-width decomposition for arithmetic. We believe that there must be an optimal - method to store commitments. + method to store commitments, reducing the cost of storing such pieces. + Further, in \Cref{section:takeaways}, we propose one possible solution to + tackle this issue. \end{itemize} -However, the default version proceeds as follows: suppose our script is of form +However, the default and most basic version proceeds as follows: +suppose our script is of form $f = \{ \elem{s_1} \elem{s_2} \ldots \elem{s_k} \}$ where $\elem{s_j}$ is either an \texttt{OP\_CODE} or an element in the stack (added via, for example, \texttt{OP\_PUSHBYTES}). Then, we start splitting the program from left to right @@ -352,33 +462,61 @@ \subsubsection{Fuzzy Search} The basic version, though, does not guarantee the optimal intermediate stack sizes. One of the proposals to improve the splitting mechanism is to make program automatically choose the optimal size. In other words, we make the -parameter $L$ variable and try to find the optimal $L$ that minimizes the -certain ``metric''. What is this metric? - -Since we want to potentially disprove the equality $z_{j+1} = f_j(z_j)$, the -cost of such disproof is the total size of $z_j$, $z_{j+1}$ and the shard $f_j$. -Denote the size of the script/state by $|\star|$. Then, we want to minimize some -sort of ``average'' of $\alpha(|z_j| + |z_{j+1}|) + |f_j|$. The factor $\alpha$ -is introduced since, besides the cost of storing $z_j$, we also need to -\textit{commit} to these values which, as we will see, significantly increases -the cost of a disprove script. In other words, $\alpha$ is a considerable factor -in practice: currently, our estimate suggests $\alpha \approx 1000$. - -Then, depending on the goal, we might choose different criteria of -``averaging'': -\begin{itemize} - \item \textbf{Maximal size}. Suppose we want to minimize the cost of the - worst-case scenario. Suppose after the launching the splitting mechanism on - the shard size $L$ we get $k_L$ shards $f_{L,1},\dots,f_{L,k_L}$ with - intermediate states $z_{L,0},\dots,z_{L,k_L}$. Then, we choose $L$ to be: +parameter $L$ variable and try to find the optimal $L=\hat{L}$ that +minimizes the +certain ``metric''. What is this metric? More importantly, what exactly are +we trying to eventually minimize? + +The most expensive part of the \textit{BitVM2} flow is the +\texttt{DisproveScript}. +Now, consider the following proposition which specifies how to +calculate the $\texttt{DisproveScript[}j\texttt{]}$ size. + +\begin{proposition} + Suppose the splitting mechanism splitted $f$ into shards $f_1,\dots,f_n$, and + the corresponding intermediate states are $z_0,\dots,z_n$. Then, + the size in bytes + of the $\texttt{DisproveScript[}j\texttt{]}$ can be approximately found as \begin{equation*} - \hat{L} := \argmin_{0 \leq L \leq L_{\max}} \left\{ \max_{ 0 \leq j < k_L } \left\{ \alpha(|z_{L,j}| + |z_{L,j+1}|) + |f_{L,j}| \right\} \right\}\;. + \gamma(|z_j| + |z_{j+1}|) + |f_j|, \end{equation*} + where $\gamma \in \mathbb{R}_{\geq 0}$ is the factor that + represents the weight of the commitment. +\end{proposition} + +\textbf{Reasoning.} Since the \texttt{DisproveScript} potentially +allows the challenger +to prove that $z_{j+1} \neq f_j(z_j)$, the script must store the (a) commitments +to $z_i$ and $z_{i+1}$, and (b) the shard $f_i$ itself. The size of +the commitment +includes the size of signature verification, which is $\gamma$ times the size of +the state (since one needs to sign each limb separately, more on that +in \Cref{sec:lamport-signature}). +Note that $\gamma$ is a considerable factor in practice: +currently, our estimate suggests $\gamma \approx 1000$, so the commitment +storage significantly increases the cost of the +\texttt{DisproveScript} (sometimes even more than the shard $f_i$ itself). + +This motivates us to minimize a certain``average'' of $\gamma(|z_j| + +|z_{j+1}|) + |f_j|$. +Depending on the goal, we might choose different criteria of ``averaging'': +\begin{itemize} + \item \textbf{Maximal size}. Suppose we want to minimize the cost of the + worst-case scenario. Suppose after the launching the splitting mechanism on + the shard size $L$ we get $k_L$ shards $f_{L,1},\dots,f_{L,k_L}$ with + intermediate states $z_{L,0},\dots,z_{L,k_L}$. Then, we choose: + \begin{equation*} + \hat{L} := \argmin_{0 \leq L \leq L_{\max}} \left\{ \max_{ 0 + \leq j < k_L } \left\{ \gamma(|z_{L,j}| + |z_{L,j+1}|) + + |f_{L,j}| \right\} \right\}\;. + \end{equation*} \item \textbf{Average size}. Suppose we want to minimize the average cost of - disproof. Then, we choose $L$ to be: - \begin{equation*} - \hat{L} := \argmin_{0 \leq L \leq L_{\max}} \left\{ \frac{1}{k_L} \sum_{0 \leq j < k_L} \left( \alpha(|z_{L,j}| + |z_{L,j+1}|) + |f_{L,j}| \right) \right\}\;. - \end{equation*} + disproof. Then, we choose $L$ to be: + \begin{equation*} + \hat{L} := \argmin_{0 \leq L \leq L_{\max}} \left\{ + \frac{1}{k_L} \sum_{0 \leq j < k_L} \left( \gamma(|z_{L,j}| + + |z_{L,j+1}|) + |f_{L,j}| \right) \right\}\;. + \end{equation*} \end{itemize} Note, however, that this algorithm is still far from being the most optimal one. @@ -388,46 +526,59 @@ \subsubsection{Fuzzy Search} The ultimate solution would be to check every possible splitting and choose the one that minimizes the cost of disproof. However, this is not feasible in practice, as the number of possible splittings is enormous (even, say, for the -fixed number of shards). +fixed number of shards). For that reason, the best splitting approach +is most likely the manual careful optimization. \subsubsection{Current State} -We implemented the basic splitting mechanism that finds $f_1,\dots,f_k$ of +We implemented both the basic and fuzzy splitting mechanism that +finds $f_1,\dots,f_n$ of almost equal size (which can be specified). It already produces valid shards and intermediate states on all of the following scripts: \begin{itemize} - \item \textbf{Big Integer Addition} (of any bitsize). - \item \textbf{Big Integer Multiplication} (of any bitsize). - \item \textbf{SHA-256} hash function. - \item \textbf{Square Fibonacci Sequence Demo}. - \item \textbf{\texttt{u32} Multiplication}. + \item \textbf{Big Integer Addition} (of any bitsize). + \item \textbf{Big Integer Multiplication} (of any bitsize). In + particular, we implemented the \texttt{u32} multiplication (which + is a ``sorta'' equivalent of disabled \texttt{OP\_MUL}). + \item \textbf{SHA-256} hash function. + \item \textbf{BitVM-friendly Big Integer Multiplication} (of any + bitsize). We will further introduce what ``BitVM-friendliness'' means. + \item \textbf{Square Fibonacci Sequence Demo}. \end{itemize} We will explore the last two functions in more detail a bit later. All the current implementation of test scripts can be found through the link below: \begin{center} - \url{https://github.com/distributed-lab/bitvm2-splitter/tree/main/bitcoin-testscripts} + \url{https://github.com/distributed-lab/nero} \end{center} +\section{Assert Transaction}\label{sec:assert-tx} -\section{Assert Transaction}\label{sec:assert-tx} When the splitting is ready, -the prover $\mathcal{P}$ publishes an \texttt{Assert} transaction, which has one -output with multiple possible spending scenarios: +When the splitting is ready, the prover $\mathcal{P}$ publishes an +\texttt{Assert} transaction, which has one output with multiple +possible spending scenarios: \begin{enumerate} - \item \texttt{PayoutScript} (\texttt{CheckSig} + \texttt{CheckLocktimeVerify} + \texttt{Covenant}) --- the transaction has passed verification, and the operator can spend the output, thereby confirming the statement $y=f(x)$. - \item $\texttt{DisproveScript[\text{$i$}]}$ --- one of the challengers has found a discrepancy in the intermediate states \(z_i\), \(z_{i-1}\) and the sub-program \(f_i\). In other words, they have proven that \(f_i(z_{i-1}) \neq z_i\), and thus, they can spend the output. + \item \texttt{PayoutScript} (\texttt{CheckSig} + + \texttt{CheckLocktimeVerify} + \texttt{Covenant}) --- the transaction + has passed verification, and the operator can spend the output, + thereby confirming the statement $y=f(x)$. + \item $\texttt{DisproveScript[\text{$i$}]}$ --- one of the challengers + has found a discrepancy in the intermediate states \(z_i\), + \(z_{i-1}\) and the sub-program \(f_i\). In other words, they have + proven that \(f_i(z_{i-1}) \neq z_i\), and thus, they can spend the + output. \end{enumerate} While the \texttt{PayoutScript} is rather trivial, we need to specify how the -\texttt{DisproveScript[\text{$i$}]} is constructed. \texttt{DispoveScript} is +\texttt{DisproveScript[\text{$i$}]} is constructed.\ \texttt{DispoveScript} is part of the MAST tree in a Taproot address, allowing the verifier to claim the transaction amount for states \(z_i\), \(z_{i-1}\), and sub-program \(f_i\). We call it \(\texttt{DisproveScript[\text{$i$}]}\) and compose it as follows: \begin{empheqboxed} -\begin{align*} + \begin{align*} \elem{z_i} \elem{z_{i-1}} \elem{f_i} \opcode{OP\_EQUAL} \opcode{OP\_NOT} -\end{align*} + \end{align*} \end{empheqboxed} This script does not need a \texttt{CheckSig}, as with the correct \(z_i\) and @@ -503,18 +654,19 @@ \subsection{Winternitz Signature}\label{sec:lamport-signature} \end{align*} \end{empheqboxed} -where \texttt{OP\_WINTERNITZVERIFY} is the verification of the Winternitz -signature (commitment), described in Bitcoin Script (as Bitcoin Script does not -have a built-in \texttt{OP\_CODE} for Winternitz signatures)\footnote{Its -implementation can be found here: +where \texttt{OP\_WINTERNITZVERIFY} is the verification of the +Winternitz signature (commitment), described in Bitcoin Script (as + Bitcoin Script does not have a built-in \texttt{OP\_CODE} for +Winternitz signatures)\footnote{Its implementation can be found here: \url{https://github.com/distributed-lab/bitvm2-splitter/blob/feature/winternitz/bitcoin-winternitz/src/lib.rs}.}. \subsubsection{Winternitz Signatures in Bitcoin - Script}\label{sec:winternitz-in-bitcoin-script} +Script}\label{sec:winternitz-in-bitcoin-script} The first biggest issue with the provided approach is that the Winternitz Script requires encoding the message $\mathsf{Enc}(m)$, which splits the state into $d$ -digit number. For \textit{BitVM2}, it means encoding each state $z_j$. However, the +digit number. For \textit{BitVM2}, it means encoding each state +$z_j$. However, the arithmetic in Bitcoin Script is limited and contains only basic opcodes such as \texttt{OP\_ADD}. To make matters worse, all the corresponding operations can be applied to 32-bit elements only, and as the last one is reserved for a sign, @@ -522,18 +674,23 @@ \subsubsection{Winternitz Signatures in Bitcoin strong, but most of the math can be implemented through 32-bit stack elements. So lets fix $\ell = 32$ --- maximum size of the stack element in bits. -The first observation is that essentially $z_j$ is a collection of 32-bit numbers (suppose this collection consists of $n_j$ numbers). Denote this fact by $z_j = (u_{j,1}, u_{j,2}, \dots, u_{j, n_j})$ where each $u_{j,k} \in \mathbb{Z}_{2^{\ell}}$. Therefore, one way to implement the message encoding is following: +The first observation is that essentially $z_j$ is a collection of +32-bit numbers (suppose this collection consists of $n_j$ numbers). +Denote this fact by $z_j = (u_{j,1}, u_{j,2}, \dots, u_{j, n_j})$ +where each $u_{j,k} \in \mathbb{Z}_{2^{\ell}}$. Therefore, one way to +implement the message encoding is following: \begin{enumerate} \item Aggregate elements of $z_j$ into a single hash digest $h_j \gets - H(u_{j,1} \parallel u_{j,2} \parallel \dots \parallel u_{j,n_j})$. - \item Use dominant free function $P(h_j)$ as described in~\cite{applied-crypto} to get the decomposition. + H(u_{j,1} \parallel u_{j,2} \parallel \dots \parallel u_{j,n_j})$. + \item Use dominant free function $P(h_j)$ as described + in~\cite{applied-crypto} to get the decomposition. \end{enumerate} However, as of now, the Bitcoin does not have the \texttt{OP\_CAT}, so there is no way we can effectively aggregate the intermediate state $z_j$ into a single stack element. Meaning, we need to create a Winternitz keypair $(\mathsf{pk}_{j,k}, \mathsf{sk}_{j,k})$ for each $u_{j,k}$ where $k \in -\{1,\dots,n_j\}$. +\{1,\dots,n_j\}$. However, there are couple of tricks to make the life easier. First, obviously, it is convenient to choose $d$ such that $d+1=2^w$ for some $w \in \mathbb{N}$. @@ -577,7 +734,7 @@ \subsubsection{Winternitz Signatures in Bitcoin \end{table} Secondly, notice the following fact: \emph{encoding the message $m$ -(essentially, being the number decomposition) is more expensive than decoding + (essentially, being the number decomposition) is more expensive than decoding the message (being the number recovery from limbs)}. In fact, if chunks are of equal lengths, the recovery of a message $m$ from $n_0$ digits can be computed very easily: simply set $m \gets \sum_{i=0}^{n_0} e_i \times 2^{wi}$. Note that @@ -587,7 +744,8 @@ \subsubsection{Winternitz Signatures in Bitcoin \begin{empheqboxed} \begin{align*} - \elem{e_j} \underbrace{\opcode{OP\_DUP} \opcode{OP\_ADD}}_{n \; \text{times}} + \elem{e_j} \underbrace{\opcode{OP\_DUP} \opcode{OP\_ADD}}_{n \; + \text{times}} \end{align*} \end{empheqboxed} @@ -607,22 +765,24 @@ \subsubsection{Winternitz Signatures in Bitcoin So overall, the algorithm to sign the states looks as follows: \begin{enumerate} - \item The prover $\mathcal{P}$ runs the program on all shards $\{f_j\}_{1 \leq - j \leq n}$ to obtain the intermediate states $\{z_j\}_{0 \leq j \leq n}$: - essentially, being the stack after executing \script{\elem{z_{j-1}} - \elem{f_j}}. + \item The prover $\mathcal{P}$ runs the program on all shards + ${\{f_j\}}_{1 \leq + j \leq n}$ to obtain the intermediate states ${\{z_j\}}_{0 \leq j \leq n}$: + essentially, being the stack after executing \script{\elem{z_{j-1}} + \elem{f_j}}. \item $\mathcal{P}$ interprets each $z_j$ as a collection of $n_j$ 32-bit - numbers: $z_j = (u_{j,1}, u_{j,2}, \dots, u_{j, n_j})$. + numbers: $z_j = (u_{j,1}, u_{j,2}, \dots, u_{j, n_j})$. \item $\mathcal{P}$ encodes each $u_{j,k}$ and forms the Winternitz keypairs - $\{(\mathsf{pk}_{j,k},\mathsf{sk}_{j,k})\}_{1 \leq k \leq n_j}$. + ${\{(\mathsf{pk}_{j,k},\mathsf{sk}_{j,k})\}}_{1 \leq k \leq n_j}$. \item Verifier $\mathcal{V}$, when publishing the - \texttt{DisproveScript[\text{$j$}]}, will add the corresponding - \textbf{encoded} states $\mathsf{Enc}(z_j)$, $\mathsf{Enc}(z_{j-1})$, and - corresponding signatures $\sigma_j$ and $\sigma_{j-1}$. + \texttt{DisproveScript[\text{$j$}]}, will add the corresponding + \textbf{encoded} states $\mathsf{Enc}(z_j)$, $\mathsf{Enc}(z_{j-1})$, and + corresponding signatures $\sigma_j$ and $\sigma_{j-1}$. \item The script, in turn, besides the verification of the intermediate states - signatures, will \textbf{recover} the original $u_{j,k}$ elements from the - encoded states and verify the equality $f_j(z_{j-1}) = z_j$ after recovery of - both $z_j$ and $z_{j-1}$. + signatures, will \textbf{recover} the original $u_{j,k}$ elements from the + encoded states and verify the equality $f_j(z_{j-1}) = z_j$ after + recovery of + both $z_j$ and $z_{j-1}$. \end{enumerate} \textbf{Script Size Analysis.} Still, even with optimizations @@ -642,7 +802,7 @@ \subsubsection{Winternitz Signatures in Bitcoin $\mathsf{Enc}(z_j)$ and corresponding signature $\sigma_j$, as each limb can be stored only as a whole byte, is around $N + n_HN = (1+n_H)N$ bytes. So the total size of the largest part of a disprove transaction ---- \texttt{witness}, is roughly $2N(n_H + d) + wn_0^2$. The specific sizes +--- \texttt{witness}, is roughly $2N(n_H + d) + wn_0^2$. The specific sizes for different $d$ can be seen in \Cref{tab:winternitz-script-size}. \iffalse{} @@ -657,16 +817,16 @@ \subsubsection{Winternitz Signatures in Bitcoin n0 = math.ceil(l / w) n1 = math.ceil((2**w * n0).bit_length() / w) k = n0 + n1 - + pk_size = 20 * k ver_size = 2 * d * k sig_size = 21*k rec_size = 0 for i in range(0, n0): rec_size += int(i * w) - + total = pk_size + ver_size + rec_size + sig_size - + print(f"{d} & {sig_size} & {pk_size} & {ver_size} & {rec_size} & {total} \\\\") \end{verbatim} \fi @@ -674,7 +834,8 @@ \subsubsection{Winternitz Signatures in Bitcoin \centering \begin{tabular}{cccccc} \toprule - $d$ & \textbf{Signature} & \textbf{Public Key} & \textbf{Verification Script} & \textbf{Recovery Script} & \textbf{Total} \\ + $d$ & \textbf{Signature} & \textbf{Public Key} & + \textbf{Verification Script} & \textbf{Recovery Script} & \textbf{Total} \\ \midrule 3 & 420 & 400 & 120 & 240 & 1180 \\ 7 & 294 & 280 & 196 & 165 & 935 \\ @@ -687,11 +848,11 @@ \subsubsection{Winternitz Signatures in Bitcoin \end{tabular} \caption{Different script sizes depending on the $d$ value per each 32-bit message. Note that, ``Signature'' column includes the - encoding of 32-bit message in it.}\label{tab:winternitz-script-size} + encoding of 32-bit message in it.}\label{tab:winternitz-script-size} \end{table} \subsubsection{Compact Winternitz commitment - scheme in Bitcoin}\label{sec:compact-winternitz-in-bitcoin} +scheme in Bitcoin}\label{sec:compact-winternitz-in-bitcoin} As we mentioned in prevoius section, the most contribution to script size comes from public key, signature, verification and recovery, but @@ -707,7 +868,11 @@ \subsubsection{Compact Winternitz commitment instead, we propose \textit{skipping} only the most significant \textit{zero} limbs of a stack element. -This makes the recovery script size equal to zero in the best-case scenario. The public key and signature hash digest number ranges from $1 + n_1$ in the best case to $N$ in the worst. The same applies to the total verification script size, as fewer checks are required for smaller public keys. +This makes the recovery script size equal to zero in the best-case +scenario. The public key and signature hash digest number ranges from +$1 + n_1$ in the best case to $N$ in the worst. The same applies to +the total verification script size, as fewer checks are required for +smaller public keys. \iffalse{} %The python script i used for this table: @@ -721,16 +886,16 @@ \subsubsection{Compact Winternitz commitment for z in range(1, 8+1): k = z + n1 - + pk_size = 20 * k ver_size = 2 * d * k sig_size = 21*k rec_size = 0 for i in range(0, z): rec_size += int(i * w) - + total = pk_size + ver_size + rec_size + sig_size - + print(f"{z} & {sig_size} & {pk_size} & {ver_size} & {rec_size} & {total} \\\\") \end{verbatim} \fi @@ -738,7 +903,8 @@ \subsubsection{Compact Winternitz commitment \centering \begin{tabular}{cccccc} \toprule - \textbf{Non-zero limbs} & \textbf{Signature} & \textbf{Public Key} & \textbf{Verification} & \textbf{Recovery} & \textbf{Total} \\ + \textbf{Non-zero limbs} & \textbf{Signature} & \textbf{Public + Key} & \textbf{Verification} & \textbf{Recovery} & \textbf{Total} \\ \midrule 1 & 63 & 60 & 90 & 0 & 213 \\ 2 & 84 & 80 & 120 & 4 & 288 \\ @@ -752,22 +918,25 @@ \subsubsection{Compact Winternitz commitment \end{tabular} \caption{Different number of possible script sizes for $d = 15$ depending on the the number of non-zero - limbs.}\label{tab:winternitz-script-size} + limbs.}\label{tab:winternitz-script-size} \end{table} \subsection{Disprove Script Specification} All in all, the $\texttt{DisproveScript[\text{$j$}]}$ is formed as follows: \begin{itemize} - \item \textbf{Witness:} $\Big\{\mathsf{Enc}(z_{j+1})$, $\sigma_{j+1}$, $\mathsf{Enc}(z_{j})$, $\sigma_j\Big\}$. + \item \textbf{Witness:} $\Big\{\mathsf{Enc}(z_{j+1})$, + $\sigma_{j+1}$, $\mathsf{Enc}(z_{j})$, $\sigma_j\Big\}$. \item \textbf{Spending Condition}: - \begin{empheqboxed} - \begin{align*} - &\elem{\mathsf{pk}_j} \opcode{\texttt{OP\_WINTERNITZVERIFY} } \opcode{\texttt{OP\_RESTORE}} \opcode{\texttt{OP\_TOALTSTACK}} \\ - &\elem{\mathsf{pk}_{j}} \opcode{\texttt{OP\_WINTERNITZVERIFY} } \opcode{\texttt{OP\_RESTORE}} \opcode{\texttt{FROM\_TOALTSTACK}} \\ - &\elem{f_j} \opcode{ \texttt{OP\_EQUAL} } \opcode{\texttt{OP\_NOT}} - \end{align*} - \end{empheqboxed} + \begin{empheqboxed} + \begin{align*} + &\elem{\mathsf{pk}_j} \opcode{\texttt{OP\_WINTERNITZVERIFY} } + \opcode{\texttt{OP\_RESTORE}} \opcode{\texttt{OP\_TOALTSTACK}} \\ + &\elem{\mathsf{pk}_{j}} \opcode{\texttt{OP\_WINTERNITZVERIFY} + } \opcode{\texttt{OP\_RESTORE}} \opcode{\texttt{FROM\_TOALTSTACK}} \\ + &\elem{f_j} \opcode{ \texttt{OP\_EQUAL} } \opcode{\texttt{OP\_NOT}} + \end{align*} + \end{empheqboxed} \end{itemize} \textbf{Note on implementation.} One more tricky part is that $z_j$, in fact, is @@ -784,7 +953,8 @@ \subsection{Structure of the MAST Tree in a Taproot The inputs of the \texttt{Assert} transaction spend the output to a Taproot address, which consists of a MAST tree of Bitcoin scripts mentioned in -\Cref{sec:assert-tx}. From the \textit{BitVM2} document, it is known that the first \(n\) +\Cref{sec:assert-tx}. From the \textit{BitVM2} document, it is known +that the first \(n\) scripts in the tree are all \(\texttt{DisproveScript[\text{$i$}]}\), where \(i \in \{1,\dots, n\}\), and the last is a script that allows the operator who published the \texttt{Assert} transaction to spend the output after some time. A @@ -792,55 +962,559 @@ \subsection{Structure of the MAST Tree in a Taproot % Drawing the Figure \tikzset{ - leaf/.style={rectangle, draw=green!90, fill=green!10, very thick, rounded corners}, - root/.style={rectangle, draw=gray!80, fill=gray!20, very thick, rounded corners}, - script/.style={rectangle, draw=blue!80, fill=blue!10, very thick, text width=2.5cm, minimum height=1cm, align=center}, - arrow/.style={thick,-{Stealth[round]},shorten >=2pt,shorten <=2pt} + leaf/.style={rectangle, draw=green!90, fill=green!10, very thick, + rounded corners}, + root/.style={rectangle, draw=gray!80, fill=gray!20, very thick, + rounded corners}, + script/.style={rectangle, draw=blue!80, fill=blue!10, very thick, + text width=2.5cm, minimum height=1cm, align=center}, + arrow/.style={thick,-{Stealth[round]},shorten >=2pt,shorten <=2pt} } \begin{figure}[h] -\centering -\begin{tikzpicture}[node distance=1cm and 2cm] - - % Root node - \node (root) [root] {\textsf{Root}}; - - % Write a text left to root - \node [left=of root, xshift=2.1cm] {$\mathsf{0x000\dots} + G \times$}; - - % Leaf nodes - \node (leaf1) [leaf, below left=of root] {$\mathsf{Leaf}_1$}; - \node (leaf2) [leaf, below right=of root] {$\mathsf{Leaf}_2$}; - - % Script nodes - \node (script1) [script, below=of leaf1, xshift=-1.5cm] {\scriptsize\texttt{DisproveScript[\text{$1$}]}}; - \node (script2) [script, below=of leaf1, xshift=1.5cm] {\scriptsize\texttt{DisproveScript[\text{$2$}]}}; - \node (script3) [script, below=of leaf2, xshift=-1.5cm] {\scriptsize\texttt{DisproveScript[\text{$3$}]}}; - \node (script4) [script, below=of leaf2, xshift=1.5cm] {\scriptsize\texttt{PayoutScript}}; - - % Draw arrows - \draw[arrow] (root) -- (leaf1); - \draw[arrow] (root) -- (leaf2); - \draw[arrow] (leaf1) -- (script1); - \draw[arrow] (leaf1) -- (script2); - \draw[arrow] (leaf2) -- (script3); - \draw[arrow] (leaf2) -- (script4); - -\end{tikzpicture} -\caption{\label{fig:assert-tx-mast-tree}Script tree in a Taproot - address with three sub-programs and two intermediate states. Here, $G$ is the generator point of the elliptic curve.} + \centering + \begin{tikzpicture}[node distance=1cm and 2cm] + + % Root node + \node (root) [root] {\textsf{Root}}; + + % Write a text left to root + \node [left=of root, xshift=2.1cm] {$\mathsf{0x000\dots} + G \times$}; + + % Leaf nodes + \node (leaf1) [leaf, below left=of root] {$\mathsf{Leaf}_1$}; + \node (leaf2) [leaf, below right=of root] {$\mathsf{Leaf}_2$}; + + % Script nodes + \node (script1) [script, below=of leaf1, xshift=-1.5cm] + {\scriptsize\texttt{DisproveScript[\text{$1$}]}}; + \node (script2) [script, below=of leaf1, xshift=1.5cm] + {\scriptsize\texttt{DisproveScript[\text{$2$}]}}; + \node (script3) [script, below=of leaf2, xshift=-1.5cm] + {\scriptsize\texttt{DisproveScript[\text{$3$}]}}; + \node (script4) [script, below=of leaf2, xshift=1.5cm] + {\scriptsize\texttt{PayoutScript}}; + + % Draw arrows + \draw[arrow] (root) -- (leaf1); + \draw[arrow] (root) -- (leaf2); + \draw[arrow] (leaf1) -- (script1); + \draw[arrow] (leaf1) -- (script2); + \draw[arrow] (leaf2) -- (script3); + \draw[arrow] (leaf2) -- (script4); + + \end{tikzpicture} + \caption{\label{fig:assert-tx-mast-tree}Script tree in a Taproot + address with three sub-programs and two intermediate states. Here, + $G$ is the generator point of the elliptic curve.} \end{figure} -\begin{figure}[htbp] +\section{Transactions Graph}\label{sec:txs-graph} + +To implement whole transaction flow as cheap as possible in most +optimistic cases, BitVM2 proposes a graph of transactions which +introduces multiple paths of interaction between prover (operator) and +verifier (comittee): + +\begin{itemize} + \item \texttt{Claim} --- a transaction with the initial statement + $f(x) = y$ assertion without any commitments to intermidiate states. + \item \texttt{OptimisticPayout} --- a transaction that, without any + dispute, prover $\mathcal{P}$ uses to spend the asserted in \texttt{Claim} + amount. + \item \texttt{Challenge} --- after publishing the \texttt{Challenge} + transactions, the \texttt{OptimisticPayout} dispute resolving is + blocked, so operator \textbf{MUST} publish the \texttt{Assert} + transaction. + \item \texttt{Assert} --- the transaction that publishes all commitments + $\sigma_1, \ldots, \sigma_n$ to intermidiate states + $z_1, \ldots, z_n$ and opens the ability for verifier + $\mathcal{V}$ to punish the + prover $\mathcal{P}$ if program execution was invalid. + \item \texttt{Disprove} --- a transaction that spends \texttt{Assert} + one if one of the program chunks published by verifier + $\mathcal{V}$ invalid. + \item \texttt{Payout} --- a transaction that returns from + \texttt{Assert} one staked amount back to prover $\mathcal{P}$ and is + spendable after some time if verifier $\mathcal{V}$ can't find an invalid + program chunk. +\end{itemize} + +To emulate covenants out implementations uses interactive +multisignature Schnorr based scheme Musig2~\cite{musig2}. Highlevel +view of the transactions graph can be seen in the +Figure~\ref{fig:txs-graph}. But we'll disscuss each transaction in +detail in next sections. + +\tikzset{ + claimtx/.pic = { + % Draw the transaction box + \node[rounded corners, minimum width=3.5cm, minimum height=2.5cm, + fill=gray!30, thick, draw=gray] + (cltxbody) at (0,0) {}; + % Draw the label of tx + \node[above] at (cltxbody.north) {\texttt{Claim}}; + % Draw the input + \node[rounded corners, minimum width=2cm, minimum height=1cm, + fill=green!30, thick, draw=green] + (input) at (-1.2,0) {$*, x$ ($d$ \bitcoin)}; + \node[left] at (input.west) {$\mathcal{P}$}; + % Draw the outputs + \node[rounded corners, minimum width=2cm, minimum height=1cm, + fill=blue!30, thick, draw=blue] + (cloutput1) at (1.2,0.2) {$d$ \bitcoin}; + \node[rounded corners, minimum width=2cm, minimum height=1cm, + fill=blue!30, thick, draw=blue] + (cloutput2) at (1.2,-0.9) {0.00001 \bitcoin}; + }, + challengetx/.pic = { + % Draw the transaction box + \node[rounded corners, minimum width=3.5cm, minimum height=2cm, + fill=gray!30, thick, draw=gray] + (chtxbody) at (0,0) {}; + % Draw the label of tx + \node[above] at (chtxbody.north) {\texttt{Challenge}}; + % Draw the input + \node[rounded corners, minimum width=2cm, minimum height=1cm, + fill=green!30, thick, draw=green] + (chinput1) at (-1.5,0.2) {$\sigma_{\mathcal{P}}$ (0.00001 \bitcoin)}; + \node[rounded corners, minimum width=2cm, minimum height=1cm, + fill=green!30, thick, draw=green] + (input) at (-1.5,-1) {$*$ ($c$ \bitcoin)}; + % show that this input is crowdfunded + \node[left] at (input.west) {\texttt{Crowdfunded}}; + % Draw the outputs + \node[rounded corners, minimum width=2cm, minimum height=1cm, fill=blue!30] + (output) at (1.5,0.2) {$c$ \bitcoin}; + \node[right] at ($(output.east) + (0.2,0)$) {$\mathcal{P}$}; + + \draw[dashed, thick, rounded corners] (-2.8,-0.3) rectangle (2.5,0.7); + % label that this is SINGLE|ANYONECANPAY + \node[below] at (2.6,-0.35) {\texttt{SINGLE|ANYONECANPAY}}; + }, + payoutoptimistictx/.pic = { + % Draw the transaction box + \node[rounded corners, minimum width=3.5cm, minimum height=2cm, + fill=gray!30, thick, draw=gray] + (optxbody) at (0,0) {}; + % Draw the label of tx + \node[above] at (optxbody.north) {\texttt{OptimisticPayout}}; + % Draw the input + \node[rounded corners, minimum width=2cm, minimum height=1cm, + fill=green!30, thick, draw=green] + (opinput1) at (-1.5,0.2) {$\sigma_{\mathcal{V} + \mathcal{P}}$ + ($d$ \bitcoin)}; + \node[rounded corners, minimum width=2cm, minimum height=1cm, + fill=green!30, thick, draw=green] + (opinput2) at (-1.5,-0.9) {$\sigma_{\mathcal{P}}$ (0.00001 \bitcoin)}; + % Draw output + \node[rounded corners, minimum width=2cm, minimum height=1cm, + fill=blue!30, thick, draw=blue] + (opoutput1) at (1.5,0.2) {$d$ \bitcoin}; + \node[right] at (opoutput1.east) {$\mathcal{P}$}; + }, + asserttx/.pic = { + % Draw the transaction box + \node[rounded corners, minimum width=4.5cm, minimum height=2cm, + fill=gray!30, thick, draw=gray] + (astxbody) at (0,0) {}; + % Draw the label of tx + \node[above] at (astxbody.north) {\texttt{Assert}}; + % Draw the input + \node[rounded corners, minimum width=2cm, minimum height=1cm, + fill=green!30, thick, draw=green] + (asinput) at (-1.3,0.2) {$\sigma_{\mathcal{V} + \mathcal{P}}; + \sigma_1, \ldots, \sigma_n$ ($d$ \bitcoin)}; + % Draw the outputs + \node[rounded corners, minimum width=2cm, minimum height=1cm, + fill=blue!30, thick, draw=blue] + (asoutput) at (1.8,0.2) {$d$ \bitcoin}; + }, + payouttx/.pic = { + % Draw the transaction box + \node[rounded corners, minimum width=3.5cm, minimum height=1.5cm, + fill=gray!30, thick, draw=gray] + (ptxbody) at (0,0) {}; + % Draw the label of tx + \node[above] at (ptxbody.north) {\texttt{Payout}}; + % Draw the input + \node[rounded corners, minimum width=2cm, minimum height=1cm, + fill=green!30, thick, draw=green] + (pinput) at (-1.5,0) {$\sigma_{\mathcal{V} + \mathcal{P}}; + \sigma_{\mathcal{P}}$ ($d$ \bitcoin)}; + % Draw output + \node[rounded corners, minimum width=2cm, minimum height=1cm, + fill=blue!30, thick, draw=blue] + (poutput) at (1.5,0) {$d$ \bitcoin}; + \node[right] at (poutput.east) {$\mathcal{P}$}; + }, + disprovetx/.pic = { + % Draw the transaction box + \node[rounded corners, minimum width=5cm, minimum height=2cm, + fill=gray!30, thick, draw=gray] + (dtxbody) at (0,0) {}; + % Draw the label of tx + \node[above] at (dtxbody.north) {\texttt{Disprove[#1]}}; + % Draw the input + \node[rounded corners, minimum width=2cm, minimum height=1cm, + fill=green!30, thick, draw=green] + (dinput) at (-1.5,0.2) + {$\sigma_{\mathcal{V} + \mathcal{P}}; \sigma_{#1}, + \sigma_{\the\numexpr#1+1\relax}, z_{#1}, + z_{\the\numexpr#1+1\relax}$ ($d$ \bitcoin)}; + % Draw output + \node[rounded corners, minimum width=2cm, minimum height=1cm, fill=blue!30] + (doutput1) at (2.5,0.2) {$b$ \bitcoin}; + \node[right] at (doutput1.east) {\texttt{Burn}}; + \node[rounded corners, minimum width=2cm, minimum height=1cm, + fill=blue!30, thick, draw=blue] + (doutput2) at (2.5,-0.9) {$a$ \bitcoin}; + \node[right] at (doutput2.east) {$\mathcal{V}$}; + + \draw[dashed, thick, rounded corners] (-3.5,-0.3) rectangle (3.5,0.7); + % label that this is SINGLE|ANYONECANPAY + \node[below] at (-2.5,-0.35) {\texttt{SINGLE}}; + }, + disproventx/.pic = { + % Draw the transaction box + \node[rounded corners, minimum width=5cm, minimum height=2cm, + fill=gray!30, thick, draw=gray] + (dtxbody) at (0,0) {}; + % Draw the label of tx + \node[above] at (dtxbody.north) {\texttt{Disprove[n-1]}}; + % Draw the input + \node[rounded corners, minimum width=2cm, minimum height=1cm, + fill=green!30, thick, draw=green] + (dinput) at (-1.5,0.2) + {$\sigma_{\mathcal{V} + \mathcal{P}}; \sigma_{n-1}, \sigma_{n}, + z_{n-1}, z_{n}$ ($d$ \bitcoin)}; + % Draw output + \node[rounded corners, minimum width=2cm, minimum height=1cm, fill=blue!30] + (doutput1) at (2.5,0.2) {$b$ \bitcoin}; + \node[right] at (doutput1.east) {\texttt{Burn}}; + \node[rounded corners, minimum width=2cm, minimum height=1cm, + fill=blue!30, thick, draw=blue] + (doutput2) at (2.5,-0.9) {$a$ \bitcoin}; + \node[right] at (doutput2.east) {$\mathcal{V}$}; + + \draw[dashed, thick, rounded corners] (-3.9,-0.3) rectangle (3.5,0.7); + % label that this is SINGLE|ANYONECANPAY + \node[below] at (-2.5,-0.35) {\texttt{SINGLE}}; + }, +} + +\begin{figure}[hb] \centering - \includegraphics[width=.9\linewidth]{../images/bitvm-txs.png} - \caption{\label{fig:bitvm-txs}Sequance of transactions in \textit{BitVM2} - with 3 subprograms and 2 intermediate states.} + \begin{tikzpicture}[scale=0.45, transform shape] + \pic [at={(0,0)}] {claimtx}; + + % create a spending branches as a romb + \node[rounded corners, fill=black, minimum width=1cm, minimum + height=1cm, text=white] + (cloutput2spending) at ($(cloutput2.east) + (2.5, 0)$) + {\texttt{P2TR (key-path)}}; + + % connect Claim challenge output and spending branch + \draw[-{Stealth[length=2mm]},thick] (cloutput2) -- (cloutput2spending); + + % add Challenge tx + \pic [at={($(cloutput2spending.east) + (4.5,-0.45)$)}] {challengetx}; + % connect Claim output and Challenge input + \draw[-{Stealth[length=2mm]},red,thick] + ($(cloutput2spending.east) - (0,0.25)$) -- (chinput1.west); + + % Add branch for optimistic payout and assert tx + \node[rounded corners, fill=black, minimum width=1cm, minimum + height=1cm, text=white] + (cloutput1spending) at ($(cloutput1.east) + (2.5,0)$) + {\texttt{P2TR (script-path)}}; + % connect Claim output and Spending branch + \draw[-{Stealth[length=2mm]},thick] (cloutput1) -- (cloutput1spending); + + % Add Optimistic Payout Script box + \node[rounded corners, fill=blue!70, rotate=90, minimum + width=1cm, minimum height=1cm, text=white, thick, draw=blue] + (clopscript) at ($(cloutput1spending.north) + (-0.75,3)$) + {\texttt{OptimisticPayoutScript}}; + % connect spending branch and Optimistic Payout Script + \draw[-,blue,thick] ($(cloutput1spending.north) + (-0.75,0)$) -- + (clopscript.west); + + % Add AssertScript box + \node[rounded corners, fill=red!50, rotate=90, minimum width=1cm, + minimum height=1cm, thick, draw=red] + (classertscript) at ($(cloutput1spending.north) + (+0.75,2)$) + {\texttt{AssertScript}}; + % connect spending branch and Assert Script + \draw[-,red,thick] ($(cloutput1spending.north) + (+0.75,0)$) -- + (classertscript.west); + + + % draw an optimistic payout transaction + \pic [at={(14,9)}] {payoutoptimistictx}; + + % draw vertical line to connect Claim challenge output and + % Optimistic Payout input + \draw[-,blue,thick] + ($(cloutput2spending.east) - (0,-0.25)$) -- + ($(cloutput2spending.east) - (-0.6, -0.25)$); + \draw[-,blue,thick] + ($(cloutput2spending.east) - (-0.6, -0.25)$) + -- ({$(cloutput2spending.east) - (-0.6, -0.25)$} |- {opinput2.west}); + \draw[-{Stealth[length=2mm]},blue,thick] + ({$(cloutput2spending.east) - (-0.6, -0.25)$} |- {opinput2.west}) + -- (opinput2.west); + + + % draw vertical line from payout optimistic script to later + % Optimistic Payout input + \draw[-,blue,thick] + ($(clopscript.east)$) -- (clopscript.east |- opinput1.west); + \draw[-{Stealth[length=2mm]},blue,thick] (clopscript.east |- opinput1.west) + -- node[above] {After Timelock $\Delta_{B}$} (opinput1.west); + + % draw Assert tx + \pic [at={(chtxbody |- (0,5))}] {asserttx}; + + % draw vertical line to connect AssertScript and Assert input + \draw[-,red,thick] + ($(classertscript.east)$) -- (classertscript.east |- asinput.west); + \draw[-{Stealth[length=2mm]},red,thick] (classertscript.east |- + asinput.west) -- (asinput.west); + + % draw branch for Assert output + \node[rounded corners, fill=black, minimum width=1cm, minimum + height=1cm, text=white] + (asoutputspending) at ($(asoutput.east) + (2.5,0)$) {\texttt{P2TR + (script-path)}}; + + % connect Assert output and Spending branch + \draw[-{Stealth[length=2mm]},thick] (asoutput) -- (asoutputspending); + + % draw payout script under spending branch for Assert output with + % small offset + \node[rounded corners, fill=blue!40, minimum width=1cm, minimum + height=1cm, text=white, thick, draw=blue] + (payoutscript) at ($(asoutputspending.south) + (2,-1)$) + {\texttt{PayoutScript}}; + % connect spending branch and Payout Script + \draw[-,blue!40,thick] ($(asoutputspending.south) - (0.9, 0)$) + -- ({$(asoutputspending.south) - (0.9, 0)$} |- {payoutscript.west}); + \draw[-{Stealth[length=2mm]},blue!40,thick] + ({$(asoutputspending.south) - (0.9, 0)$} |- {payoutscript.west}) + -- (payoutscript.west); + + % draw disprove scripts under spending branch for Assert output + % with small offset + \node[rounded corners, fill=red!40, minimum width=1cm, minimum + height=1cm, thick, draw=red] + (dscript1) at ($(payoutscript.south) - (0, 0.6)$) + {\texttt{DisproveScript[1]}}; + + \node[rounded corners, fill=red!40, minimum width=1cm, minimum + height=1cm, thick, draw=red] + (dscript2) at ($(dscript1.south) - (0, 0.6)$) {\texttt{DisproveScript[2]}}; + + \node[rounded corners, fill=red!40, minimum width=1cm, minimum + height=1cm, thick, draw=red] + (dscriptskip) at ($(dscript2.south) - (0, 0.6)$) {$\ldots$}; + + \node[rounded corners, fill=red!40, minimum width=1cm, minimum + height=1cm, thick, draw=red] + (dscriptn) at ($(dscriptskip.south) - (0, 0.6)$) + {\texttt{DisproveScript[n]}}; + + % connect branch and disprove scripts + \draw[-,red!40,thick] ($(asoutputspending.south) - (1.3, 0)$) + -- ({$(asoutputspending.south) - (1.3, 0)$} |- {dscriptn.west}); + \draw[-{Stealth[length=2mm]},red!40,thick] + ({$(asoutputspending.south) - (1.3, 0)$} |- {dscriptn.west}) + -- (dscriptn.west); + -- (dscriptskip.west); + \draw[-{Stealth[length=2mm]},red!40,thick] + ({$(asoutputspending.south) - (1.3, 0)$} |- {dscript2.west}) + -- (dscript2.west); + \draw[-{Stealth[length=2mm]},red!40,thick] + ({$(asoutputspending.south) - (1.3, 0)$} |- {dscript1.west}) + -- (dscript1.west); + + % draw payout tx on the same level as Optimistic Payout + \pic [at={($(optxbody) + (10,0)$)}] {payouttx}; + % connect payout script and payout tx + \draw[-,blue!40,thick] (payoutscript.east) -- + ($(payoutscript.east) + (1,0)$); + \draw[-,blue!40,thick] ($(payoutscript.east) + (1,0)$) + -- node[midway, above, sloped] {After timelock $\Delta_{A}$} + ({$(payoutscript.east) + (1,0)$} + |- {pinput.west}); + \draw[-{Stealth[length=2mm]},blue!40,thick] + ({$(payoutscript.east) + (1,0)$} |- {pinput.west}) + -- (pinput.west); + + % draw disprove txs + \pic [at={($(ptxbody) + (1,-2.5)$)}] {disprovetx={1}}; + % draw line from disprove script to disprove tx input + \draw[-,red!40,thick] (dscript1.east) -- ($(dscript1.east) + (0.9,0)$); + \draw[-,red!40,thick] ($(dscript1.east) + (0.9,0)$) + -- ({$(dscript1.east) + (0.9,0)$} |- {dinput.west}); + \draw[-{Stealth[length=2mm]},red!40,thick] ({$(dscript1.east) + + (0.9,0)$} |- {dinput.west}) + -- (dinput.west); + + \pic [at={($(dtxbody) + (0,-3)$)}] {disprovetx={2}}; + \draw[-,red!40,thick] (dscript2.east) -- ($(dscript2.east) + (1.1,0)$); + \draw[-,red!40,thick] ($(dscript2.east) + (1.1,0)$) + -- ({$(dscript2.east) + (1.1,0)$} |- {dinput.west}); + \draw[-{Stealth[length=2mm]},red!40,thick] ({$(dscript2.east) + + (1.1,0)$} |- {dinput.west}) + -- (dinput.west); + + \node[rounded corners, minimum width=3cm, minimum height=1cm, + fill=gray!30, thick, draw=gray] + (dtxbody) at ($(dtxbody) + (0,-3)$) {\ldots}; + + \pic [at={($(dtxbody) + (0,-3)$)}] {disproventx}; + \draw[-,red!40,thick] (dscriptn.east) -- ($(dscriptn.east) + (0.8,0)$); + \draw[-,red!40,thick] ($(dscriptn.east) + (0.8,0)$) + -- ({$(dscriptn.east) + (0.8,0)$} |- {dinput.west}); + \draw[-{Stealth[length=2mm]},red!40,thick] ({$(dscriptn.east) + + (0.8,0)$} |- {dinput.west}) + -- (dinput.west); + \end{tikzpicture} + \caption[BitVM2 Transaction Graph]{BitVM2 transaction graph + p implementation in \nero. \textcolor{blue}{Blue} --- is + \texttt{OptimisticPayout} flow, \textcolor{red}{red} --- is start of + the dispute with \texttt{Challenge} and \texttt{Assert} + transactions, \textcolor{blue!30}{violet} --- is \texttt{Payout} + flow, and \textcolor{red!30}{pink} --- is prover punishment. + $\sigma_{\mathcal{P}}$ --- is provers Schnorr signature, + $\sigma_{\mathcal{P} + \mathcal{V}}$ --- is Musig2 multisignature + of both prover and + verifier, $\sigma_1, \ldots, \sigma_n$ --- are Winternitz + commitments (signatures) + on intermidiate states $z_1, \ldots, z_n$, $d$ --- staked by prover + amount, $c$ --- crowdfunded amount for prover to publish Assert tx, + $b$ --- configurable burn amount ($b < d$), $a$ --- reward for + punishing prover to verifier}\label{fig:txs-graph} \end{figure} -\section{Exploring BitVM2 Potential using Toy Examples}\label{sec:covenants-emulation} +\subsection{Claim}\label{sec:claim-tx} -Finally, in this section, we explore the potential of \textit{BitVM2} using some toy +As mentioned earlier, the \texttt{Claim} transaction is the first +transaction in the graph. It is used to state the initial assertion +$f(x) = y$ by publishing $x$ in witness of first input (as $f$ is +assumed known by both parties at start of a session). If +$y \in \{\mathsf{True}, \mathsf{False}\}$, $\mathsf{True}$ is assumed as +expected value, otherwise both $x$ and $y$ are published. + +Claim tranasctions has two outputs: + +\begin{itemize} + \item \texttt{ClaimAssertOutput} --- \texttt{P2TR} address with two + alternative spending conditions and unspendable inner key: + \begin{itemize} + \item \texttt{OptimisticPayoutScript} --- checks that timelock + $\Delta_B$ passed and expects aggregated Musig2 signature from both + parties. + \begin{empheqboxed} + \begin{align*} + &\elem{\Delta_B} \opcode{OP\_CHECKSEQUANCEVERIFY} \\ + &\elem{\mathsf{pk}_{\mathcal{P} + \mathcal{V}}} + \opcode{OP\_CHECKSIG} + \end{align*} + \end{empheqboxed} + + where $\mathsf{pk}_{\mathcal{P} + \mathcal{V}}$ --- Musig2 + aggregated Schnorr + $X$-only public key, $\Delta_B$ --- timelock in blocks (the default one + is around two weeks). + \item \texttt{AssertScript} --- checks Winternitz commitments + $\sigma_1, \ldots, \sigma_n$ for intermidiate states $z_1, \ldots, z_n$. + + \begin{empheqboxed} + \begin{align*} + & \elem{\mathsf{pk}_{\mathcal{P} + \mathcal{V}}} + \opcode{OP\_CHECKSIGVERIFY} \\ + & \elem{\mathsf{pk}_n} \opcode{OP\_WINTERNITZVERIFY} + \opcode{OP\_DROP} \\ + & \elem{\mathsf{pk}_{n-1}} \opcode{OP\_WINTERNITZVERIFY} + \opcode{OP\_DROP} \\ + & \ldots \\ + & \elem{\mathsf{pk}_0} \opcode{OP\_WINTERNITZVERIFY} + \end{align*} + \end{empheqboxed} + where $\mathsf{pk}_{\mathcal{P} + \mathcal{V}}$ --- Musig2 + aggregated Schnorr + $X$-only public key, $\mathsf{pk}_0, \ldots, \mathsf{pk}_n$ --- + Winternitz public keys for verification of commitments on + intermidiate states. + \end{itemize} + \item \texttt{ClaimChallengeOutput} --- \texttt{P2TR} output with + \textit{key-path} only spent by \texttt{Challenge} or + \texttt{OptimisticPayout} transactions. +\end{itemize} + +\subsection{Optimistic Payout}\label{sec:optimistic-payout-tx} + +The \texttt{OptimisticPayout} transaction is published when timelock +$\Delta_B$ passed and nobody started dispute by publishing +\texttt{Challenge} one, so prover returns staked BTCs back. Note that +the \texttt{OptimisticPayout} is valid only when none of the +\texttt{Challenge} or \texttt{Assert} transactions were published (see +Figure~\ref{fig:txs-graph}). + +\texttt{OptimisticPayout} spends both outputs of \texttt{Claim}, for +the first output by \texttt{Optimistic PayoutScript} and using provers +Taproot signature for the second one. + +\subsection{Challenge}\label{sec:challenge-tx} + +By publishing \texttt{Challenge} transaction verifier starts dispute, +so the \texttt{OptimisticPayout} flow is blocked and the only way to +return provers funds back is by publishing the \texttt{Assert} +tranasction. + +As total fee to pay for both \texttt{Assert} and \texttt{Disprove} +transactions is large enough to bankrupt prover in most cases, the +verifier and/or any other external source can help prover by +crowdfunding BTCs in \texttt{Challenge}. That's why \texttt{Challenge} +is signed with both \texttt{SINGLE} and \texttt{ANYONECANPAY}, so +other inputs can be added. + +Output amount $c$ in na\"\i{}ve approach is calculated as: + +\[ + c = \mathsf{feerate} \cdot (w_{\mathsf{Assert}} + \max_{i \in \{1, + \ldots, n\}}{w_{\mathsf{Disprove[i]}}}) +\] + +where $w_{\mathsf{Assert}}$ and $w_{\mathsf{Disprove[i]}}$ are weights +of \texttt{Assert} and \texttt{Disprove} transactions respectively. + +\subsection{Assert, Payout and +Disprove}\label{sec:assert-payout-disprove-txs} + +They are discussed in detail in Section~\ref{sec:assert-tx}. The only +added thing, that both scripts in \texttt{Assert} require aggregated +by both parties Musig2 signatures for covenants emulation, so +\texttt{Payout} and \texttt{Disprove} transaction path them to +fullfill this requirement. + +\texttt{Disprove} transaction is signed with \texttt{SINGLE} flag, so +verifiers can add their's outputs to it and receive +$a = d - b- \mathsf{fee}$ difference as a reward for punishment +malicious prover. Also, \texttt{Disprove} uses technic called +``Child-Pays-For-Parent'' which lets \texttt{Disprove} transaction to +spend left Bitcoins as fee for parent \texttt{Assert} and +\texttt{Claim} transactions if need so. + +\section{Exploring BitVM2 Potential using Toy +Examples}\label{sec:covenants-emulation} + +Finally, in this section, we explore the potential of \textit{BitVM2} +using some toy +Finally, in this section, we explore the potential and main problems +of \textit{BitVM2} using some toy examples. We will consider the following functions: \begin{itemize} \item \textbf{\texttt{u32} Multiplication} --- a function that @@ -849,10 +1523,12 @@ \section{Exploring BitVM2 Potential using Toy Examples}\label{sec:covenants-emul calculates the $n$-th element of the square Fibonacci sequence. \end{itemize} -We will demonstrate that the current implementation of \textit{BitVM2} and current +We will demonstrate that the current implementation of +\textit{BitVM2} and current approach to writing Mathematics (finite field arithmetic, elliptic curve operations etc.) cannot handle even the first example. Based on the second -example, we will show that with the appropriate ideology, the \textit{BitVM2} can still +example, we will show that with the appropriate ideology, the +\textit{BitVM2} can still be used to verify the execution of complex programs, but written in a different way. We call such functions as \textbf{BitVM-friendly functions}. \subsection{u32 Multiplication} @@ -872,40 +1548,43 @@ \subsubsection{Implementation Notes} integer multiplication in Bitcoin Script. Essentially, the Bitcoin script utilizes the double-and-add method, commonly used for elliptic curve arithmetic. The idea is following: we can first decompose one of the integers to the binary -form (say, $y=(y_0,\dots,y_{N-1})_2$ where $N$ is the bitsize of $y$). Next, we +form (say, $y={(y_0,\dots,y_{N-1})}_2$ where $N$ is the bitsize of +$y$). Next, we iterate through each bit and on each step, we double the temporary variable and add it to the result if the corresponding bit in $y$ is $1$. The concrete algorithm is described in \Cref{alg:double_and_add}. \begin{algorithm} - \caption{Double-and-add method for integer multiplication}\label{alg:double_and_add} - \Input{$x,y$ --- two \texttt{u32} integers being multiplied, $N$ --- bitsize of $y$.} + \caption{Double-and-add method for integer + multiplication}\label{alg:double_and_add} + \Input{$x,y$ --- two \texttt{u32} integers being multiplied, $N$ + --- bitsize of $y$.} \Output{Result of the multiplication $x \times y$} - - Decompose $y$ to the binary form: $(y_0,y_1,\dots,k_{N-1})_2$ - + + Decompose $y$ to the binary form: ${(y_0,y_1,\dots,k_{N-1})}_2$ + $r \gets 0$ - + $t \gets x$ - + \For{$i \in \{0,\dots,N-1\}$}{ - \If{$y_i = 1$}{ - $r \gets r + t$ - } - - $t \gets 2 \times t$ + \If{$y_i = 1$}{ + $r \gets r + t$ + } + + $t \gets 2 \times t$ } - + \Return{Integer $r$} - + \end{algorithm} Note that implementing long addition and doubling in Bitcoin Script is quite cheap, so algorithm turns out to be relatively efficient --- you can read more -in our recently published paper \cite{w-width-mul}, where we analyze various +in our recently published paper~\cite{w-width-mul}, where we analyze various strategies of big integer multiplication. In our particular case, we assume that \texttt{u32} is just a special case of the big integer with the total bitsize of -$N=32$. +$N=32$. \subsubsection{Split Cost Analysis} @@ -919,7 +1598,8 @@ \subsubsection{Split Cost Analysis} \centering \begin{tabular}{cccc} \toprule - \textbf{Shard number} & \textbf{Shard Size} & \textbf{\# Elements in state} & \textbf{Estimated Commitment Cost} \\ + \textbf{Shard number} & \textbf{Shard Size} & \textbf{\# Elements + in state} & \textbf{Estimated Commitment Cost} \\ \midrule 1 & 623B & 37 & 37kB \\ 2 & 640B & 32 & 32kB \\ @@ -927,11 +1607,14 @@ \subsubsection{Split Cost Analysis} 4 & 640B & 22 & 22kB \\ 5 & 640B & 17 & 17kB \\ 6 & 640B & 12 & 12kB \\ - 7 & 627B & 3 & 3kB \\ + 7 & 627B & 3 & 3kB \\ \bottomrule \end{tabular} - \caption{The result of splitting the program into chunks of approximated size $600B$. The second column represents the shard size $|f_j|$, the third column number of elements in $z_j$ and finally, the last column is the estimated cost of singing $z_j$.} - \label{tab:u32_split} + \caption{The result of splitting the program into chunks of + approximated size $600B$. The second column represents the shard + size $|f_j|$, the third column number of elements in $z_j$ and + finally, the last column is the estimated cost of singing + $z_j$.}\label{tab:u32_split} \end{table} Notice an interesting fact: the cost of a single commitment exceeds the cost of @@ -940,11 +1623,26 @@ \subsubsection{Split Cost Analysis} script of size $37\text{kB}+32\text{kB}+623\text{B} \approx 69.6\text{kB}$! This leads us to the essential conclusion. -\textbf{Takeaway.} \textit{Optimizing the intermediate states representation is crucial for the BitVM2. Even if the program is split into small chunks, the cost of the commitment can still be overwhelming.} +\textbf{Takeaway.} \textit{Optimizing the intermediate states + representation is crucial for the BitVM2. Even if the program is +split into small chunks, the cost of the commitment can still be overwhelming.} -This leads us to the question: can we throw \textit{BitVM2} out of the window due to such +This leads us to the question: can we throw \textit{BitVM2} out of +the window due to such inefficiency and wait for the \texttt{OP\_CAT}? The answer is obviously no (for -what other reason are we writing this paper?). We can still use \textit{BitVM2}, but we +what other reason are we writing this paper?). We can still use +\textit{BitVM2}, but we +\begin{proposition} + Optimizing the intermediate states representation is crucial for the BitVM2. + Even if the program is split into small chunks, the cost of the commitment can + still be overwhelming. +\end{proposition} + +This leads us to the question: can we throw \textit{BitVM2} out of +the window due to such +inefficiency and wait for the \texttt{OP\_CAT}? The answer is obviously no (for +what other reason are we writing this paper?). We can still use +\textit{BitVM2}, but we need to change the way we write the programs. We call such programs \textbf{BitVM-friendly functions}. We provide the first example below. @@ -953,7 +1651,7 @@ \subsection{Square Fibonacci Sequence} input is a pair of elements $(x_0,x_1)$ from the field $\mathbb{F}_q$. For the sake of convenience, we choose $\mathbb{F}_q$ to be the prime field of BN254 curve, which is frequently used for zk-SNARKs. Then, our program $f_n$ consists -in finding the $(n-1)^{\text{th}}$ element in the sequence: +in finding the ${(n-1)}^{\text{th}}$ element in the sequence: \begin{equation*} x_{j+2} = x_{j+1}^2 + x_j^2, \; \text{over $\mathbb{F}_q$.} \end{equation*} @@ -972,50 +1670,57 @@ \subsection{Square Fibonacci Sequence} state transition function can be implemented as: \begin{empheqboxed} \begin{align*} - \opcode{\texttt{FQ::DUP}} \, \opcode{\texttt{Fq::SQUARE}} \elem{2} \opcode{\texttt{Fq::OP\_ROLL}} \, \opcode{\texttt{Fq::SQUARE}} \, \opcode{\texttt{Fq::ADD}} + \opcode{\texttt{FQ::DUP}} \, \opcode{\texttt{Fq::SQUARE}} + \elem{2} \opcode{\texttt{Fq::OP\_ROLL}} \, + \opcode{\texttt{Fq::SQUARE}} \, \opcode{\texttt{Fq::ADD}} \end{align*} \end{empheqboxed} -The size of this transition is roughly \textit{270 kB} and it requires the -storage of 18 elements in the stack, costing additional \textit{18 kB}. So the -rough size of \texttt{DisproveScript} is \textbf{290 kB}, which is a lot, but +The size of this transition is roughly \textit{270kB} and it requires the +storage of 18 elements in the stack, costing additional \textit{18kB}. So the +rough size of \texttt{DisproveScript} is \textbf{290kB}, which is a lot, but still manageable. In turn, consider the function $f_n$, written in Bitcoin script: \begin{empheqboxed} \begin{align*} &\textbf{for} \; i \in \{1,\dots,n\} \; \textbf{do} \\ - & \;\;\;\; \opcode{\texttt{FQ::DUP}} \, \opcode{\texttt{Fq::SQUARE}} \elem{2} \opcode{\texttt{Fq::ROLL}} \, \opcode{\texttt{Fq::SQUARE}} \, \opcode{\texttt{Fq::ADD}} \\ + & \;\;\;\; \opcode{\texttt{FQ::DUP}} \, + \opcode{\texttt{Fq::SQUARE}} \elem{2} \opcode{\texttt{Fq::ROLL}} + \, \opcode{\texttt{Fq::SQUARE}} \, \opcode{\texttt{Fq::ADD}} \\ & \textbf{end} \\ & \opcode{\texttt{Fq::SWAP}} \; \opcode{\texttt{Fq::DROP}} \end{align*} \end{empheqboxed} -For $n=128$, the size is roughly \textbf{35 MB}, which, in contrast, is not at -all manageable. However, the decomposition of the function would make roughly -$n$ scripts, each of size \textbf{290 kB}. +For $n=128$, the size is roughly \textbf{35MB}, which is a lot. However, the +decomposition of the function would make roughly $n$ scripts, each of size +\textbf{290kB}. Additionally, notice that regardless of $n$, the size of the disprove scripts is always the same. Even if we take, say, $n=10000$, making the direct computation cost roughly $2\text{GB}$, we would have 10000 disprove transactions, each of -size \textbf{290 kB}. Moreover, since the cost of storing the disprove scripts +size \textbf{290kB}. Moreover, since the cost of storing the disprove scripts in the Taptree is negligible, \emph{it does not matter how many chunks we split the program into}. -\section{Takeaways and Future Directions} +\section{BitVM2-Friendly Scripts}\label{section:takeaways} -All in all, we believe that, currently, in order to make \textit{BitVM2} practical, the +All in all, we believe that, currently, in order to make +\textit{BitVM2} practical, the whole Groth16 verifier should be written in the \textbf{BitVM-friendly} format. We give an informal definition below. \begin{definition} A function $f$ is called \textbf{BitVM-friendly} if: \begin{itemize} - \item It can be split into the shards $f_1,\dots,f_n$ of relatively small size. - \item The intermediate states $\{z_j\}_{0 \leq j \leq n}$ contain a small number of elements, making the commitment cheap enough. + \item It can be split into the shards $f_1,\dots,f_n$ of + relatively small size. + \item The intermediate states ${\{z_j\}}_{0 \leq j \leq n}$ contain + a small number of elements, making the commitment cheap enough. \end{itemize} This way, the worst-case disprove script would cost $\max_{1 \leq j \leq - n}\left(|f_j| + \alpha(|z_j| + |z_{j-1}|)\right)$ for $\alpha \approx + n}\left(|f_j| + \gamma(|z_j| + |z_{j-1}|)\right)$ for $\gamma \approx 1000$\footnote{This constant, after further optimizations, is subject to change.}. Note that the number of shards almost does not influence the cost since building the larger tree is typically not a problem. @@ -1023,14 +1728,17 @@ \section{Takeaways and Future Directions} It is a question, though, whether such BitVM-friendly function exists for all the Groth16 ingredients. However, we believe that many functions can be -rewritten in such a way. Take, for example, the big integer multiplication. A -great cost of such method is storing the bit decomposition of the number. So, if -we have an $N$-bit integer, the cost of storing the decomposition is roughly -$\alpha N$ (currently, this corresponds to $N \, \text{kB}$). Well, that is a -lot, especially for 254-bit long integers, which are currently used in the -Groth16 verifier. Moreover, there is no efficient way to split the program to -avoid storing the decomposition: you initialize the table at the very beginning -and drop at the very end. +rewritten in such a way. Take, for example, the double-and-add big integer +multiplication\footnote{We acknowledge that current state-of-the-art uses + $w$-width decomposition or Karatsuba Multiplication, but the + problems mentioned +further still persist in these approaches.}. A great cost of such method is +storing the bit decomposition of the number. So, if we have an $N$-bit integer, +the cost of storing the decomposition is roughly $\gamma N$ (currently, this +corresponds to $N \, \text{kB}$). Well, that is a lot, especially for 254-bit +long integers, which are currently used in the Groth16 verifier. Moreover, there +is no efficient way to split the program to avoid storing the decomposition: you +initialize the table at the very beginning and drop at the very end. So how to fix this? The answer is simple: manually construct the script so that at the end of each shard, the decomposition is dropped and $y$ is, therefore, @@ -1043,42 +1751,190 @@ \section{Takeaways and Future Directions} \begin{algorithm} \caption{BitVM-friendly double-and-add method}\label{alg:double_and_add} - \Input{$x,y$ --- two \texttt{u32} integers being multiplied, $N$ --- bitsize of $y$, $s$ --- parameter to regulate the number of shards.} + \Input{$x,y$ --- two \texttt{u32} integers being multiplied, $N$ + --- bitsize of $y$.} \Output{Result of the multiplication $x \times y$} - - \For{$i \in \{0,\dots,N/s\}$}{ + + $r \gets 0$ + + $t \gets x$ + + \For{$i \in \{0,\dots,N\}$}{ \textcolor{blue!50!black}{\textbf{Start the shard $i$}} - Decompose $y$ into the binary form: $y=(y_0,\dots,y_{N-1})_2$ + \textcolor{blue!50!black}{Decompose $y$ into the binary form: + $y=(y_0,\dots,y_{N-1})_2$} - \For{$j \in \{0,\dots,s\}$}{ - \If{$y_{is+j} = 1$}{ - $r \gets r + t$ - } - - $t \gets 2 \times t$ + \If{$y_i = 1$}{ + $r \gets r + t$ } - Recover $y$ back to the original form: $y \gets \sum_{i=0}^{N-1} y_i2^i$. + $t \gets 2 \times t$ - \textcolor{blue!50!black}{\textbf{End shard $i$}} + \textcolor{orange!60!black}{Recover $y$ back to the original + form: $y \gets \sum_{i=0}^{N-1} y_i2^i$.} + + \textcolor{orange!60!black}{\textbf{End shard $i$}} } - - \Return{Integer $r$} \label{alg:double_and_add_bitvm_friendly} - \end{algorithm} +Why such script is BitVM-friendly? Consider the following statement. + +\begin{theorem} + The cost of $i^{\text{th}}$ \texttt{DisproveScript} of + BitVM-friendly double-and-add method above costs roughly $|g_i| + + 2\gamma\lceil N/\ell \rceil + 4\gamma\lceil 2N/\ell \rceil$ bytes + (where $\ell=30$ is the limb bitsize), where $g_i$ is the script that: + \begin{enumerate} + \item Decomposes $y$ to the binary form. + \item Checks that the $i^{\text{th}}$ bit is $1$. If so, adds $t$ + to $r$ and doubles $t$. + \item Recovers $y$ back to the original integer form. + \end{enumerate} + + Note that typically $|g_i|$ is much smaller than $2\gamma\lceil + N/\ell \rceil + 4\gamma\lceil 2N/\ell \rceil$. In turn, this size + is significantly less than the cost of commitment to the lookup + table, which is $\gamma N$. +\end{theorem} + +\textbf{Proof.} At the end of each shard, we store $r$, $t$, and $y$. +Potentially, $r$ and $t$ are $2N$-bit integers, so the number of limbs to +represent each of them is $\lceil 2N/\ell \rceil$. In turn, $y$ is an $N$-bit +integer, and thus consists of $\lceil N/\ell\rceil$ limbs. Therefore, it takes +$\gamma(\lceil N/\ell \rceil+2\lceil 2N/\ell \rceil)$ to commit to these +values. However, the \texttt{DisproveScript} contains commitments to +two states, so +the total commitment size is twice this value: $\gamma(2\lceil 2N/\ell \rceil + +\lceil N/\ell \rceil)$. Finally, add the size of the script $g_i$ to get the +final result. \hfill $\square$ + +\subsection*{Implementing BitVM-friendly Windowed Multiplication} + +Now, in this section, we implement the BitVM-friendly windowed multiplication +using idea above. We first specify the basic windowed multiplication +algorithm in \Cref{alg:windowed}. + +\begin{algorithm} + \caption{BitVM-friendly $w$-width windowed method for big integer + multiplication}\label{alg:windowed} + \Input{Two integers to multiply $x$ and $y$.} + \Output{Result of multiplication $x \times y$.} + + Decompose $y$ to the $w$-width form: $(y_0,y_1,\dots,y_{L-1})_w$ + + Precompute values $\{0, x, 2x, 3x, \dots,(2^w-1)x\}$ (in other + words, implement the lookup table). Denote by $\mathcal{T}[j] = + [j]P$ -- referencing the lookup table at index $j$. + + $q \gets 0$ + + \For{$i \in \{L-1,\dots,0\}$}{ + \For{$\_ \in \{1,\dots,w\}$} { + $q \gets 2 \times q$ + } + + $q \gets q + \mathcal{T}[y_i]$ + } + + \Return{$q$} +\end{algorithm} + +The main idea of the BitVM-friendly windowed multiplication is to split the +program into shards, where at the end of each shard, we drop the lookup table, +except for the element $x$, and restore $y$ from the $w$-width decomposition +$(y_0,\dots,y_{L-1})_w$ as $y \gets \sum_{i=0}^{L-1}y_i2^{wi}$. Informally, +the algorithm is described in \Cref{alg:windowed_bitvm_friendly}. + +\begin{algorithm} + \caption{$w$-width windowed method for big integer + multiplication}\label{alg:windowed_bitvm_friendly} + \Input{Two integers to multiply $x$ and $y$.} + \Output{Result of multiplication $x \times y$.} + + $q \gets 0$ + + \For{$i \in \{L-1,\dots,0\}$}{ + \textcolor{blue!50!black}{\textbf{Start the shard $L-1-i$}} + + \textcolor{blue!50!black}{Decompose $y$ into the $w$-width form: + $y=(y_0,\dots,y_{L-1})_w$} + + \textcolor{blue!50!black}{Create a lookup table $\mathcal{T}$ for $x$} + + \For{$\_ \in \{1,\dots,w\}$} { + $q \gets 2 \times q$ + } + + $q \gets q + \mathcal{T}[y_i]$ + + \textcolor{orange!60!black}{Recover $y$ back to the original + form: $y \gets \sum_{i=0}^{L-1} y_i2^{wi}$.} + + \textcolor{orange!60!black}{Restore $x$ from the lookup table + $\mathcal{T}$ and delete the rest.} + + \textcolor{orange!60!black}{\textbf{End shard $L-1-i$}} + } + + \textcolor{blue!50!black}{\textbf{Start the shard $L$}} + + \textcolor{blue!50!black}{Drop $x$ and $y$} + + \Return{$q$} +\end{algorithm} + +The concrete implementation of this script can be found here: +\begin{center} + \url{https://github.com/distributed-lab/nero/blob/main/bitcoin-testscripts/src/friendly/bigint_mul.rs} +\end{center} + +Why is this script friendly? The logic here is the same as for the +double-and-add algorithm. + +\begin{theorem} + The cost of $i^{\text{th}}$ \texttt{DisproveScript} of BitVM-friendly + $w$-window multiplication method above costs roughly $|g_i| + + 4\gamma\lceil N/\ell \rceil + 2\gamma\lceil 2N/\ell \rceil$ bytes (where + $\ell=30$ is the limb bitsize), where $g_i$ is the script that: + \begin{enumerate} + \item Decomposes $y$ to the $w$-width form and builds the lookup + table $\mathcal{T}$. + \item Performs $q \gets 2^{w}q$ operation and adds + $\mathcal{T}[y_i]$ to $q$. + \item Recovers $x$ and $y$ back to the original form while + dropping $\mathcal{T}$ and decomposition. + \end{enumerate} + + Note that typically $|g_i|$ is much smaller than $4\gamma\lceil + N/\ell \rceil + 2\gamma\lceil 2N/\ell \rceil$. In turn, this size + is significantly less than the cost of commitment to the lookup + table and decomposition, which is $\gamma (2^w-1) \lceil N/\ell + \rceil + \gamma \lceil N/w \rceil$. +\end{theorem} + +\textbf{Proof.} The state consists of two $N$-bit integers $x$ and $y$, together +with a $2N$-bit temporary variable $q$. Since we need two states to represent +the \texttt{DisproveScript}, the total cost is $2\gamma\lceil N/\ell \rceil + +4\gamma\lceil 2N/\ell \rceil$. Finally, add the size of the script $g_i$ to get +the result. + +Why is this better? Previously, we needed to commit to $2^{w}-1$ $N$-bit +integers, which constitute the lookup table $\mathcal{T}$, which takes $\gamma +(2^w-1)\lceil N/\ell \rceil$ bytes to commit. Add $\gamma\lceil N/w \rceil$ to +account for the decomposition of $y$. \hfill $\square$ + +\subsection*{Future Directions} + That being said, our future directions are the following: \begin{itemize} - \item Try writing the aforementioned algorithm in the BitVM-friendly way. - \item Experiment whether $w$-width decomposition might make multiplication - more friendly. \item Implement the cost-effective version of the architecture (Section 5.3 in - \cite{bitvm2}). + \cite{bitvm2}). \item Run the architecture with the simple demo function verification on the - Bitcoin mainnet. + Bitcoin mainnet. + \item Implement more BitVM-friendly functions and analyze their cost. \end{itemize} \printbibliography{} diff --git a/docs/paper/refs.bib b/docs/paper/refs.bib index 4eb4434..529184f 100644 --- a/docs/paper/refs.bib +++ b/docs/paper/refs.bib @@ -37,3 +37,11 @@ @misc{w-width-mul url = {https://eprint.iacr.org/2024/1236} } +@misc{musig2, + author = {Jonas Nick and Tim Ruffing and Yannick Seurin}, + title = {{MuSig2}: Simple Two-Round Schnorr Multi-Signatures}, + howpublished = {Cryptology {ePrint} Archive, Paper 2020/1261}, + year = {2020}, + doi = {10.1007/978-3-030-84242-0_8}, + url = {https://eprint.iacr.org/2020/1261} +} \ No newline at end of file diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index c14bdf5..495fcfb 100644 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [dev-dependencies] -bitcoincore-rpc = "0.17.0" +jsonrpc = "0.18.0" bitcoin = { workspace = true, features = [ "std", "serde" ] } color-eyre = "0.6.3" eyre = "0.6.12" @@ -16,6 +16,6 @@ tracing.workspace = true tracing-subscriber.workspace = true hex = "0.4.3" -bitvm2-core.path = "../core" +nero-core.path = "../nero-core" bitcoin-splitter.path = "../bitcoin-splitter" bitcoin-testscripts.path = "../bitcoin-testscripts" \ No newline at end of file diff --git a/integration-tests/src/common/mod.rs b/integration-tests/src/common/mod.rs index a5f450e..e545315 100644 --- a/integration-tests/src/common/mod.rs +++ b/integration-tests/src/common/mod.rs @@ -1,16 +1,15 @@ use std::collections::HashMap; -use bitcoincore_rpc::{ - bitcoin::{ - address::{NetworkChecked, NetworkUnchecked}, - Address, Amount, - }, - json::AddressType, - jsonrpc::{self, error::RpcError}, - Error, RpcApi, +use bitcoin::{ + address::{NetworkChecked, NetworkUnchecked}, + consensus::{Decodable, Encodable}, + io::Cursor, + Address, Amount, Denomination, Transaction, Txid, }; use ini::Ini; +use jsonrpc::{error::RpcError, Client}; use once_cell::sync::Lazy; +use serde_json::{json, value::to_raw_value}; /// Store at compile time the configuration file of local Bitcoind /// node, and parse it at start of the runtime. @@ -20,7 +19,7 @@ pub(crate) static BITCOIN_CONFIG: Lazy = /// Bitcoin params client to local node. /// /// Parameters are read from local ./configs/bitcoind.conf file. -pub(crate) static BITCOIN_CLIENT_PARAMS: Lazy<(String, bitcoincore_rpc::Auth)> = Lazy::new(|| { +pub(crate) static BITCOIN_CLIENT_PARAMS: Lazy<(String, String, String)> = Lazy::new(|| { let config = &BITCOIN_CONFIG; let regtest_section = config.section(Some("regtest")).unwrap(); @@ -30,43 +29,43 @@ pub(crate) static BITCOIN_CLIENT_PARAMS: Lazy<(String, bitcoincore_rpc::Auth)> = let username = regtest_section.get("rpcuser").unwrap(); let password = regtest_section.get("rpcpassword").unwrap(); - ( - url, - bitcoincore_rpc::Auth::UserPass(username.to_owned(), password.to_owned()), - ) + (url, username.to_owned(), password.to_owned()) }); /// initialize bitcoin client from params -pub(crate) fn init_bitcoin_client() -> eyre::Result { - let (mut url, auth) = BITCOIN_CLIENT_PARAMS.clone(); +pub(crate) fn init_client() -> eyre::Result { + let (mut url, username, password) = BITCOIN_CLIENT_PARAMS.clone(); url.push_str(&format!("/wallet/{}", WALLET_NAME)); - bitcoincore_rpc::Client::new(&url, auth).map_err(Into::into) + Client::simple_http(&url, Some(username), Some(password)).map_err(Into::into) } /// Wallet name which will be used in tests. -pub(crate) const WALLET_NAME: &str = "bitvm2-tests-wallet"; +pub(crate) const WALLET_NAME: &str = "nero-tests-wallet"; /// Address label which will be used in tests. -pub(crate) const ADDRESS_LABEL: &str = "bitvm2-tests-label"; +pub(crate) const ADDRESS_LABEL: &str = "nero-tests-label"; /// Init wallet if one is not initialized. -pub(crate) fn init_wallet() -> eyre::Result> { - let client = init_bitcoin_client()?; +pub(crate) fn init_wallet() -> eyre::Result
{ + let client = init_client()?; // init wallet tracing::info!("Initilizing wallet..."); - match client.create_wallet(WALLET_NAME, None, None, None, None) { + match create_wallet(&client, WALLET_NAME) { Ok(_) => {} - Err(Error::JsonRpc(jsonrpc::Error::Rpc(RpcError { code: -4, .. }))) => {} + // Was already created, so let's skip it + Err(jsonrpc::Error::Rpc(RpcError { code: -4, .. })) => { + tracing::info!("Wallet was already created"); + } Err(err) => return Err(err.into()), }; // Get existing address, create one if is there is none. - let address = match get_addresses_by_label()? { + let address = match get_addresses_by_label(&client)? { Some(addrs) => addrs.0.into_keys().next().unwrap(), - None => client.get_new_address(Some(ADDRESS_LABEL), Some(AddressType::Bech32m))?, + None => get_new_address(&client, ADDRESS_LABEL)?, } .assume_checked(); tracing::info!(%address, "Got balance for funding"); @@ -76,18 +75,18 @@ pub(crate) fn init_wallet() -> eyre::Result> { Ok(address) } -pub(crate) const MIN_REQUIRED_AMOUNT: Amount = Amount::from_sat(1_0000_0000); +pub(crate) const MIN_REQUIRED_AMOUNT: Amount = Amount::ONE_BTC; /// Fund address with minimum required amount of BTC. pub(crate) fn fund_address(address: &Address) -> eyre::Result<()> { - let client = init_bitcoin_client()?; + let client = init_client()?; // if already has enough, leave - if client.get_balance(None, None)? >= MIN_REQUIRED_AMOUNT { + if get_balance(&client)? >= MIN_REQUIRED_AMOUNT { return Ok(()); } - let block_count = client.get_block_count()?; + let block_count = get_block_count(&client)?; // if it's only the fresh instance, generate initial 101 blocks if block_count <= 2 { @@ -95,15 +94,15 @@ pub(crate) fn fund_address(address: &Address) -> eyre::Result<() block_num = 101, "Bitcoin blockchain is fresh, genereting initial blocks..." ); - client.generate_to_address(101, address)?; + generate_to_address(&client, 101, address.clone())?; return Ok(()); } // otherwise geneate blocks until address would have anough tracing::info!(%block_count, "Generating blocks one by one"); for i in 0..101 { - client.generate_to_address(i, address)?; - let current_balance = client.get_balance(None, None)?; + generate_to_address(&client, i, address.clone())?; + let current_balance = get_balance(&client)?; if current_balance >= MIN_REQUIRED_AMOUNT { return Ok(()); } @@ -118,16 +117,145 @@ pub(crate) fn fund_address(address: &Address) -> eyre::Result<() Ok(()) } +/* Let's fork bitcoincore-rpc instead for BitVM branch compatability */ + +pub(crate) fn create_wallet( + client: &Client, + name: &str, +) -> Result { + client.call("createwallet", Some(&to_raw_value(&[name]).unwrap())) +} + +pub(crate) fn get_new_address( + client: &Client, + label: &str, +) -> Result, jsonrpc::Error> { + client.call( + "getnewaddress", + Some(&to_raw_value(&[label, "bech32m"]).unwrap()), + ) +} + +pub(crate) fn get_balance(client: &Client) -> Result { + let number: f64 = client.call("getbalance", None)?; + + Ok(Amount::from_float_in(number, Denomination::Bitcoin).unwrap()) +} + +pub(crate) fn get_block_count(client: &Client) -> Result { + client.call("getblockcount", None) +} + +pub(crate) fn generate_to_address( + client: &Client, + blocks: usize, + address: Address, +) -> Result { + client.call( + "generatetoaddress", + Some( + &to_raw_value(&[ + to_raw_value(&blocks).unwrap(), + to_raw_value(&address).unwrap(), + ]) + .unwrap(), + ), + ) +} + +pub(crate) fn fund_raw_transaction( + client: &Client, + tx: &Transaction, + change_pos: isize, +) -> Result { + let hextx = { + // TODO(Velnbur): only this works and not this: + // `bitcoin::consensus::encode::serialize_hex(tx)` because + // fuck, bitcoin, I don't know... + let mut buff = Vec::new(); + tx.version.consensus_encode(&mut buff).unwrap(); + // 0x00.consensus_encode(&mut buff).unwrap(); + // 0x01.consensus_encode(&mut buff).unwrap(); + tx.input.consensus_encode(&mut buff).unwrap(); + tx.output.consensus_encode(&mut buff).unwrap(); + // for input in &tx.input { + // input.witness.consensus_encode(&mut buff).unwrap(); + // } + tx.lock_time.consensus_encode(&mut buff).unwrap(); + hex::encode(&buff) + }; + + let options = json!({ + "changePosition": change_pos, + }); + + let params = to_raw_value(&[ + to_raw_value(&hextx).unwrap(), + to_raw_value(&options).unwrap(), + // to_raw_value(&false).unwrap(), + ]) + .unwrap(); + + let value: serde_json::Value = client.call("fundrawtransaction", Some(¶ms))?; + + let hextx = value + .as_object() + .unwrap() + .get("hex") + .unwrap() + .as_str() + .unwrap(); + let decoded_bytes = hex::decode(hextx).unwrap(); + let mut cursor = Cursor::new(decoded_bytes); + let tx = Transaction::consensus_decode(&mut cursor).unwrap(); + + Ok(tx) +} + +pub(crate) fn sign_raw_transaction_with_wallet( + client: &Client, + tx: &Transaction, +) -> Result { + let hextx = bitcoin::consensus::encode::serialize_hex(&tx); + + let params = vec![to_raw_value(&hextx).unwrap()]; + + let params = to_raw_value(¶ms).unwrap(); + let value: serde_json::Value = client.call("signrawtransactionwithwallet", Some(¶ms))?; + + let hextx = value + .as_object() + .unwrap() + .get("hex") + .unwrap() + .as_str() + .unwrap(); + let decoded_bytes = hex::decode(hextx).unwrap(); + let mut cursor = Cursor::new(decoded_bytes); + let tx = Transaction::consensus_decode(&mut cursor).unwrap(); + + Ok(tx) +} + +pub(crate) fn send_raw_transaciton( + client: &Client, + tx: &Transaction, +) -> Result { + let hextx = bitcoin::consensus::encode::serialize_hex(tx); + let params = to_raw_value(&[hextx]).unwrap(); + client.call("sendrawtransaction", Some(¶ms)) +} + #[derive(serde::Deserialize)] -#[serde(transparent)] struct GetAddressesByLabel(HashMap, serde_json::Value>); -fn get_addresses_by_label() -> eyre::Result> { - let client = init_bitcoin_client()?; - - match client.call("getaddressesbylabel", &[ADDRESS_LABEL.into()]) { +fn get_addresses_by_label(client: &Client) -> eyre::Result> { + match client.call( + "getaddressesbylabel", + Some(&to_raw_value(&[ADDRESS_LABEL]).unwrap()), + ) { Ok(value) => Ok(Some(value)), - Err(Error::JsonRpc(jsonrpc::Error::Rpc(RpcError { code: -11, .. }))) => Ok(None), + Err(jsonrpc::Error::Rpc(RpcError { code: -11, .. })) => Ok(None), Err(err) => Err(err.into()), } } diff --git a/integration-tests/src/fetch_balance.rs b/integration-tests/src/fetch_balance.rs index 6bd8725..066f2f7 100644 --- a/integration-tests/src/fetch_balance.rs +++ b/integration-tests/src/fetch_balance.rs @@ -1,16 +1,14 @@ -use bitcoincore_rpc::RpcApi; - -use crate::common::{init_bitcoin_client, init_wallet, MIN_REQUIRED_AMOUNT}; +use crate::common::{get_balance, init_client, init_wallet, MIN_REQUIRED_AMOUNT}; #[test] fn test_ensure_user_has_min_btc() -> eyre::Result<()> { color_eyre::install()?; tracing_subscriber::fmt().init(); - let client = init_bitcoin_client()?; + let client = init_client()?; let _address = init_wallet()?; - let balance = client.get_balance(None, None)?; + let balance = get_balance(&client)?; assert!(balance > MIN_REQUIRED_AMOUNT, "current balance {}", balance); diff --git a/integration-tests/src/lib.rs b/integration-tests/src/lib.rs index 8170757..339a0ae 100644 --- a/integration-tests/src/lib.rs +++ b/integration-tests/src/lib.rs @@ -5,4 +5,4 @@ mod common; mod fetch_balance; #[cfg(test)] -mod taproot; +mod operator; diff --git a/integration-tests/src/operator.rs b/integration-tests/src/operator.rs new file mode 100644 index 0000000..8812293 --- /dev/null +++ b/integration-tests/src/operator.rs @@ -0,0 +1,347 @@ +use bitcoin::{ + key::{ + rand::{rngs::StdRng, thread_rng}, + Secp256k1, + }, + relative::Height, + Address, Amount, FeeRate, Network, PrivateKey, ScriptBuf, +}; +use bitcoin_splitter::split::script::{IOPair, SplitableScript}; +use bitcoin_testscripts::int_mul_windowed::U32MulScript; +use jsonrpc::Client; +use nero_core::{ + musig2::{NonceSeed, SecNonce}, + operator::{ + FinalOperator, NoncesAggregationOperator, Operator, OperatorConfig, PartialSignatures, + SignaturesAggOperator, UnfundedOperator, + }, +}; +use once_cell::sync::Lazy; + +use crate::common::{ + fund_raw_transaction, generate_to_address, init_client, init_wallet, send_raw_transaciton, + sign_raw_transaction_with_wallet, +}; + +const CLAIM_CHALLENGE_PERIOD: u16 = 6; +const ASSERT_CHALLENGE_PERIOD: u16 = 6; +const FEE_RATE: FeeRate = FeeRate::from_sat_per_vb_unchecked(5); + +/// In tests we are assuming the comitte consists of 1 participant. +static COMITTEE_PRIVATE_KEY: Lazy = Lazy::new(|| { + "cNMMXcLoM65N5GaULU7ct2vexmQnJ5i5j3Sjc6iNnEF18vY7gzn9" + .parse() + .unwrap() +}); + +static COMITTEE_SECNONCE: Lazy = Lazy::new(|| { + SecNonce::build(NonceSeed::from([1; 32])) + .with_seckey(COMITTEE_PRIVATE_KEY.inner) + .build() +}); + +fn setup_unfunded_operator( + wallet_addr: ScriptBuf, + distort: bool, +) -> UnfundedOperator { + let IOPair { input, .. } = S::generate_valid_io_pair(); + let ctx = Secp256k1::new(); + + let config = OperatorConfig { + network: Network::Regtest, + operator_script_pubkey: wallet_addr, + staked_amount: Amount::from_sat(50_000), + input, + claim_challenge_period: Height::from(CLAIM_CHALLENGE_PERIOD), + assert_challenge_period: Height::from(ASSERT_CHALLENGE_PERIOD), + comittee: vec![COMITTEE_PRIVATE_KEY.inner.public_key(&ctx)], + seed: [1u8; 32], + }; + + let operator = if !distort { + Operator::::new::<_, StdRng>(config) + } else { + Operator::::new_distorted::<_, StdRng>(config) + }; + + UnfundedOperator::from_operator(operator, FEE_RATE) +} + +fn sign_txs_from_operator( + operator: &SignaturesAggOperator, +) -> eyre::Result { + let ctx = Secp256k1::new(); + + let claim_tx = operator.claim_tx(); + + let assert_tx = operator.unsigned_assert_tx(); + let partial_assert_sig = assert_tx.sign_partial_from_claim( + &ctx, + claim_tx, + // This method adds secret key to list of comittee + // participants, so this time we exclude + vec![operator.context().operator_pubkey()], + operator.aggnonce(), + COMITTEE_PRIVATE_KEY.inner, + COMITTEE_SECNONCE.clone(), + ); + + let payout_optimistic_tx = operator.unsigned_payout_optimistic(); + let partial_payout_optimistic_sig = payout_optimistic_tx.sign_partial_from_claim( + &ctx, + claim_tx, + vec![operator.context().operator_pubkey()], + operator.aggnonce(), + COMITTEE_PRIVATE_KEY.inner, + COMITTEE_SECNONCE.clone(), + ); + + let payout_tx = operator.unsigned_payout_tx(); + let partial_payout_sig = payout_tx.sign_partial_from_assert( + &ctx, + assert_tx, + vec![operator.context().operator_pubkey()], + operator.aggnonce(), + COMITTEE_PRIVATE_KEY.inner, + COMITTEE_SECNONCE.clone(), + ); + + let assert_output = assert_tx.output(&ctx); + let partial_disprove_sigs = operator + .unsigned_disprove_txs() + .iter() + .map(|disprove| { + disprove.sign_partial( + &ctx, + &assert_output, + vec![operator.context().operator_pubkey()], + operator.aggnonce(), + COMITTEE_PRIVATE_KEY.inner, + COMITTEE_SECNONCE.clone(), + ) + }) + .map(|sig| vec![sig]) + .collect::>(); + + Ok(PartialSignatures { + partial_assert_sigs: vec![partial_assert_sig], + partial_payout_optimistic_sigs: vec![partial_payout_optimistic_sig], + partial_payout_sigs: vec![partial_payout_sig], + partial_disprove_sigs, + }) +} + +struct TestSetup { + operator: FinalOperator, + wallet_addr: Address, + client: Client, +} + +fn setup_operator(distort: bool) -> eyre::Result> { + let client = init_client()?; + let wallet_addr = init_wallet()?; + let unfunded_operator = setup_unfunded_operator::(wallet_addr.script_pubkey(), distort); + + let tx = unfunded_operator.unsigned_claim_tx(); + let funded_claim_tx = fund_raw_transaction(&client, &tx, 2)?; + tracing::info!( + tx = bitcoin::consensus::encode::serialize_hex(&funded_claim_tx), + "Claim is funded" + ); + let signed_funded_tx = sign_raw_transaction_with_wallet(&client, &funded_claim_tx)?; + tracing::info!( + tx = bitcoin::consensus::encode::serialize_hex(&signed_funded_tx), + "Funded claim is signed" + ); + let funding_inputs = signed_funded_tx.input; + let change_output = signed_funded_tx.output.get(2).cloned(); + + let nonce_agg_operator = NoncesAggregationOperator::from_unfunded_operator( + unfunded_operator, + funding_inputs, + change_output, + FEE_RATE, + &mut thread_rng(), + ); + + let comitte_pub_nonce = COMITTEE_SECNONCE.public_nonce(); + + let sigs_agg_operator = SignaturesAggOperator::from_nonces_agg_operator( + nonce_agg_operator, + vec![comitte_pub_nonce], + FEE_RATE, + ); + + let partial_sigs = sign_txs_from_operator(&sigs_agg_operator)?; + let final_operator = + FinalOperator::from_signatures_agg_operator(sigs_agg_operator, partial_sigs); + + Ok(TestSetup { + operator: final_operator, + wallet_addr, + client, + }) +} + +fn test_operator_optimistic_payout() -> eyre::Result<()> { + let TestSetup { + operator, + wallet_addr, + client, + } = setup_operator::(false)?; + let ctx = Secp256k1::new(); + + let claim_tx = operator.claim_tx(); + let tx = claim_tx.to_tx(&ctx); + tracing::info!( + tx = bitcoin::consensus::encode::serialize_hex(&tx), + "Sending claim" + ); + send_raw_transaciton(&client, &tx)?; + generate_to_address( + &client, + (CLAIM_CHALLENGE_PERIOD + 1).into(), + wallet_addr.clone(), + )?; + + let payout_optimistic_tx = operator.payout_optimistic_tx(); + let tx = payout_optimistic_tx.to_tx(); + tracing::info!( + txid = tx.compute_txid().to_string(), + tx = bitcoin::consensus::encode::serialize_hex(&tx), + "Sending payout optimistic" + ); + send_raw_transaciton(&client, &tx)?; + generate_to_address(&client, 1, wallet_addr)?; + + Ok(()) +} + +fn setup_assert_scenario( + operator: &FinalOperator, + ctx: &Secp256k1, + client: &Client, + wallet_addr: &Address, +) -> eyre::Result<()> { + let claim_tx = operator.claim_tx(); + let tx = claim_tx.to_tx(ctx); + tracing::info!( + tx = bitcoin::consensus::encode::serialize_hex(&tx), + "Sending claim" + ); + send_raw_transaciton(client, &tx)?; + generate_to_address(client, 1, wallet_addr.clone())?; + let challenge_tx = operator.challenge_tx(); + let tx = challenge_tx.to_tx(); + tracing::info!( + tx = bitcoin::consensus::encode::serialize_hex(&tx), + "Got challenge tx" + ); + let mut funded_challenge_tx = fund_raw_transaction(client, &tx, 1)?; + funded_challenge_tx.input[0].witness = tx.input[0].witness.clone(); + tracing::info!( + tx = bitcoin::consensus::encode::serialize_hex(&funded_challenge_tx), + "Challenge is funded" + ); + let signed_challenge_tx = sign_raw_transaction_with_wallet(client, &funded_challenge_tx)?; + tracing::info!( + txid = %signed_challenge_tx.compute_txid(), + tx = bitcoin::consensus::encode::serialize_hex(&signed_challenge_tx), + "Funded challenge is signed, sending..." + ); + + send_raw_transaciton(client, &signed_challenge_tx)?; + let assert_tx = operator.assert_tx(); + let tx = assert_tx.to_tx(ctx); + tracing::info!( + tx = bitcoin::consensus::encode::serialize_hex(&tx), + "Sending assert tx" + ); + send_raw_transaciton(client, &tx)?; + Ok(()) +} + +fn test_operator_payout() -> eyre::Result<()> { + let TestSetup { + operator, + wallet_addr, + client, + } = setup_operator::(false)?; + let ctx = Secp256k1::new(); + + setup_assert_scenario(&operator, &ctx, &client, &wallet_addr)?; + + generate_to_address( + &client, + (ASSERT_CHALLENGE_PERIOD + 1).into(), + wallet_addr.clone(), + )?; + + let payout_tx = operator.payout_tx(); + let tx = payout_tx.to_tx(&ctx); + tracing::info!( + tx = bitcoin::consensus::encode::serialize_hex(&tx), + "Sending payout tx" + ); + send_raw_transaciton(&client, &tx)?; + generate_to_address(&client, 1, wallet_addr)?; + + Ok(()) +} + +fn test_operator_disprove() -> eyre::Result<()> { + let TestSetup { + operator, + wallet_addr, + client, + } = setup_operator::(true)?; + let ctx = Secp256k1::new(); + + setup_assert_scenario(&operator, &ctx, &client, &wallet_addr)?; + + generate_to_address(&client, 1, wallet_addr.clone())?; + + let found_disprove = operator + .disprove_txs() + .iter() + .enumerate() + .find_map(|(idx, disprove)| { + let tx = disprove.to_tx(&ctx); + tracing::info!( + ?idx, + tx = bitcoin::consensus::encode::serialize_hex(&tx), + "Sending disprove tx" + ); + + if let Err(err) = send_raw_transaciton(&client, &tx) { + tracing::info!( + %err, + "Failed to spend disprove tx" + ); + None + } else { + Some(disprove) + } + }); + + assert!(found_disprove.is_some()); + generate_to_address(&client, 1, wallet_addr)?; + + Ok(()) +} + +fn test_operator_generic() -> eyre::Result<()> { + test_operator_payout::()?; + test_operator_disprove::()?; + test_operator_optimistic_payout::()?; + + Ok(()) +} + +#[test] +fn test_operator_u32mul() -> eyre::Result<()> { + color_eyre::install()?; + tracing_subscriber::fmt().init(); + + test_operator_generic::() +} diff --git a/integration-tests/src/taproot.rs b/integration-tests/src/taproot.rs deleted file mode 100644 index b25e60c..0000000 --- a/integration-tests/src/taproot.rs +++ /dev/null @@ -1,292 +0,0 @@ -use std::{env, fs, str::FromStr as _}; - -use bitcoin::{ - consensus::{Decodable, Encodable as _}, - io::Cursor, - key::{rand::rngs::SmallRng, Secp256k1}, - relative::Height, - secp256k1::{All, PublicKey, SecretKey}, - Address, Amount, CompressedPublicKey, Network, OutPoint, Transaction, TxOut, Txid, -}; -use bitcoin_splitter::split::script::{IOPair, SplitableScript}; -use bitcoin_testscripts::{ - int_mul_windowed::{U254MulScript, U32MulScript}, - square_fibonacci::SquareFibonacciScript, -}; -use bitcoincore_rpc::{ - bitcoin::consensus::{Decodable as _, Encodable as _}, - RawTx as _, RpcApi, -}; -use bitvm2_core::{ - assert::{AssertTransaction, Options}, - treepp::*, -}; -use once_cell::sync::Lazy; - -use crate::common::{init_bitcoin_client, init_wallet}; - -static OPERATOR_SECKEY: Lazy = Lazy::new(|| { - "50c8f972285ad27527d79c80fe4df1b63c1192047713438b45758ea4e110a88b" - .parse() - .unwrap() -}); - -static OPERATOR_PUBKEY: Lazy = Lazy::new(|| { - let ctx = Secp256k1::new(); - OPERATOR_SECKEY.public_key(&ctx) -}); - -macro_rules! hex { - ($tx:expr) => {{ - let mut buf = Vec::new(); - $tx.consensus_encode(&mut buf).unwrap(); - hex::encode(&buf) - }}; -} - -macro_rules! txconv { - ($tx:expr) => {{ - let mut buf = Vec::new(); - $tx.consensus_encode(&mut buf).unwrap(); - let mut cursor = std::io::Cursor::new(&buf); - bitcoincore_rpc::bitcoin::Transaction::consensus_decode(&mut cursor)?.raw_hex() - }}; -} - -struct TestSetup { - ctx: Secp256k1, - client: bitcoincore_rpc::Client, - funder_address: bitcoincore_rpc::bitcoin::Address, - input_script: Script, - _output_script: Script, - funding_txid: Txid, - funding_txout_idx: usize, - funding_txout: TxOut, -} - -fn setup_test(amount_sats: u64) -> eyre::Result -where - S: SplitableScript, -{ - let client = init_bitcoin_client()?; - let address = init_wallet()?; - - let IOPair { input, output } = S::generate_invalid_io_pair(); - - let ctx = Secp256k1::new(); - - let operator_pubkey = OPERATOR_SECKEY.public_key(&ctx); - let operator_p2wpkh_addr = Address::p2wpkh( - &CompressedPublicKey::try_from(bitcoin::PublicKey::new(operator_pubkey)).unwrap(), - Network::Regtest, - ); - - // TODO(Velnbur): fix version of bitcoincorerpc and Bitcoin for this... - let operator_funding_txid = client.send_to_address( - &bitcoincore_rpc::bitcoin::Address::from_str(&operator_p2wpkh_addr.to_string()) - .unwrap() - .assume_checked(), - bitcoincore_rpc::bitcoin::Amount::from_sat(amount_sats), - None, - None, - None, - None, - None, - None, - )?; - let tx = client.get_raw_transaction(&operator_funding_txid, None)?; - let tx = { - let mut buf = Vec::new(); - tx.consensus_encode(&mut buf).unwrap(); - let mut cursor = Cursor::new(&buf); - Transaction::consensus_decode(&mut cursor)? - }; - - tracing::info!(hex = %hex!(tx), txid = %operator_funding_txid, "Created funding"); - client.generate_to_address(6, &address)?; - - // find txout - let txid = tx.compute_txid(); - let (idx, funding_txout) = tx - .output - .into_iter() - .enumerate() - .find(|(_idx, out)| out.value == Amount::from_sat(amount_sats)) - .unwrap(); - - Ok(TestSetup { - ctx, - client, - funder_address: address, - input_script: input, - _output_script: output, - funding_txid: txid, - funding_txout_idx: idx, - funding_txout, - }) -} - -fn test_script_payout_spending() -> eyre::Result<()> -where - S: SplitableScript, -{ - // Approximate amount of satoshis to fullfill the fees for all - // transactions in tests. - const APPROX_TXOUT_AMOUNT: u64 = 71_000; - - let TestSetup { - ctx, - client, - input_script, - funding_txid, - funding_txout_idx, - funding_txout, - funder_address, - .. - } = setup_test::(APPROX_TXOUT_AMOUNT)?; - - let operator_xonly = OPERATOR_PUBKEY.x_only_public_key().0; - let assert_tx = AssertTransaction::::with_options( - input_script, - operator_xonly, - Amount::from_sat(APPROX_TXOUT_AMOUNT - 1_000), - Options { - payout_locktime: Height::from(1), - }, - ); - - let atx = assert_tx.clone().spend_p2wpkh_input_tx( - &ctx, - &OPERATOR_SECKEY, - funding_txout.clone(), - OutPoint::new(funding_txid, funding_txout_idx as u32), - )?; - - println!("Txid: {}", atx.compute_txid()); - println!("Assert: {}", hex!(atx)); - client.send_raw_transaction(txconv!(atx))?; - client.generate_to_address(1, &funder_address)?; - - let payout_tx = assert_tx.payout_transaction( - &ctx, - TxOut { - value: Amount::from_sat(APPROX_TXOUT_AMOUNT - 2_000), - script_pubkey: funding_txout.script_pubkey, - }, - OutPoint::new(atx.compute_txid(), 0), - &OPERATOR_SECKEY, - )?; - - println!("Txid: {}", payout_tx.compute_txid()); - println!("Payout: {}", hex!(payout_tx)); - client.send_raw_transaction(txconv!(payout_tx))?; - client.generate_to_address(6, &funder_address)?; - - Ok(()) -} - -fn test_script_disprove_distorted() -> eyre::Result<()> -where - S: SplitableScript, -{ - // Approximate amount of satoshis to fullfill the fees for all - // transactions in tests. - const APPROX_TXOUT_AMOUNT: u64 = 100_000; - - let TestSetup { - ctx, - client, - input_script, - funding_txid, - funding_txout_idx, - funding_txout, - funder_address, - .. - } = setup_test::(APPROX_TXOUT_AMOUNT)?; - - let operator_xonly = OPERATOR_PUBKEY.x_only_public_key().0; - let (assert_tx, distored_idx) = - AssertTransaction::::with_options_distorted::<[u8; 32], SmallRng>( - input_script, - operator_xonly, - // remove 10% from amount to fulfill the fee - Amount::from_sat(APPROX_TXOUT_AMOUNT * 9 / 10), - Options { - payout_locktime: Height::from(1), - }, - [1; 32], - ); - - let atx = assert_tx.clone().spend_p2wpkh_input_tx( - &ctx, - &OPERATOR_SECKEY, - funding_txout.clone(), - OutPoint::new(funding_txid, funding_txout_idx as u32), - )?; - - println!("Txid: {}", atx.compute_txid()); - println!("Assert: {}", hex!(atx)); - client.send_raw_transaction(txconv!(atx))?; - client.generate_to_address(1, &funder_address)?; - - let disprove_txs = assert_tx.clone().disprove_transactions( - &ctx, - TxOut { - // take only 10% percent and leave other for the fee. - // This values is euristic and should calculated by - // ourself instead in future. - value: Amount::from_sat(APPROX_TXOUT_AMOUNT / 10), - script_pubkey: funding_txout.script_pubkey, - }, - OutPoint::new(atx.compute_txid(), 0), - )?; - - let disprove_tx = disprove_txs - .get(&assert_tx.disprove_scripts[distored_idx]) - .unwrap(); - client.send_raw_transaction(txconv!(disprove_tx))?; - - let hexed = hex!(disprove_tx); - println!("Txid: {}", disprove_tx.compute_txid()); - println!("DisproveSize: {}", hexed.len() / 2); - - if env::var("NERO_TESTS_TX_FILE").is_ok() { - fs::write("./disprove.txt", hexed)?; - } - - client.generate_to_address(6, &funder_address)?; - - Ok(()) -} - -#[test] -#[ignore = "tx-size"] -fn test_u254_mul_disprove() -> eyre::Result<()> { - color_eyre::install()?; - tracing_subscriber::fmt().init(); - test_script_disprove_distorted::() -} - -#[test] -fn test_u254_mul_payout() -> eyre::Result<()> { - color_eyre::install()?; - tracing_subscriber::fmt().init(); - test_script_payout_spending::() -} - -#[test] -fn test_square_fibonachi() -> eyre::Result<()> { - const FIB_STEPS: usize = 1024; - - color_eyre::install()?; - tracing_subscriber::fmt().init(); - test_script_disprove_distorted::>() -} - -#[test] -#[ignore = "TODO: Figure out why it fails sometimes"] -fn test_u32mul_disprove() -> eyre::Result<()> { - color_eyre::install()?; - tracing_subscriber::fmt().init(); - test_script_disprove_distorted::() -} diff --git a/nero-cli/Cargo.toml b/nero-cli/Cargo.toml deleted file mode 100644 index c57028a..0000000 --- a/nero-cli/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "nero-cli" -version = "0.1.0" -edition = "2021" - -[dependencies] -bitcoin.workspace = true -bitcoincore-rpc = "0.17.0" -clap = "4.5.20" -clap-verbosity = "2.1.0" -color-eyre = "0.6.3" -dirs-next = "2.0.0" -eyre = "0.6.12" -serde = { version = "1.0.210", features = ["serde_derive"] } -toml = "0.8.19" -tracing.workspace = true -tracing-log = "0.2.0" -tracing-subscriber.workspace = true - -bitvm2-core.path = "../core" -hex = "0.4.3" -bitcoin-testscripts = { version = "0.1.0", path = "../bitcoin-testscripts" } -bitcoin-splitter = { version = "0.1.0", path = "../bitcoin-splitter" } -bitcoin-window-mul.workspace = true -num-bigint = "0.4.6" diff --git a/nero-cli/src/cli/actions.rs b/nero-cli/src/cli/actions.rs deleted file mode 100644 index 0ac3fb0..0000000 --- a/nero-cli/src/cli/actions.rs +++ /dev/null @@ -1,312 +0,0 @@ -use std::{collections::BTreeMap, fs, path::PathBuf, str::FromStr as _}; - -use bitcoin::{ - address::NetworkUnchecked, - consensus::{Decodable, Encodable}, - hashes::Hash, - io::Cursor, - key::rand::{rngs::SmallRng, thread_rng}, - secp256k1::SecretKey, - Address, Amount, Network, OutPoint, ScriptBuf, Transaction, TxOut, Txid, XOnlyPublicKey, -}; -use bitcoin_splitter::split::script::{IOPair, SplitableScript}; -use bitcoin_testscripts::square_fibonacci::SquareFibonacciScript; -use bitcoincore_rpc::{ - bitcoin::{ - consensus::{Decodable as _, Encodable as _}, - hashes::Hash as _, - }, - RpcApi, -}; -use bitvm2_core::{ - assert::{payout_script::PayoutScript, AssertTransaction, Options}, - disprove::DisproveScript, -}; -use clap::Args; - -use crate::context::Context; - -#[derive(Args, Debug, Clone)] -pub struct AssertTxArgs { - #[arg(long)] - pub input: PathBuf, - #[arg(long)] - pub amount: Amount, - #[arg(long)] - pub pubkey: XOnlyPublicKey, - #[arg(long)] - pub distort: bool, -} - -pub fn assert_tx(ctx: Context, args: AssertTxArgs) -> eyre::Result<()> { - let input_script: ScriptBuf = hex::decode(fs::read_to_string(args.input)?)?.into(); - - let opts = Options::default(); - - // FIXME(Velnbur): make this optionally valid - let (assert, invalid_chunk_idx) = - AssertTransaction::>::with_options_distorted::< - [u8; 32], - SmallRng, - >(input_script, args.pubkey, args.amount, opts, [1; 32]); - - let assert_output_address = Address::from_script( - &assert.txout(ctx.secp_ctx()).script_pubkey, - Network::Regtest, - )?; - - let assert_txid = ctx.client().send_to_address( - &bitcoincore_rpc::bitcoin::Address::from_str(&assert_output_address.to_string()) - .unwrap() - .assume_checked(), - bitcoincore_rpc::bitcoin::Amount::from_sat(args.amount.to_sat()), - None, - None, - None, - None, - None, - None, - )?; - - let assert_tx = fetch_tx(&ctx, assert_txid)?; - - let output = assert_tx - .output - .iter() - .position(|out| out.script_pubkey == assert_output_address.script_pubkey()) - .unwrap(); - - println!("{assert_txid}:{output}"); - if args.distort { - println!("{invalid_chunk_idx}"); - } - - fs::write( - "payout.txt", - assert.payout_script.to_script().to_hex_string(), - )?; - - let base_path = PathBuf::from("disproves"); - if base_path.exists() { - // TODO(Velnbur): we should ask about that later - fs::remove_dir_all(&base_path)?; - } - - for (idx, disprove_script) in assert.disprove_scripts.iter().enumerate() { - let disprove_base_path = base_path.join(format!("{:06}", idx)); - fs::create_dir_all(&disprove_base_path)?; - fs::write( - disprove_base_path.join("script_pubkey.txt"), - disprove_script.script_pubkey.to_hex_string(), - )?; - fs::write( - disprove_base_path.join("witness.txt"), - disprove_script.script_witness.to_hex_string(), - )?; - } - - Ok(()) -} - -fn fetch_tx( - ctx: &Context, - assert_txid: bitcoincore_rpc::bitcoin::Txid, -) -> Result { - let tx = ctx.client().get_raw_transaction(&assert_txid, None)?; - let mut buf = Vec::with_capacity(tx.size()); - tx.consensus_encode(&mut buf)?; - let mut cursor = Cursor::new(buf); - Ok(Transaction::consensus_decode(&mut cursor)?) -} - -#[derive(Args, Debug, Clone)] -pub struct PayoutSpendArgs { - #[arg(long)] - assert: bitcoincore_rpc::bitcoin::OutPoint, - #[arg(long)] - address: Address, - #[arg(long)] - seckey: SecretKey, -} - -pub fn spend_payout(ctx: Context, args: PayoutSpendArgs) -> eyre::Result<()> { - let base_path = PathBuf::from("disproves"); - - let dirs = fs::read_dir(base_path)?; - let mut disprove_scripts = BTreeMap::new(); - - for dir in dirs.filter_map(Result::ok) { - let dir_name = dir.file_name().into_string().unwrap(); - - let Ok(disprove_script_num) = dir_name.parse::() else { - continue; - }; - - let script_pubkey: ScriptBuf = - hex::decode(fs::read_to_string(dir.path().join("script_pubkey.txt"))?)?.into(); - let script_witness: ScriptBuf = - hex::decode(fs::read_to_string(dir.path().join("witness.txt"))?)?.into(); - - disprove_scripts.insert( - disprove_script_num, - DisproveScript { - script_witness, - script_pubkey, - }, - ); - } - - let disprove_scripts = disprove_scripts.values().cloned().collect::>(); - // let payout_script: ScriptBuf = hex::decode(fs::read_to_string("input.txt")?)?.into(); - - let assert_tx = ctx.client().get_raw_transaction(&args.assert.txid, None)?; - - let assert_txout = { - let mut buf = Vec::new(); - assert_tx.output[args.assert.vout as usize].consensus_encode(&mut buf)?; - let mut cursor = Cursor::new(buf); - TxOut::consensus_decode(&mut cursor)? - }; - let operator_pubkey = args.seckey.public_key(ctx.secp_ctx()).x_only_public_key().0; - let payout = PayoutScript::new(operator_pubkey); - - let assert = AssertTransaction::>::from_scripts( - operator_pubkey, - payout, - disprove_scripts, - assert_txout.value, - ); - - let tx = assert.payout_transaction( - ctx.secp_ctx(), - TxOut { - script_pubkey: args.address.assume_checked().script_pubkey(), - value: assert_txout.value.unchecked_sub(Amount::from_sat(80_000)), - }, - OutPoint::new( - Txid::from_byte_array(args.assert.txid.to_byte_array()), - args.assert.vout, - ), - &args.seckey, - )?; - - let tx = { - let mut buf = Vec::new(); - tx.consensus_encode(&mut buf)?; - let mut cursor = std::io::Cursor::new(buf); - bitcoincore_rpc::bitcoin::Transaction::consensus_decode(&mut cursor)? - }; - - ctx.client().send_raw_transaction(&tx)?; - - println!("{}", tx.txid()); - - Ok(()) -} - -#[derive(Args, Debug, Clone)] -pub struct DisproveSpendArgs { - #[arg(long)] - assert: bitcoincore_rpc::bitcoin::OutPoint, - #[arg(long)] - address: Address, - #[arg(long)] - disprove: usize, -} - -pub fn spend_disprove(ctx: Context, args: DisproveSpendArgs) -> eyre::Result<()> { - let base_path = PathBuf::from("disproves"); - - let dirs = fs::read_dir(base_path)?; - let mut disprove_scripts = BTreeMap::new(); - - for dir in dirs.filter_map(Result::ok) { - let dir_name = dir.file_name().into_string().unwrap(); - - let Ok(disprove_script_num) = dir_name.parse::() else { - continue; - }; - - let script_pubkey: ScriptBuf = - hex::decode(fs::read_to_string(dir.path().join("script_pubkey.txt"))?)?.into(); - let script_witness: ScriptBuf = - hex::decode(fs::read_to_string(dir.path().join("witness.txt"))?)?.into(); - - disprove_scripts.insert( - disprove_script_num, - DisproveScript { - script_witness, - script_pubkey, - }, - ); - } - - let disprove_scripts = disprove_scripts.values().cloned().collect::>(); - let payout_script: ScriptBuf = hex::decode(fs::read_to_string("payout.txt")?)?.into(); - - let assert_tx = ctx.client().get_raw_transaction(&args.assert.txid, None)?; - - let assert_txout = { - let mut buf = Vec::new(); - assert_tx.output[args.assert.vout as usize].consensus_encode(&mut buf)?; - let mut cursor = Cursor::new(buf); - TxOut::consensus_decode(&mut cursor)? - }; - - let disprove_script = &disprove_scripts[args.disprove]; - let tx = &AssertTransaction::>::form_disprove_transactions( - payout_script, - &disprove_scripts, - ctx.secp_ctx(), - TxOut { - script_pubkey: args.address.assume_checked().script_pubkey(), - value: assert_txout.value.unchecked_sub(Amount::from_sat(80_000)), - }, - OutPoint::new( - Txid::from_byte_array(*args.assert.txid.as_raw_hash().as_byte_array()), - args.assert.vout, - ), - )?[disprove_script]; - - let tx = { - let mut buf = Vec::new(); - tx.consensus_encode(&mut buf)?; - let mut cursor = std::io::Cursor::new(buf); - bitcoincore_rpc::bitcoin::Transaction::consensus_decode(&mut cursor)? - }; - - println!("{}", tx.txid()); - - ctx.client().send_raw_transaction(&tx)?; - - Ok(()) -} - -const DEFAULT_OUTPUT_FILE: &str = "input.txt"; - -#[derive(Args, Debug, Clone)] -pub struct GenerateInputArgs { - #[arg(long)] - output: Option, -} - -pub fn generate_input(_ctx: Context, args: GenerateInputArgs) -> eyre::Result<()> { - let output = args.output.unwrap_or_else(|| DEFAULT_OUTPUT_FILE.into()); - - let IOPair { input, .. } = SquareFibonacciScript::<1024>::generate_valid_io_pair(); - - println!("{}", input.to_asm_string()); - - fs::write(output, input.to_hex_string())?; - - Ok(()) -} - -pub fn generate_keys(ctx: Context) -> eyre::Result<()> { - let (seckey, pubkey) = ctx.secp_ctx().generate_keypair(&mut thread_rng()); - - println!("{}", seckey.display_secret()); - println!("{}", pubkey.x_only_public_key().0); - - Ok(()) -} diff --git a/nero-cli/src/cli/mod.rs b/nero-cli/src/cli/mod.rs deleted file mode 100644 index a25a183..0000000 --- a/nero-cli/src/cli/mod.rs +++ /dev/null @@ -1,55 +0,0 @@ -use std::path::PathBuf; - -use clap::{Parser, Subcommand}; -use clap_verbosity::Verbosity; -use tracing_log::AsTrace as _; - -use crate::{config::Config, context::Context}; - -use self::actions::{AssertTxArgs, DisproveSpendArgs, GenerateInputArgs, PayoutSpendArgs}; - -mod actions; - -#[derive(Parser)] -pub struct Cli { - #[command(flatten)] - verbosity: Verbosity, - - #[arg(long)] - config: Option, - - #[command(subcommand)] - cmd: Command, -} - -impl Cli { - pub fn parse() -> Self { - ::parse() - } - - pub fn run(self) -> eyre::Result<()> { - tracing_subscriber::fmt() - .with_max_level(self.verbosity.log_level_filter().as_trace()) - .init(); - - let config = Config::load(self.config.clone())?; - let context = Context::from_config(config)?; - - match self.cmd { - Command::AssertTx(args) => actions::assert_tx(context, args), - Command::GenerateInput(args) => actions::generate_input(context, args), - Command::SpendPayout(args) => actions::spend_payout(context, args), - Command::SpendDisprove(args) => actions::spend_disprove(context, args), - Command::GenerateKeys => actions::generate_keys(context), - } - } -} - -#[derive(Subcommand, Debug, Clone)] -pub enum Command { - AssertTx(AssertTxArgs), - GenerateInput(GenerateInputArgs), - SpendPayout(PayoutSpendArgs), - SpendDisprove(DisproveSpendArgs), - GenerateKeys, -} diff --git a/nero-cli/src/config.rs b/nero-cli/src/config.rs deleted file mode 100644 index 6e4e5e7..0000000 --- a/nero-cli/src/config.rs +++ /dev/null @@ -1,31 +0,0 @@ -use std::{fs, path::PathBuf}; - -use dirs_next::config_dir; - -#[derive(serde::Deserialize, serde::Serialize)] -pub struct Config { - pub bitcoin_password: String, - pub bitcoin_username: String, - pub bitcoin_url: String, -} - -impl Config { - pub fn load(path: Option) -> eyre::Result { - let config_file = path.unwrap_or_else(|| { - config_dir() - .map(|base| base.join("nero").join("config.toml")) - .unwrap() - }); - - let src = fs::read_to_string(config_file)?; - - toml::from_str(&src).map_err(Into::into) - } - - pub fn bitcoinrpc_auth(&self) -> bitcoincore_rpc::Auth { - bitcoincore_rpc::Auth::UserPass( - self.bitcoin_username.clone(), - self.bitcoin_password.clone(), - ) - } -} diff --git a/nero-cli/src/context.rs b/nero-cli/src/context.rs deleted file mode 100644 index c453130..0000000 --- a/nero-cli/src/context.rs +++ /dev/null @@ -1,26 +0,0 @@ -use bitcoin::{key::Secp256k1, secp256k1::All}; - -use crate::config::Config; - -pub struct Context { - client: bitcoincore_rpc::Client, - - secp: Secp256k1, -} - -impl Context { - pub fn from_config(config: Config) -> eyre::Result { - let client = bitcoincore_rpc::Client::new(&config.bitcoin_url, config.bitcoinrpc_auth())?; - let secp = Secp256k1::new(); - - Ok(Self { client, secp }) - } - - pub fn client(&self) -> &bitcoincore_rpc::Client { - &self.client - } - - pub fn secp_ctx(&self) -> &Secp256k1 { - &self.secp - } -} diff --git a/nero-cli/src/main.rs b/nero-cli/src/main.rs deleted file mode 100644 index 901461a..0000000 --- a/nero-cli/src/main.rs +++ /dev/null @@ -1,15 +0,0 @@ -use cli::Cli; - -mod cli; -mod config; -mod context; - -fn main() -> eyre::Result<()> { - color_eyre::install()?; - - let cli = Cli::parse(); - - cli.run()?; - - Ok(()) -} diff --git a/core/Cargo.toml b/nero-core/Cargo.toml similarity index 91% rename from core/Cargo.toml rename to nero-core/Cargo.toml index b95cfaf..aaaf472 100644 --- a/core/Cargo.toml +++ b/nero-core/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "bitvm2-core" +name = "nero-core" version = "0.1.0" edition = "2021" @@ -8,7 +8,6 @@ edition = "2021" bitcoin = { workspace = true, features = ["rand-std"]} bitcoin-script = { git = "https://github.com/BitVM/rust-bitcoin-script" } bitcoin-scriptexec = { path = "../bitcoin-scriptexec" } -bitcoin-script-stack = { git = "https://github.com/FairgateLabs/rust-bitcoin-script-stack"} # BitVM scripts bitcoin-window-mul.workspace = true @@ -38,4 +37,6 @@ ark-std = "0.4.0" konst = "0.3.9" once_cell = "1.19.0" eyre = "0.6.12" +itertools = "0.13.0" +musig2 = { version = "0.0.11", features = ["rand"] } diff --git a/nero-core/src/assert/mod.rs b/nero-core/src/assert/mod.rs new file mode 100644 index 0000000..6208b50 --- /dev/null +++ b/nero-core/src/assert/mod.rs @@ -0,0 +1,301 @@ +use std::iter; + +use bitcoin::{ + absolute::LockTime, + key::{constants::SCHNORR_SIGNATURE_SIZE, Secp256k1, Verification}, + relative::Height, + sighash::{Prevouts, SighashCache}, + taproot::{self, ControlBlock, LeafVersion, TaprootBuilder, TaprootSpendInfo}, + transaction::Version, + Amount, FeeRate, OutPoint, ScriptBuf, Sequence, TapLeafHash, TapSighashType, Transaction, TxIn, + TxOut, Txid, Weight, Witness, XOnlyPublicKey, +}; +use bitcoin_splitter::split::script::SplitableScript; +use musig2::{ + secp256k1::{schnorr::Signature, PublicKey, SecretKey, Signing}, + AggNonce, PartialSignature, SecNonce, +}; + +use crate::{ + claim::{scripts::AssertScript, FundedClaim}, + context::Context, + disprove::{extract_signed_states, signing::SignedIntermediateState, DisproveScript}, + payout::PayoutScript, + schnorr_sign_partial, UNSPENDABLE_KEY, +}; + +const DISPROVE_SCRIPT_WEIGHT: u32 = 1; +const PAYOUT_SCRIPT_WEIGHT: u32 = 5; + +pub struct Assert { + claim_txid: Txid, + disprove_scripts: Vec, + payout_script: PayoutScript, + staked_amount: Amount, +} + +impl Assert { + pub fn from_context( + ctx: &Context, + claim_txid: Txid, + fee_rate: FeeRate, + ) -> Self { + Self { + claim_txid, + disprove_scripts: ctx.disprove_scripts.clone(), + payout_script: PayoutScript::with_locktime( + ctx.operator_pubkey.into(), + ctx.comittee_aggpubkey(), + ctx.assert_challenge_period, + ), + staked_amount: ctx.staked_amount + + fee_rate + .checked_mul_by_weight(ctx.largest_disprove_weight) + .unwrap(), + } + } + + pub fn new( + disprove_scripts: &[DisproveScript], + operator_pubkey: XOnlyPublicKey, + assert_challenge_period: Height, + claim_txid: Txid, + comittee_aggpubkey: XOnlyPublicKey, + staked_amount: Amount, + ) -> Self { + Self { + claim_txid, + disprove_scripts: disprove_scripts.to_owned(), + payout_script: PayoutScript::with_locktime( + operator_pubkey, + comittee_aggpubkey, + assert_challenge_period, + ), + staked_amount, + } + } + + pub fn taproot(&self, ctx: &Secp256k1) -> TaprootSpendInfo + where + C: Verification, + { + let scripts_with_weights = + iter::once((PAYOUT_SCRIPT_WEIGHT, self.payout_script.to_script())).chain( + self.disprove_scripts + .iter() + .map(|script| (DISPROVE_SCRIPT_WEIGHT, script.to_script_pubkey())), + ); + + TaprootBuilder::with_huffman_tree(scripts_with_weights) + .expect("Weights are low, and number of scripts shoudn't create the tree greater than 128 in depth (I believe)") + .finalize(ctx, *UNSPENDABLE_KEY) + .expect("Scripts and keys should be valid") + } + + pub fn to_unsigned_tx(&self, ctx: &Secp256k1) -> Transaction + where + C: Verification, + { + Transaction { + version: Version::ONE, + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint::new(self.claim_txid, 0), + script_sig: ScriptBuf::new(), + sequence: Sequence::ZERO, + witness: Witness::new(), + }], + output: vec![self.output(ctx)], + } + } + + fn to_unsigned_tx_with_witness(&self, ctx: &Secp256k1) -> Transaction { + let mut unsigned_tx = self.to_unsigned_tx(ctx); + + let witness = &mut unsigned_tx.input[0].witness; + + for disprove in &self.disprove_scripts { + for element in disprove.to_witness_stack_elements() { + witness.push(element); + } + } + + unsigned_tx + } + + pub fn compute_weight(&self, ctx: &Secp256k1) -> Weight { + let unsigned_tx = self.to_unsigned_tx_with_witness(ctx); + + unsigned_tx.weight() + /* comitte signature */ Weight::from_witness_data_size(SCHNORR_SIGNATURE_SIZE as u64) + } + + pub fn compute_txid(&self, ctx: &Secp256k1) -> Txid { + let unsigned_tx = self.to_unsigned_tx(ctx); + unsigned_tx.compute_txid() + } + + pub fn output(&self, ctx: &Secp256k1) -> TxOut { + let taproot = self.taproot(ctx); + TxOut { + value: self.staked_amount, + script_pubkey: ScriptBuf::new_p2tr_tweaked(taproot.output_key()), + } + } + + /// Create partial Schnorr signatures from claim transaction. + pub fn sign_partial_from_claim( + &self, + ctx: &Secp256k1, + claim: &FundedClaim, + comittee_pubkeys: Vec, + agg_nonce: &AggNonce, + secret_key: SecretKey, + secnonce: SecNonce, + ) -> PartialSignature { + let claim_assert_output = &claim.to_tx(ctx).output[0]; + let claim_assert_script = claim.assert_script(); + + self.sign_partial( + ctx, + claim_assert_output, + claim_assert_script, + comittee_pubkeys, + agg_nonce, + secret_key, + secnonce, + ) + } + + // Let's reconsider the number of parameters later. + #[allow(clippy::too_many_arguments)] + /// Partially sign transaction using operator's key. + pub fn sign_partial<'a, C: Verification + Signing>( + &self, + ctx: &Secp256k1, + claim_assert_output: &TxOut, + claim_assert_script: AssertScript<'a, impl Iterator>, + comittee_pubkeys: Vec, + agg_nonce: &AggNonce, + secret_key: SecretKey, + secnonce: SecNonce, + ) -> PartialSignature { + let sighash = self.sighash(ctx, claim_assert_output, claim_assert_script); + + schnorr_sign_partial( + ctx, + sighash, + comittee_pubkeys, + agg_nonce, + secret_key, + secnonce, + ) + } + + /// Return sighash of assert transaction for signing. + pub(crate) fn sighash<'a, C: Verification>( + &self, + ctx: &Secp256k1, + claim_assert_output: &TxOut, + claim_assert_script: AssertScript<'a, impl Iterator>, + ) -> bitcoin::TapSighash { + let script = claim_assert_script.into_script(); + let leaf_hash = TapLeafHash::from_script(&script, LeafVersion::TapScript); + let unsigned_tx = self.to_unsigned_tx(ctx); + + SighashCache::new(&unsigned_tx) + .taproot_script_spend_signature_hash( + 0, + &Prevouts::All(&[claim_assert_output]), + leaf_hash, + TapSighashType::Default, + ) + .unwrap() + } + + pub fn payout_script(&self) -> &PayoutScript { + &self.payout_script + } +} + +pub struct SignedAssert { + inner: Assert, + signature: taproot::Signature, + assert_script: ScriptBuf, + assert_script_control_block: ControlBlock, +} + +impl SignedAssert { + pub fn new<'a>( + inner: Assert, + signature: impl Into, + assert_script: AssertScript<'a, impl Iterator>, + assert_script_control_block: ControlBlock, + ) -> Self { + Self { + inner, + signature: taproot::Signature { + signature: signature.into(), + sighash_type: TapSighashType::Default, + }, + assert_script: assert_script.into_script(), + assert_script_control_block, + } + } + + /// Return signed transaction which is ready fo publishing. + pub fn to_tx(&self, ctx: &Secp256k1) -> Transaction { + let mut unsigned_tx = self.inner.to_unsigned_tx(ctx); + + let witness = &mut unsigned_tx.input[0].witness; + + let signed_states = extract_signed_states(&self.inner.disprove_scripts); + + /* Push witness stack */ + for state in signed_states.rev() { + for element in state.witness_elements() { + witness.push(element); + } + } + witness.push(self.signature.serialize()); + + /* Push script */ + witness.push(self.assert_script.clone()); + + /* Push control block */ + witness.push(self.assert_script_control_block.serialize()); + + unsigned_tx + } + + pub fn payout_script(&self) -> &PayoutScript { + &self.inner.payout_script + } + + pub fn disprove_script(&self, idx: usize) -> &DisproveScript { + &self.inner.disprove_scripts[idx] + } + + pub fn payout_script_control_block(&self, ctx: &Secp256k1) -> ControlBlock { + let script = self.inner.payout_script(); + + let taptree = self.inner.taproot(ctx); + + taptree + .control_block(&(script.to_script(), LeafVersion::TapScript)) + .expect("Payout script is included into taptree") + } + + pub fn disprove_script_control_block( + &self, + ctx: &Secp256k1, + idx: usize, + ) -> ControlBlock { + let script = self.inner.disprove_scripts[idx].to_script_pubkey(); + + let taptree = self.inner.taproot(ctx); + + taptree + .control_block(&(script, LeafVersion::TapScript)) + .expect("Payout script is included into taptree") + } +} diff --git a/nero-core/src/challenge/mod.rs b/nero-core/src/challenge/mod.rs new file mode 100644 index 0000000..78af070 --- /dev/null +++ b/nero-core/src/challenge/mod.rs @@ -0,0 +1,116 @@ +//! Provides challenge transaction. + +use bitcoin::{ + absolute::LockTime, + key::{Keypair, Parity, Secp256k1}, + sighash::{Prevouts, SighashCache}, + taproot::Signature, + transaction::Version, + Amount, OutPoint, ScriptBuf, Sequence, TapSighashType, TapTweakHash, Transaction, TxIn, TxOut, + Txid, Witness, +}; +use musig2::secp256k1::{schnorr, SecretKey, Signing}; + +const SIGHASHTYPE: TapSighashType = TapSighashType::SinglePlusAnyoneCanPay; + +pub struct Challenge { + /// ID of Claim transaction. + claim_txid: Txid, + + /// Crowdfundedd amount in BTC required for operator to pay the fee for + /// assert and disprove transaction. + assert_tx_fee_amount: Amount, + + /// Public key of operator which will get + /// [`Challenge::assert_tx_fee_amount`] after the challenge transcation is + /// published. + operator_script_pubkey: ScriptBuf, +} + +impl Challenge { + pub fn new(operator: ScriptBuf, claim_txid: Txid, assert_tx_fee_amount: Amount) -> Self { + Self { + claim_txid, + assert_tx_fee_amount, + operator_script_pubkey: operator, + } + } + + pub fn to_unsigned_tx(&self) -> Transaction { + Transaction { + version: Version::ONE, + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint::new(self.claim_txid, 1), + script_sig: ScriptBuf::new(), + sequence: Sequence::ZERO, + witness: Witness::new(), + }], + output: vec![TxOut { + value: self.assert_tx_fee_amount, + script_pubkey: self.operator_script_pubkey.clone(), + }], + } + } + + pub fn sign( + self, + ctx: &Secp256k1, + claim_challenge_txout: &TxOut, + mut operator_seckey: SecretKey, + ) -> SignedChallenge { + let unsigned_tx = self.to_unsigned_tx(); + const INPUT_INDEX: usize = /* challenge has only one input */ 0; + let sighash = SighashCache::new(unsigned_tx) + .taproot_key_spend_signature_hash( + INPUT_INDEX, + &Prevouts::All(&[claim_challenge_txout]), + SIGHASHTYPE, + ) + .unwrap(); + + let (xonly, parity) = operator_seckey.public_key(ctx).x_only_public_key(); + if parity == Parity::Odd { + operator_seckey = operator_seckey.negate(); + } + let tweak = TapTweakHash::from_key_and_tweak(xonly, None); + operator_seckey = operator_seckey.add_tweak(&tweak.to_scalar()).unwrap(); + + let sig = ctx.sign_schnorr( + &sighash.into(), + &Keypair::from_secret_key(ctx, &operator_seckey), + ); + + SignedChallenge::new(self, sig) + } +} + +pub struct SignedChallenge { + /// Unsigned challenge transaciton. + inner: Challenge, + + /// Operator's signature for Single + AnyoneCanPay + operators_sig: Signature, +} + +impl SignedChallenge { + pub(crate) fn new(inner: Challenge, sig: impl Into) -> Self { + Self { + inner, + operators_sig: Signature { + signature: sig.into(), + sighash_type: SIGHASHTYPE, + }, + } + } + + pub fn to_tx(&self) -> Transaction { + let mut unsigned_tx = self.inner.to_unsigned_tx(); + + // add signature to tx, converting it to signed one. + let witness = &mut unsigned_tx.input[0].witness; + witness.push(self.operators_sig.serialize()); + + unsigned_tx + } +} diff --git a/nero-core/src/claim/mod.rs b/nero-core/src/claim/mod.rs new file mode 100644 index 0000000..ba852de --- /dev/null +++ b/nero-core/src/claim/mod.rs @@ -0,0 +1,231 @@ +//! Clain transaction. + +use bitcoin::{ + absolute::LockTime, + key::{Secp256k1, Verification}, + relative::Height, + taproot::{ControlBlock, LeafVersion, TaprootBuilder, TaprootSpendInfo}, + transaction::Version, + Amount, FeeRate, Transaction, TxIn, TxOut, Txid, XOnlyPublicKey, +}; +use bitcoin_splitter::split::script::SplitableScript; + +use crate::{ + context::Context, disprove::signing::SignedIntermediateState, treepp::*, UNSPENDABLE_KEY, +}; + +use self::scripts::{AssertScript, OptimisticPayoutScript}; + +pub(crate) mod scripts; + +const DUST_AMOUNT: Amount = Amount::from_sat(1_000); + +pub struct Claim { + /// Amount stacked for claim + amount: Amount, + + /// Public keys of comittee prepared for aggregation. + comittee_aggpubkey: XOnlyPublicKey, + + /// Claim transaction challenge period. + claim_challenge_period: Height, + + /// Output of the operator's wallet for spending + operator_pubkey: XOnlyPublicKey, + + /// All signed states + signed_states: Vec, +} + +impl Claim { + pub fn from_context( + ctx: &Context, + fee_rate: FeeRate, + ) -> Self { + Self { + amount: ctx.staked_amount + + fee_rate + .checked_mul_by_weight(ctx.assert_tx_weight + ctx.largest_disprove_weight) + .unwrap(), + operator_pubkey: ctx.operator_pubkey.into(), + comittee_aggpubkey: ctx.comittee_aggpubkey(), + claim_challenge_period: ctx.claim_challenge_period, + signed_states: ctx.signed_states(), + } + } + + pub fn challenge_output(&self, ctx: &Secp256k1) -> TxOut { + TxOut { + value: DUST_AMOUNT, + script_pubkey: Script::new_p2tr(ctx, self.operator_pubkey, None), + } + } + + pub fn assert_output(&self, ctx: &Secp256k1) -> TxOut + where + C: Verification, + { + let taptree = self.taptree(ctx); + + TxOut { + value: self.amount, + script_pubkey: Script::new_p2tr_tweaked(taptree.output_key()), + } + } + + fn taptree(&self, ctx: &Secp256k1) -> TaprootSpendInfo { + TaprootBuilder::with_huffman_tree([ + (9, self.optimistic_payout_script().to_script()), + (1, self.assert_script().into_script()), + ]) + .unwrap() + .finalize(ctx, *UNSPENDABLE_KEY) + .unwrap() + } + + pub fn optimistic_payout_script(&self) -> OptimisticPayoutScript { + OptimisticPayoutScript::new(self.claim_challenge_period, self.comittee_aggpubkey) + } + + pub fn assert_script( + &self, + ) -> AssertScript<'_, impl Iterator> { + AssertScript::new(self.comittee_aggpubkey, self.signed_states.iter()) + } + + pub fn to_unsigned_tx(&self, ctx: &Secp256k1) -> Transaction + where + C: Verification, + { + let challenge_output = self.challenge_output(ctx); + let assert_output = self.assert_output(ctx); + + Transaction { + // Requires for OP_CSV + version: Version::ONE, + lock_time: LockTime::ZERO, + // Inputs are empty as they should be funded by wallet. + input: vec![], + output: vec![assert_output, challenge_output], + } + } +} + +pub struct FundedClaim { + /// Inner clain transaction. + claim: Claim, + /// $x$ - input of the program + #[allow(dead_code)] + input: Script, + /// Funding input got from external wallet. + funding_inputs: Vec, + /// The change output created after funding the claim tx. + change_output: Option, +} + +impl FundedClaim { + /// Construct new funded claim transaction. + pub fn new( + claim: Claim, + funding_inputs: Vec, + change_output: Option, + program_input: Script, + ) -> Self { + Self { + claim, + funding_inputs, + input: program_input, + change_output, + } + } + + /// Construct bitcoin transaction from funded claim. + pub fn to_tx(&self, ctx: &Secp256k1) -> Transaction + where + C: Verification, + { + let mut unsigned_tx = self.claim.to_unsigned_tx(ctx); + + // Fullfill the last elements of witness stack + // for instruction in self.input.instructions() { + // match instruction.unwrap() { + // Instruction::PushBytes(bytes) => { + // funding_input.witness.push(bytes.as_bytes()); + // } + // Instruction::Op(opcode) => { + // match opcode.classify(ClassifyContext::TapScript) { + // bitcoin::opcodes::Class::PushNum(num) => { + // let buf: Vec = + // num.to_le_bytes().into_iter().filter(|b| *b != 0).collect(); + // funding_input.witness.push(buf); + // } + // _ => { + // unreachable!("script witness shouldn't have opcodes, got {opcode}") + // } + // }; + // } + // } + // } + + unsigned_tx.input.clone_from(&self.funding_inputs); + if let Some(output) = &self.change_output { + unsigned_tx.output.push(output.clone()); + } + + unsigned_tx + } + + pub fn compute_txid(&self, ctx: &Secp256k1) -> Txid { + let tx = self.to_tx(ctx); + tx.compute_txid() + } + + pub fn optimistic_payout_script(&self) -> OptimisticPayoutScript { + self.claim.optimistic_payout_script() + } + + pub fn assert_script( + &self, + ) -> AssertScript<'_, impl Iterator> { + self.claim.assert_script() + } + + // TODO(Velnbur): Current implementation is memory inefficient. Here we + // contruct script twice, in taptree creation and fetching the control + // block after it. + pub fn assert_script_control_block(&self, ctx: &Secp256k1) -> ControlBlock { + let taptree = self.claim.taptree(ctx); + + taptree + .control_block(&(self.assert_script().into_script(), LeafVersion::TapScript)) + .expect("taptree was constructed including assert script!") + } + + // TODO(Velnbur): Current implementation is memory inefficient. Here we + // contruct script twice, in taptree creation and fetching the control + // block after it. + pub fn optimistic_payout_script_control_block( + &self, + ctx: &Secp256k1, + ) -> ControlBlock { + let taptree = self.claim.taptree(ctx); + + taptree + .control_block(&( + self.optimistic_payout_script().to_script(), + LeafVersion::TapScript, + )) + .expect("taptree was constructed including assert script!") + } + + pub fn assert_output(&self, ctx: &Secp256k1) -> TxOut + where + C: Verification, + { + self.claim.assert_output(ctx) + } + + pub(crate) fn challenge_output(&self, ctx: &Secp256k1) -> TxOut { + self.claim.challenge_output(ctx) + } +} diff --git a/nero-core/src/claim/scripts.rs b/nero-core/src/claim/scripts.rs new file mode 100644 index 0000000..eea78b7 --- /dev/null +++ b/nero-core/src/claim/scripts.rs @@ -0,0 +1,83 @@ +use bitcoin::{relative::Height, XOnlyPublicKey}; +use bitcoin_script::script; +use bitcoin_winternitz::u32::N0; + +use crate::disprove::signing::SignedIntermediateState; +use crate::scripts::{OP_CHECKCOVENANT, OP_CHECKCOVENANTVERIFY}; +use crate::treepp::*; + +/// Script which is spent in optimistic payout flow after challenge period +/// ends. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct OptimisticPayoutScript { + claim_challenge_period: Height, + comittee_agg_pubkey: XOnlyPublicKey, +} + +impl OptimisticPayoutScript { + pub fn new(claim_challenge_period: Height, comittee_agg_pubkey: XOnlyPublicKey) -> Self { + Self { + claim_challenge_period, + comittee_agg_pubkey, + } + } + + pub fn to_script(self) -> Script { + script! { + { self.claim_challenge_period.value() as i32 } + OP_CSV + OP_DROP + { OP_CHECKCOVENANT(self.comittee_agg_pubkey) } + } + } +} + +/// `AssertScript` from original BitVM2 paper. Checks from stack all +/// Winternitz commitments for signed intermidiate states. +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct AssertScript<'a, I> +where + I: Iterator, +{ + comittee_agg_pubkey: XOnlyPublicKey, + states: I, +} + +impl<'a, I> AssertScript<'a, I> +where + I: Iterator, +{ + pub fn new(comittee_agg_pubkey: XOnlyPublicKey, states: I) -> Self { + Self { + comittee_agg_pubkey, + states, + } + } + + pub fn into_script(self) -> Script { + script! { + { OP_CHECKCOVENANTVERIFY(self.comittee_agg_pubkey) } + for state in self.states { + for element in &state.altstack { + { element.public_key.checksig_verify_script() } + // FIXME(Velnbur): in script above we copied and + // pushed the elements, but immidiaatle droped here. + // That's why we should create a separate script where + // we don't copy it on the stack and only check for + // existance. + for _ in 0..N0 { + OP_DROP + } + } + for element in state.stack.iter().rev() { + { element.public_key.checksig_verify_script() } + // FIXME(Velnbur): the same here + for _ in 0..N0 { + OP_DROP + } + } + } + OP_TRUE + } + } +} diff --git a/nero-core/src/context.rs b/nero-core/src/context.rs new file mode 100644 index 0000000..abdcb79 --- /dev/null +++ b/nero-core/src/context.rs @@ -0,0 +1,265 @@ +//! Provides BitVM2 flow context. + +use std::{iter, marker::PhantomData, str::FromStr}; + +use bitcoin::{ + key::{Secp256k1, Verification}, + relative::Height, + secp256k1::PublicKey, + taproot::LeafVersion, + Amount, Txid, Weight, +}; +use bitcoin_splitter::split::script::SplitableScript; +use musig2::{secp::Point, KeyAggContext}; + +use crate::{ + assert::Assert, + disprove::{ + form_disprove_scripts_distorted_with_seed, form_disprove_scripts_with_seed, + signing::SignedIntermediateState, Disprove, DisproveScript, + }, + payout::PAYOUT_APPROX_WEIGHT, + treepp::*, +}; + +/// Global context of BitVM2 flow. +pub struct Context { + pub(crate) secp: Secp256k1, + + /// $x$ - the input of the program flow asserts. + #[allow(dead_code)] + pub(crate) input: Script, + + /// The splitted into disprove scripts program. + pub(crate) disprove_scripts: Vec, + + /// Fresh secret key generated for current session. + pub(crate) operator_pubkey: PublicKey, + + /// Fresh secret key generated for current session. + pub(crate) operator_script_pubkey: Script, + + /// Public keys of comitte for emulating covenants. + pub(crate) comittee: Vec, + + /// Claim transaction challenge period. + pub(crate) claim_challenge_period: Height, + + /// Assert transaction challenge period. + pub(crate) assert_challenge_period: Height, + + /// Stacked amount mentioned in paper as $d$. + pub(crate) staked_amount: Amount, + + /// Transaction weights of disprove transaction in the same order. + /// + /// It's required for calculating the fee for disprove transaction. + pub(crate) largest_disprove_weight: Weight, + + /// Assert transaction weight. + /// + /// It's required to calculate it before hand as with current fee rate we + /// can predict the fee required for assert transaction. + pub(crate) assert_tx_weight: Weight, + + pub(crate) payout_tx_weight: Weight, + + /// Program that current flow asserts. + __program: PhantomData, +} + +impl Context { + /// Setup context for BitVM2 flow. + #[allow(clippy::too_many_arguments)] + pub fn compute_setup( + ctx: Secp256k1, + staked_amount: Amount, + input: Script, + claim_challenge_period: Height, + assert_challenge_period: Height, + operator_pubkey: PublicKey, + operator_script_pubkey: Script, + mut comittee: Vec, + seed: Seed, + ) -> Self + where + Seed: Sized + Default + AsMut<[u8]> + Copy, + Rng: rand::SeedableRng + rand::Rng, + { + // Always sort the order of keys in comittee before doing anything. + comittee.sort(); + let key_ctx = + KeyAggContext::new(iter::once(operator_pubkey).chain(comittee.clone())).unwrap(); + let comittee_aggpubkey = key_ctx.aggregated_pubkey(); + + let disprove_scripts = form_disprove_scripts_with_seed::( + input.clone(), + comittee_aggpubkey, + seed, + ); + + let dummy_txid = + Txid::from_str("6ac23d25c784f97c75a0ebd5985d6db0c8c4b4c1f6d0bd684b5a2087b7abeb30") + .expect("const valid txid"); + + let assert_tx = Assert::new( + &disprove_scripts, + operator_pubkey.into(), + assert_challenge_period, + dummy_txid, + comittee_aggpubkey, + Amount::ZERO, + ); + let taproot = assert_tx.taproot(&ctx); + + let disprove_txs = disprove_scripts + .iter() + .map(|script| { + Disprove::new( + script, + dummy_txid, + taproot + // TODO(Velnbur): another place which generates a large chunk of memory + // just for getting a control block. We should create a PR in rust-bitcoin to + // avoid that. + .control_block(&(script.to_script_pubkey(), LeafVersion::TapScript)) + .unwrap(), + ) + }) + .collect::>(); + + let assert_tx_weight = assert_tx.compute_weight(&ctx); + + Self { + staked_amount, + input, + secp: ctx, + operator_pubkey, + operator_script_pubkey, + largest_disprove_weight: disprove_txs + .iter() + .map(|tx| tx.compute_weigth()) + .max() + .unwrap(), + disprove_scripts, + claim_challenge_period, + assert_challenge_period, + comittee, + assert_tx_weight, + payout_tx_weight: PAYOUT_APPROX_WEIGHT, + __program: Default::default(), + } + } + + /// Setup context for BitVM2 flow. + #[allow(clippy::too_many_arguments)] + pub fn compute_setup_distorted( + ctx: Secp256k1, + staked_amount: Amount, + input: Script, + claim_challenge_period: Height, + assert_challenge_period: Height, + operator_pubkey: PublicKey, + operator_script_pubkey: Script, + mut comittee: Vec, + seed: Seed, + ) -> Self + where + Seed: Sized + Default + AsMut<[u8]> + Copy, + Rng: rand::SeedableRng + rand::Rng, + { + // Always sort the order of keys in comittee before doing anything. + comittee.sort(); + let key_ctx = + KeyAggContext::new(iter::once(operator_pubkey).chain(comittee.clone())).unwrap(); + let comittee_aggpubkey = key_ctx.aggregated_pubkey(); + + let (disprove_scripts, _) = form_disprove_scripts_distorted_with_seed::( + input.clone(), + comittee_aggpubkey, + seed, + ); + + let dummy_txid = + Txid::from_str("6ac23d25c784f97c75a0ebd5985d6db0c8c4b4c1f6d0bd684b5a2087b7abeb30") + .expect("const valid txid"); + + let assert_tx = Assert::new( + &disprove_scripts, + operator_pubkey.into(), + assert_challenge_period, + dummy_txid, + comittee_aggpubkey, + Amount::ZERO, + ); + let taproot = assert_tx.taproot(&ctx); + + let disprove_txs = disprove_scripts + .iter() + .map(|script| { + Disprove::new( + script, + dummy_txid, + taproot + // TODO(Velnbur): another place which generates a large chunk of memory + // just for getting a control block. We should create a PR in rust-bitcoin to + // avoid that. + .control_block(&(script.to_script_pubkey(), LeafVersion::TapScript)) + .unwrap(), + ) + }) + .collect::>(); + + let assert_tx_weight = assert_tx.compute_weight(&ctx); + + Self { + staked_amount, + input, + secp: ctx, + operator_pubkey, + operator_script_pubkey, + largest_disprove_weight: disprove_txs + .iter() + .map(|tx| tx.compute_weigth()) + .max() + .unwrap(), + disprove_scripts, + claim_challenge_period, + assert_challenge_period, + comittee, + assert_tx_weight, + payout_tx_weight: PAYOUT_APPROX_WEIGHT, + __program: Default::default(), + } + } + + /// Comittee aggregated public key. + pub(crate) fn comittee_aggpubkey>(&self) -> T { + let ctx = self.comittee_keyaggctx(); + + ctx.aggregated_pubkey() + } + + pub(crate) fn comittee_keyaggctx(&self) -> KeyAggContext { + let mut points = iter::once(self.operator_pubkey) + .chain(self.comittee.clone()) + .collect::>(); + points.sort(); + KeyAggContext::new(points).unwrap() + } + + /// Return list of signed states from disprove scripts. + pub(crate) fn signed_states(&self) -> Vec { + iter::once(self.disprove_scripts[0].from_state.clone()) + .chain(self.disprove_scripts.iter().map(|d| d.to_state.clone())) + .collect() + } + + pub fn comittee(&self) -> &[PublicKey] { + &self.comittee + } + + pub fn operator_pubkey(&self) -> PublicKey { + self.operator_pubkey + } +} diff --git a/nero-core/src/disprove/mod.rs b/nero-core/src/disprove/mod.rs new file mode 100644 index 0000000..f72f335 --- /dev/null +++ b/nero-core/src/disprove/mod.rs @@ -0,0 +1,471 @@ +use std::iter; + +use bitcoin::{ + absolute::LockTime, + key::{constants::SCHNORR_SIGNATURE_SIZE, Secp256k1, TweakedPublicKey, Verification}, + sighash::{Prevouts, SighashCache}, + taproot::{ControlBlock, LeafVersion, Signature}, + transaction::Version, + Amount, OutPoint, Sequence, TapLeafHash, TapSighashType, Transaction, TxIn, TxOut, Txid, + Weight, Witness, XOnlyPublicKey, +}; +use bitcoin_utils::{comparison::OP_LONGNOTEQUAL, pseudo::OP_LONGFROMALTSTACK, treepp::*}; + +use itertools::Itertools; +use musig2::{ + secp256k1::{schnorr, PublicKey, SecretKey, Signing}, + AggNonce, PartialSignature, SecNonce, +}; +use signing::SignedIntermediateState; + +use bitcoin_splitter::split::{ + core::SplitType, + intermediate_state::IntermediateState, + script::{SplitResult, SplitableScript}, +}; + +use crate::{schnorr_sign_partial, scripts::OP_CHECKCOVENANTVERIFY, UNSPENDABLE_KEY}; + +pub mod signing; + +#[cfg(test)] +mod tests; + +const SIGHASHTYPE: TapSighashType = TapSighashType::Single; + +pub struct Disprove { + /// Disprove script. + /// + /// Used for calculating the burn amount, particlarly the weight of Disprove transaction. + script: DisproveScript, + /// ID of Assert transaction which output disprove tx spends. + assert_txid: Txid, + /// Control block required for spending the disprove. + control_block: ControlBlock, +} + +impl Disprove { + pub fn new(script: &DisproveScript, assert_txid: Txid, control_block: ControlBlock) -> Self { + Self { + script: script.clone(), + assert_txid, + control_block, + } + } + + pub fn to_unsigned_tx(&self) -> Transaction { + Transaction { + version: Version::ONE, + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint::new(self.assert_txid, 0), + script_sig: Script::new(), + sequence: Sequence::ZERO, + witness: Witness::new(), + }], + output: vec![ + // Burn output + TxOut { + // TODO(Velnbur): calculate by ourself or make the amount configurable. + value: Amount::from_sat(4_000), + script_pubkey: Script::new_p2tr_tweaked( + TweakedPublicKey::dangerous_assume_tweaked(*UNSPENDABLE_KEY), + ), + }, + ], + } + } + + #[allow(clippy::too_many_arguments)] + pub fn sign_partial( + &self, + ctx: &Secp256k1, + assert_output: &TxOut, + comittee_pubkeys: Vec, + agg_nonce: &AggNonce, + secret_key: SecretKey, + secnonce: SecNonce, + ) -> PartialSignature { + let sighash = self.sighash(assert_output); + + schnorr_sign_partial( + ctx, + sighash, + comittee_pubkeys, + agg_nonce, + secret_key, + secnonce, + ) + } + + pub(crate) fn sighash(&self, assert_txout: &TxOut) -> bitcoin::TapSighash { + let unsigned_tx = self.to_unsigned_tx(); + + SighashCache::new(&unsigned_tx) + .taproot_script_spend_signature_hash( + /* assert output spending input is the first one */ 0, + &Prevouts::All(&[assert_txout]), + TapLeafHash::from_script(&self.script.to_script_pubkey(), LeafVersion::TapScript), + SIGHASHTYPE, + ) + .unwrap() + } + + fn unsigned_tx_with_witness(&self) -> Transaction { + let mut unsigned_tx = self.to_unsigned_tx(); + + let witness = &mut unsigned_tx.input[0].witness; + + for element in self.script.to_witness_stack_elements() { + witness.push(element); + } + + witness.push(self.script.to_script_pubkey()); + witness.push(self.control_block.serialize()); + unsigned_tx + } + + /// Computes transaction weight + // TODO(Velnbur): current implementation requires copying large + // script into transaction for simpler weight calculations. But in + // future we should calculate it by ourself. + pub fn compute_weigth(&self) -> Weight { + let unsigned_tx = self.unsigned_tx_with_witness(); + + unsigned_tx.weight() + Weight::from_witness_data_size(SCHNORR_SIGNATURE_SIZE as u64) + } + + pub fn script(&self) -> &DisproveScript { + &self.script + } +} + +pub struct SignedDisprove { + inner: Disprove, + + covenants_sig: Signature, +} + +impl SignedDisprove { + pub fn new(inner: Disprove, covenants_sig: impl Into) -> Self { + Self { + inner, + covenants_sig: Signature { + signature: covenants_sig.into(), + sighash_type: SIGHASHTYPE, + }, + } + } + + pub fn to_tx(&self, _ctx: &Secp256k1) -> Transaction { + let mut unsigned_tx = self.inner.to_unsigned_tx(); + + let witness = &mut unsigned_tx.input[0].witness; + + // Push winternitz signatures, stack and altstack elements. + for element in self.inner.script.to_witness_stack_elements() { + witness.push(element); + } + // Push convenants signature to stack. + witness.push(self.covenants_sig.serialize()); + + witness.push(self.inner.script.to_script_pubkey()); + witness.push(self.inner.control_block.serialize()); + + unsigned_tx + } +} + +/// Script letting challengers spend the **Assert** transaction +/// output if the operator computated substates incorrectly. +/// +/// This a typed version of [`Script`] can be easily converted into it. +/// +/// The script structure in general is simple: +/// ## Witness: +/// ```bitcoin_script +/// { Enc(z[i+1]) and Sig[i+1] } // Zipped +/// { Enc(z[i]) and Sig[i] } // Zipped +/// ``` +/// +/// ## Script: +/// ```bitcoin_script +/// { pk[i] } // { Zip(Enc(z[i+1]), Sig[i+1]), Zip(Enc(z[i]), Sig[i]), pk[i] } +/// { OP_WINTERNITZVERIFY } // { Zip(Enc(z[i+1]), Sig[i+1]), Enc(z[i]) } +/// { OP_RESTORE } // { Zip(Enc(z[i+1]), Sig[i+1]), z[i] } +/// { OP_TOALTSTACK } // { Zip(Enc(z[i+1]), Sig[i+1]) } +/// { pk[i+1] } // { Zip(Enc(z[i+1]), Sig[i+1]), pk[i+1] } +/// { OP_WINTERNITZVERIFY } // { Enc(z[i+1]) } +/// { OP_RESTORE } // { z[i+1] } +/// { OP_FROMALTSTACK } // { z[i+1] z[i] } +/// { fn[i] } // { z[i+1] fn[i](z[i]) } +/// { OP_EQUAL } // { z[i+1] == fn[i](z[i]) } +/// { OP_NOT } // { z[i+1] != fn[i](z[i]) } +/// ``` +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct DisproveScript { + pub from_state: SignedIntermediateState, + pub to_state: SignedIntermediateState, + pub function: Script, + pub covenants_aggpubkey: XOnlyPublicKey, +} + +impl DisproveScript { + /// Given the previous and current states, and the function that was executed, + /// creates a new DisproveScript according to the BitVM2 protocol. + pub fn new( + from: IntermediateState, + to: IntermediateState, + function: Script, + covenants_aggpubkey: impl Into, + ) -> Self { + // Sign the states with the regular entropy randomness + let from_signed = SignedIntermediateState::sign(from); + let to_signed = SignedIntermediateState::sign(to); + + Self::from_signed_states(from_signed, to_signed, function, covenants_aggpubkey.into()) + } + + /// Given the previous and current states, and the function that was executed, + /// creates a new DisproveScript according to the BitVM2 protocol. + /// + /// The randomness is derived from the `seed`. + pub fn new_with_seed( + from: IntermediateState, + to: IntermediateState, + function: Script, + seed: Seed, + covenants_aggpubkey: XOnlyPublicKey, + ) -> Self + where + Seed: Sized + Default + AsMut<[u8]> + Copy, + Rng: rand::SeedableRng + rand::Rng, + { + // Sign the states with the seed randomness + let from_signed = SignedIntermediateState::sign_with_seed::(from, seed); + let to_signed = SignedIntermediateState::sign_with_seed::(to, seed); + + Self::from_signed_states(from_signed, to_signed, function, covenants_aggpubkey) + } + + /// Construct new disprove script from already signed state. + pub fn from_signed_states( + from_signed: SignedIntermediateState, + to_signed: SignedIntermediateState, + function: Script, + covenants_aggpubkey: XOnlyPublicKey, + ) -> Self { + Self { + from_state: from_signed, + to_state: to_signed, + function, + covenants_aggpubkey, + } + } + + /// Given the previous and current states signed, and the function that was executed, + /// creates a new DisproveScript according to the BitVM2 protocol. + pub fn to_script_pubkey(&self) -> Script { + script! { + { OP_CHECKCOVENANTVERIFY(self.covenants_aggpubkey) } + + // Step 1. Public key + verification of "to" state + { self.to_state.verification_script_toaltstack() } // This leaves z[i+1] in the altstack + { self.from_state.verification_script() } // This leaves z[i].mainstack in the mainstack, while (z[i+1], z[i].altstack) is still in the altstack + + // Step 2. Applying function and popping "to" state + { self.function.clone() } // This leaves f[i](z[i]).mainstack in the mainstack and { z[i+1].altstack, f[i](z[i]).altstack } in the altstack + { OP_LONGFROMALTSTACK(self.to_state.altstack.len()) } + { self.to_state.verification_script_fromaltstack() } // This leaves z[i+1].mainstack and f[i](z[i]).mainstack in the mainstack, while f[i](z[i]).altstack and z[i+1].alstack is in the altstack + + // Step 3. + // At this point, our stack consists of: + // { f[i](z[i]).mainstack, f[i](z[i]).altstack, z[i+1].mainstack } + // while the altstack has z[i+1].altstack. + // Thus, we have to pick f[i](z[i]).mainstack to the top of the stack + for _ in (0..self.to_state.stack.len()).rev() { + { self.to_state.total_len() + self.to_state.stack.len() - 1 } OP_ROLL + } + + // At this point, we should have + // { f[i](z[i]).altstack, z[i+1].mainstack, f[i](z[i]).mainstack } + + // Step 4. Checking if z[i+1] == f(z[i]) + // a) Mainstack verification + { OP_LONGNOTEQUAL(self.to_state.stack.len()) } + + // b) Altstack verification + { OP_LONGFROMALTSTACK(self.to_state.altstack.len()) } + + // Since currently our stack looks like: + // { f[i](z[i]).altstack, {bit}, z[i+1].altstack, }, + // we need to push f[i](z[i]).altstack to the top of the stack + for _ in 0..self.to_state.altstack.len() { + { 2*self.to_state.altstack.len() } OP_ROLL + } + + { OP_LONGNOTEQUAL(self.to_state.altstack.len()) } + OP_BOOLOR + } + } + + /// Construct elements for witness stack which fulfill the spending + /// condition in assert transaction taptree. + pub fn to_witness_stack_elements(&self) -> Vec> { + let mut stack = Vec::new(); + stack.extend(self.from_state.witness_elements()); + stack.extend(self.to_state.witness_elements()); + stack + } + + /// Construct script sig for fullfiling the disprove script conditions. + pub fn to_script_sig(&self, comittee_signature: Signature) -> Script { + script! { + { self.from_state.to_script_sig() } + { self.to_state.to_script_sig() } + { comittee_signature.serialize().to_vec() } + } + } +} + +/// Given the `input` script, [`SplitResult`] and `constructor`, does the following: +/// - For each shard, creates a DisproveScript using `constructor` +/// - Returns the list of [`DisproveScript`]s. +fn disprove_scripts_with_constructor( + input: Script, + split_result: SplitResult, + covenants_aggpubkey: XOnlyPublicKey, + constructor: F, +) -> Vec +where + F: Fn(IntermediateState, IntermediateState, Script, XOnlyPublicKey) -> DisproveScript + Clone, +{ + assert_eq!( + split_result.shards.len(), + split_result.intermediate_states.len(), + "Shards and intermediate states must have the same length" + ); + + iter::once(IntermediateState::from_inject_script(input)) + .chain(split_result.intermediate_states) + .tuple_windows() + .zip(split_result.shards) + .map(|((from, to), function)| constructor(from, to, function, covenants_aggpubkey)) + .collect() +} + +/// Given the script and its input, does the following: +/// - Splits the script into shards +/// - For each shard, creates a [`DisproveScript`] +/// - Returns the list of [`DisproveScript`]s +pub fn form_disprove_scripts( + input: Script, + covenants_aggpubkey: XOnlyPublicKey, +) -> Vec { + let split_result = S::default_split(input.clone(), SplitType::default()); + disprove_scripts_with_constructor( + input, + split_result, + covenants_aggpubkey, + DisproveScript::new, + ) +} + +/// Given the script and its input, does the following: +/// - Splits the script into shards +/// - Distorts the random intermediate state, making +/// two state transitions incorrect +/// - For each shard, creates a [`DisproveScript`] +/// - Returns the list of [`DisproveScript`]s and the index of distorted shard +pub fn form_disprove_scripts_distorted( + input: Script, + covenants_aggpubkey: XOnlyPublicKey, +) -> (Vec, usize) { + // Splitting the script into shards + let split_result = S::default_split(input.clone(), SplitType::default()); + + // Distorting the output of the random shard + let (distorted_split_result, distorted_shard_id) = split_result.distort(); + + // Creating the disprove scripts + let disprove_scripts = disprove_scripts_with_constructor( + input, + distorted_split_result, + covenants_aggpubkey, + DisproveScript::new, + ); + + // Returning the result + (disprove_scripts, distorted_shard_id) +} + +/// Given the script and its input, does the following: +/// - Splits the script into shards +/// - For each shard, creates a [`DisproveScript`] +/// - Returns the list of [`DisproveScript`]s +/// +/// The randomness is derived from the `seed`. +pub fn form_disprove_scripts_with_seed( + input: Script, + covenants_aggpubkey: XOnlyPublicKey, + seed: Seed, +) -> Vec +where + S: SplitableScript, + Seed: Sized + Default + AsMut<[u8]> + Copy, + Rng: rand::SeedableRng + rand::Rng, +{ + let split_result = S::default_split(input.clone(), SplitType::default()); + disprove_scripts_with_constructor( + input, + split_result, + covenants_aggpubkey, + |from, to, shard, covenants_aggpubkey: XOnlyPublicKey| { + DisproveScript::new_with_seed::(from, to, shard, seed, covenants_aggpubkey) + }, + ) +} + +/// Given the script and its input, does the following: +/// - Splits the script into shards +/// - Distorts the random intermediate state, making +/// two state transitions incorrect +/// - For each shard, creates a [`DisproveScript`] +/// - Returns the list of [`DisproveScript`]s and the index of distorted shard +/// +/// The randomness is derived from the `seed`. +pub fn form_disprove_scripts_distorted_with_seed( + input: Script, + covenants_aggpubkey: XOnlyPublicKey, + seed: Seed, +) -> (Vec, usize) +where + S: SplitableScript, + Seed: Sized + Default + AsMut<[u8]> + Copy, + Rng: rand::SeedableRng + rand::Rng, +{ + // Splitting the script into shards + let split_result = S::default_split(input.clone(), SplitType::default()); + + // Distorting the output of the random shard + let (distorted_split_result, distorted_shard_id) = split_result.distort(); + + // Creating the disprove scripts + let disprove_scripts = disprove_scripts_with_constructor( + input, + distorted_split_result, + covenants_aggpubkey, + |from, to, shard, covenants_aggpubkey| { + DisproveScript::new_with_seed::(from, to, shard, seed, covenants_aggpubkey) + }, + ); + + // Returning the result + (disprove_scripts, distorted_shard_id) +} + +pub fn extract_signed_states( + disproves: &[DisproveScript], +) -> impl DoubleEndedIterator { + iter::once(&disproves[0].from_state).chain(disproves.iter().map(|d| &d.to_state)) +} diff --git a/core/src/disprove/signing.rs b/nero-core/src/disprove/signing.rs similarity index 86% rename from core/src/disprove/signing.rs rename to nero-core/src/disprove/signing.rs index f47d6f6..a7e77e2 100644 --- a/core/src/disprove/signing.rs +++ b/nero-core/src/disprove/signing.rs @@ -10,7 +10,7 @@ const MAX_STACK_ELEMENT_VALUE: u32 = (1 << 31) - 1; /// Struct handling information about a single u32 element in the state array. /// Namely, besides the element itself, it also contains the public key, secret key, /// and the signature of the element. -#[derive(Clone, Copy, Debug)] +#[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct SignedStackElement { pub stack_element: u32, pub encoding: Message, @@ -66,7 +66,7 @@ impl SignedStackElement { /// u32 values (both in mainstack and altstack), but this struct /// also contains the public keys, secret keys, and signatures /// of the elements in the state array. -#[derive(Clone, Debug)] +#[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct SignedIntermediateState { pub stack: Vec, pub altstack: Vec, @@ -74,12 +74,12 @@ pub struct SignedIntermediateState { impl SignedIntermediateState { /// Creates a new [`SignedIntermediateState`] from the given intermediate state - pub fn sign(state: &IntermediateState) -> Self { + pub fn sign(state: IntermediateState) -> Self { Self::sign_fn(state, SignedStackElement::sign) } /// Creates a new [`SignedIntermediateState`] from the given intermediate state - pub fn sign_with_seed(state: &IntermediateState, seed: Seed) -> Self + pub fn sign_with_seed(state: IntermediateState, seed: Seed) -> Self where Seed: Sized + Default + AsMut<[u8]> + Copy, Rng: rand::SeedableRng + rand::Rng, @@ -93,7 +93,7 @@ impl SignedIntermediateState { /// /// The function takes the mainstack and altstack, converts them to the array of /// `u32` elements and applies the signing function to each element. - fn sign_fn(state: &IntermediateState, sign_fn: F) -> Self + fn sign_fn(state: IntermediateState, sign_fn: F) -> Self where F: Fn(u32) -> SignedStackElement + Clone, { @@ -119,15 +119,30 @@ impl SignedIntermediateState { /// Script that pushes zipped signature and message to the stack for /// each signed element in the stack and altstack. - pub fn witness_script(&self) -> Script { + pub fn witness_elements(&self) -> Vec> { + let mut elements = Vec::new(); + // Pushing the stack + for element in self.stack.clone() { + elements.extend(element.signature.to_witness_stack_elements()); + } + + // Pushing the altstack + for element in self.altstack.clone().into_iter().rev() { + elements.extend(element.signature.to_witness_stack_elements()); + } + + elements + } + + pub fn to_script_sig(&self) -> Script { script! { // Pushing the stack - for element in self.stack.clone() { + for element in &self.stack { { element.signature.to_script_sig() } } // Pushing the altstack - for element in self.altstack.clone().into_iter().rev() { + for element in self.altstack.iter().rev() { { element.signature.to_script_sig() } } } @@ -140,14 +155,14 @@ impl SignedIntermediateState { script! { // For each element, we need to push the public key and run the // Winternitz verification script - for element in self.altstack.clone() { + for element in &self.altstack { { checksig_verify_script(&element.public_key) } { Message::recovery_script() } OP_TOALTSTACK } // Do the same for the mainstack - for element in self.stack.clone().into_iter().rev() { + for element in self.stack.iter().rev() { { checksig_verify_script(&element.public_key) } { Message::recovery_script() } OP_TOALTSTACK diff --git a/core/src/disprove/tests.rs b/nero-core/src/disprove/tests.rs similarity index 67% rename from core/src/disprove/tests.rs rename to nero-core/src/disprove/tests.rs index 59a29fb..7ee79e2 100644 --- a/core/src/disprove/tests.rs +++ b/nero-core/src/disprove/tests.rs @@ -1,11 +1,6 @@ -use std::{fs, path::Path, str::FromStr as _}; - use crate::disprove::{form_disprove_scripts_distorted, DisproveScript}; -use bitcoin::{ - consensus::Encodable as _, hashes::Hash as _, key::Secp256k1, secp256k1::SecretKey, Amount, - OutPoint, TxOut, WPubkeyHash, -}; +use bitcoin::{key::Secp256k1, taproot::LeafVersion, TapLeafHash}; use bitcoin_splitter::split::{ core::SplitType, intermediate_state::IntermediateState, @@ -16,15 +11,12 @@ use bitcoin_testscripts::{ int_mul_windowed::U254MulScript, square_fibonacci::SquareFibonacciScript, }; -use bitcoin_utils::stack_to_script; -use bitcoin_utils::{comparison::OP_LONGEQUALVERIFY, treepp::*}; +use bitcoin_utils::{comittee_signature, comparison::OP_LONGEQUALVERIFY, treepp::*}; +use bitcoin_utils::{debug::execute_script_with_leaf, stack_to_script}; use bitcoin_window_mul::{bigint::U508, traits::comparable::Comparable}; -use once_cell::sync::Lazy; +use rand::thread_rng; -use crate::{ - assert::AssertTransaction, disprove::form_disprove_scripts, - disprove::signing::SignedIntermediateState, -}; +use crate::{disprove::form_disprove_scripts, disprove::signing::SignedIntermediateState}; #[test] pub fn test_stack_sign_and_verify() { @@ -38,11 +30,11 @@ pub fn test_stack_sign_and_verify() { ); // Now, we sign the state - let signed_state = SignedIntermediateState::sign(&state); + let signed_state = SignedIntermediateState::sign(state); // Check that witness + verification scripts are correct let verify_script = script! { - { signed_state.witness_script() } + { signed_state.to_script_sig() } { signed_state.verification_script() } OP_4 OP_EQUALVERIFY OP_3 OP_EQUALVERIFY @@ -67,11 +59,11 @@ pub fn test_zero_stack_sign_and_verify() { ); // Now, we sign the state - let signed_state = SignedIntermediateState::sign(&state); + let signed_state = SignedIntermediateState::sign(state); // Check that witness + verification scripts are correct let verify_script = script! { - { signed_state.witness_script() } + { signed_state.to_script_sig() } { signed_state.verification_script() } for _ in 0..30 { OP_0 OP_EQUALVERIFY @@ -97,11 +89,11 @@ pub fn test_stack_sign_and_verify_with_altstack() { ); // Now, we sign the state - let signed_state = SignedIntermediateState::sign(&state); + let signed_state = SignedIntermediateState::sign(state); // Check that witness + verification scripts are correct let verify_script = script! { - { signed_state.witness_script() } + { signed_state.to_script_sig() } { signed_state.verification_script() } { 1636 } OP_EQUALVERIFY OP_3 OP_EQUALVERIFY @@ -126,11 +118,11 @@ pub fn test_stack_sign_and_verify_bigint() { for (i, intermediate_state) in split_result.intermediate_states.into_iter().enumerate() { // Now, we sign the state - let signed_state = SignedIntermediateState::sign(&intermediate_state.clone()); + let signed_state = SignedIntermediateState::sign(intermediate_state.clone()); // Check that witness + verification scripts are correct let verify_script = script! { - { signed_state.witness_script() } + { signed_state.to_script_sig() } { signed_state.verification_script() } { stack_to_script(&intermediate_state.stack) } { OP_LONGEQUALVERIFY(signed_state.stack.len()) } @@ -164,17 +156,24 @@ pub fn test_trivial_disprove_script_success() { OP_ADD }; + let secp_ctx = Secp256k1::new(); + let (seckey, pubkey) = secp_ctx.generate_keypair(&mut thread_rng()); + // Now, form the disprove script - let disprove_script = DisproveScript::new(&state_from, &state_to, &function); + let disprove_script = + DisproveScript::new(state_from, state_to, function, pubkey.x_only_public_key().0); + let leaf_hash = + TapLeafHash::from_script(&disprove_script.to_script_pubkey(), LeafVersion::TapScript); + let signature = comittee_signature(&disprove_script.to_script_pubkey(), &secp_ctx, seckey); // Check that witness + verification scripts are satisfied let verify_script = script! { - { disprove_script.script_witness } - { disprove_script.script_pubkey } + { disprove_script.to_script_sig(signature) } + { disprove_script.to_script_pubkey() } }; - let result = execute_script(verify_script); - assert!(result.success, "Verification failed"); + let result = execute_script_with_leaf(verify_script, leaf_hash); + assert!(result.success, "Verification failed\n: {result}"); } #[test] @@ -199,13 +198,17 @@ pub fn test_trivial_disprove_script_should_fail() { OP_ADD }; + let secp_ctx = Secp256k1::new(); + let (seckey, pubkey) = secp_ctx.generate_keypair(&mut thread_rng()); + // Now, form the disprove script - let disprove_script = DisproveScript::new(&state_from, &state_to, &function); + let disprove_script = DisproveScript::new(state_from, state_to, function, pubkey); + let signature = comittee_signature(&disprove_script.to_script_pubkey(), &secp_ctx, seckey); // Check that witness + verification scripts are satisfied let verify_script = script! { - { disprove_script.script_witness } - { disprove_script.script_pubkey } + { disprove_script.to_script_sig(signature) } + { disprove_script.to_script_pubkey() } }; let result = execute_script(verify_script); @@ -235,13 +238,17 @@ pub fn test_disprove_script_with_altstack_should_fail() { OP_ADD OP_TOALTSTACK OP_TOALTSTACK }; + let secp_ctx = Secp256k1::new(); + let (seckey, pubkey) = secp_ctx.generate_keypair(&mut thread_rng()); + // Now, form the disprove script - let disprove_script = DisproveScript::new(&state_from, &state_to, &function); + let disprove_script = DisproveScript::new(state_from, state_to, function, pubkey); + let signature = comittee_signature(&disprove_script.to_script_pubkey(), &secp_ctx, seckey); // Check that witness + verification scripts are satisfied let verify_script = script! { - { disprove_script.script_witness } - { disprove_script.script_pubkey } + { disprove_script.to_script_sig(signature) } + { disprove_script.to_script_pubkey() } }; let result = execute_script(verify_script); @@ -271,18 +278,24 @@ pub fn test_disprove_script_with_altstack_success() { OP_ADD OP_TOALTSTACK OP_TOALTSTACK }; + let secp_ctx = Secp256k1::new(); + let (seckey, pubkey) = secp_ctx.generate_keypair(&mut thread_rng()); + // Now, form the disprove script - let disprove_script = DisproveScript::new(&state_from, &state_to, &function); + let disprove_script = DisproveScript::new(state_from, state_to, function, pubkey); + let leaf_hash = + TapLeafHash::from_script(&disprove_script.to_script_pubkey(), LeafVersion::TapScript); + let signature = comittee_signature(&disprove_script.to_script_pubkey(), &secp_ctx, seckey); // Check that witness + verification scripts are satisfied let verify_script = script! { - { disprove_script.script_witness } - { disprove_script.script_pubkey } + { disprove_script.to_script_sig(signature) } + { disprove_script.to_script_pubkey() } }; - let result = execute_script(verify_script); + let result = execute_script_with_leaf(verify_script, leaf_hash); - assert!(result.success, "Verification failed"); + assert!(result.success, "Verification failed\n: {result}"); } #[test] @@ -307,13 +320,17 @@ pub fn test_disprove_script_with_altstack_2() { OP_FROMALTSTACK OP_ADD OP_TOALTSTACK OP_TOALTSTACK }; + let secp_ctx = Secp256k1::new(); + let (seckey, pubkey) = secp_ctx.generate_keypair(&mut thread_rng()); + // Now, form the disprove script - let disprove_script = DisproveScript::new(&state_from, &state_to, &function); + let disprove_script = DisproveScript::new(state_from, state_to, function, pubkey); + let signature = comittee_signature(&disprove_script.to_script_pubkey(), &secp_ctx, seckey); // Check that witness + verification scripts are satisfied let verify_script = script! { - { disprove_script.script_witness } - { disprove_script.script_pubkey } + { disprove_script.to_script_sig(signature) } + { disprove_script.to_script_pubkey() } }; let result = execute_script(verify_script); @@ -342,18 +359,25 @@ pub fn test_disprove_script_mul_script() { let result = execute_script(verification_script); assert!(!result.success, "verification has failed"); + let secp_ctx = Secp256k1::new(); + let (seckey, pubkey) = secp_ctx.generate_keypair(&mut thread_rng()); + // Now, we form the disprove script for each shard for i in 0..(split_result.shards.len() - 1) { let disprove_script = DisproveScript::new( - &split_result.intermediate_states[i], - &split_result.intermediate_states[i + 1], - &split_result.shards[i + 1], + split_result.intermediate_states[i].clone(), + split_result.intermediate_states[i + 1].clone(), + split_result.shards[i + 1].clone(), + pubkey, ); + // Now, form the disprove script + let signature = comittee_signature(&disprove_script.to_script_pubkey(), &secp_ctx, seckey); + // Check that witness + verification scripts are satisfied let verify_script = script! { - { disprove_script.script_witness } - { disprove_script.script_pubkey } + { disprove_script.to_script_sig(signature) } + { disprove_script.to_script_pubkey() } }; let result = execute_script(verify_script); @@ -386,21 +410,30 @@ pub fn test_disprove_script_fibonacci_script_invalid_input() { let result = execute_script(verification_script); assert!(!result.success, "verification has failed"); + let secp_ctx = Secp256k1::new(); + let (seckey, pubkey) = secp_ctx.generate_keypair(&mut thread_rng()); + // Now, we form the disprove script for each shard for i in 0..(split_result.shards.len() - 1) { let disprove_script = DisproveScript::new( - &split_result.intermediate_states[i], - &split_result.intermediate_states[i + 1], - &split_result.shards[i + 1], + split_result.intermediate_states[i].clone(), + split_result.intermediate_states[i + 1].clone(), + split_result.shards[i + 1].clone(), + pubkey, ); + // Now, form the disprove script + let signature = comittee_signature(&disprove_script.to_script_pubkey(), &secp_ctx, seckey); + let leaf_hash = + TapLeafHash::from_script(&disprove_script.to_script_pubkey(), LeafVersion::TapScript); + // Check that witness + verification scripts are satisfied let verify_script = script! { - { disprove_script.script_witness } - { disprove_script.script_pubkey } + { disprove_script.to_script_sig(signature) } + { disprove_script.to_script_pubkey() } }; - let result = execute_script(verify_script); + let result = execute_script_with_leaf(verify_script, leaf_hash); assert!(!result.success, "Verification {:?} failed", i + 1); } } @@ -417,18 +450,25 @@ pub fn test_disprove_script_fibonacci_script_valid_input() { let split_result = SquareFibonacciScript::::default_split(input, SplitType::ByInstructions); + let secp_ctx = Secp256k1::new(); + let (seckey, pubkey) = secp_ctx.generate_keypair(&mut thread_rng()); + // Now, we form the disprove script for each shard for i in 0..(split_result.shards.len() - 1) { let disprove_script = DisproveScript::new( - &split_result.intermediate_states[i], - &split_result.intermediate_states[i + 1], - &split_result.shards[i + 1], + split_result.intermediate_states[i].clone(), + split_result.intermediate_states[i + 1].clone(), + split_result.shards[i + 1].clone(), + pubkey, ); + // Now, form the disprove script + let signature = comittee_signature(&disprove_script.to_script_pubkey(), &secp_ctx, seckey); + // Check that witness + verification scripts are satisfied let verify_script = script! { - { disprove_script.script_witness } - { disprove_script.script_pubkey } + { disprove_script.to_script_sig(signature) } + { disprove_script.to_script_pubkey() } }; let result = execute_script(verify_script); @@ -445,21 +485,29 @@ pub fn test_distorted_disprove_script_fibonacci_sequence() { // First, we generate the pair of input and output scripts let IOPair { input, output: _ } = FibonacciScript::generate_valid_io_pair(); + let secp_ctx = Secp256k1::new(); + let (seckey, pubkey) = secp_ctx.generate_keypair(&mut thread_rng()); + // Splitting the script into shards let (disprove_scripts, distorted_id) = - form_disprove_scripts_distorted::(input.clone()); + form_disprove_scripts_distorted::(input.clone(), pubkey.into()); println!("Distorted ID: {:?}", distorted_id); // Now, we form the disprove script for each shard for (i, disprove_script) in disprove_scripts.into_iter().enumerate() { + // Now, form the disprove script + let signature = comittee_signature(&disprove_script.to_script_pubkey(), &secp_ctx, seckey); + let leaf_hash = + TapLeafHash::from_script(&disprove_script.to_script_pubkey(), LeafVersion::TapScript); + // Check that witness + verification scripts are satisfied only for the distorted shard let verify_script = script! { - { disprove_script.clone().script_witness } - { disprove_script.clone().script_pubkey } + { disprove_script.to_script_sig(signature) } + { disprove_script.to_script_pubkey() } }; - let result = execute_script(verify_script); + let result = execute_script_with_leaf(verify_script, leaf_hash); if i == distorted_id || i == distorted_id + 1 { assert!(result.success, "Verification {:?} failed", i + 1); @@ -474,99 +522,24 @@ pub fn test_disprove_script_batch_correctness() { // First, we generate the pair of input and output scripts let IOPair { input, output: _ } = U254MulScript::generate_valid_io_pair(); + let secp_ctx = Secp256k1::new(); + let (seckey, pubkey) = secp_ctx.generate_keypair(&mut thread_rng()); + // Splitting the script into shards - let disprove_scripts = form_disprove_scripts::(input.clone()); + let disprove_scripts = form_disprove_scripts::(input.clone(), pubkey.into()); // Now, we form the disprove script for each shard for (i, disprove_script) in disprove_scripts.into_iter().enumerate() { - // Check that witness + verification scripts are satisfied + // Now, form the disprove script + let signature = comittee_signature(&disprove_script.to_script_pubkey(), &secp_ctx, seckey); + + // Check that witness + verification scripts are satisfied only for the distorted shard let verify_script = script! { - { disprove_script.script_witness } - { disprove_script.script_pubkey } + { disprove_script.to_script_sig(signature) } + { disprove_script.to_script_pubkey() } }; let result = execute_script(verify_script); assert!(!result.success, "Verification {:?} failed", i + 1); } } - -static SECKEY: Lazy = Lazy::new(|| { - "50c8f972285ad27527d79c80fe4df1b63c1192047713438b45758ea4e110a88b" - .parse() - .unwrap() -}); - -#[test] -fn test_assert_tx_signing() { - let IOPair { input, .. } = U254MulScript::generate_invalid_io_pair(); - - let ctx = Secp256k1::new(); - - let operator_pubkey = SECKEY.public_key(&ctx); - let operator_xonly = operator_pubkey.x_only_public_key().0; - - let assert_tx = - AssertTransaction::::new(input, operator_xonly, Amount::from_sat(70_000)); - - let operator_script_pubkey = - Script::new_p2wpkh(&WPubkeyHash::hash(&operator_pubkey.serialize())); - - let utxo = TxOut { - value: Amount::from_sat(73_000), - script_pubkey: operator_script_pubkey.clone(), - }; - - let outpoint = - OutPoint::from_str("a85d89b4666fed622281d3589474aa1f87971b54bd5d9c1899ed2e8e0447cc06:0") - .unwrap(); - - let tx = assert_tx - .clone() - .spend_p2wpkh_input_tx(&ctx, &SECKEY, utxo, outpoint) - .unwrap(); - - let txid = tx.compute_txid(); - println!("Assert:"); - dump_hex_tx_to_file(tx, "assert.hex"); - - let payout = assert_tx - .clone() - .payout_transaction( - &ctx, - TxOut { - value: Amount::from_sat(69_000), - script_pubkey: operator_script_pubkey.clone(), - }, - OutPoint::new(txid, 0), - &SECKEY, - ) - .unwrap(); - println!("Payout:"); - dump_hex_tx_to_file(payout, "payout.hex"); - - let disprove_txs = assert_tx - .clone() - .disprove_transactions( - &ctx, - TxOut { - value: Amount::from_sat(69_000), - script_pubkey: operator_script_pubkey, - }, - OutPoint::new(txid, 0), - ) - .unwrap(); - - println!("Number of disprove scripts: {}", disprove_txs.len()); - for (idx, (_script, tx)) in disprove_txs.into_iter().enumerate() { - println!("Disprove{idx}:"); - dump_hex_tx_to_file(tx, format!("disprove_{}.hex", idx)); - } -} - -fn dump_hex_tx_to_file(tx: bitcoin::Transaction, path: impl AsRef) { - let mut buf = Vec::new(); - tx.consensus_encode(&mut buf).unwrap(); - println!("Length: {}", buf.len()); - let encoded = hex::encode(&buf); - fs::write(path, encoded).unwrap(); -} diff --git a/nero-core/src/lib.rs b/nero-core/src/lib.rs new file mode 100644 index 0000000..6046a4e --- /dev/null +++ b/nero-core/src/lib.rs @@ -0,0 +1,68 @@ +use std::iter; + +use bitcoin::{ + key::{Secp256k1, Verification}, + TapSighash, XOnlyPublicKey, +}; +use musig2::{ + secp256k1::{PublicKey, SecretKey, Signing}, + AggNonce, KeyAggContext, PartialSignature, SecNonce, +}; +use once_cell::sync::Lazy; + +pub use musig2; + +pub mod assert; +pub mod challenge; +pub mod claim; +pub mod context; +pub mod disprove; +pub mod operator; +pub mod payout; +pub mod scripts; + +#[allow(dead_code)] +// Re-export what is needed to write treepp scripts +pub mod treepp { + pub use bitcoin_script::{define_pushable, script}; + pub use bitcoin_utils::debug::{execute_script, run}; + + define_pushable!(); + pub use bitcoin::ScriptBuf as Script; +} + +/// Unspendable key used in inner key of taproot addresses through protocol. +/// +/// The definition you can find in BIP341. +pub(crate) static UNSPENDABLE_KEY: Lazy = Lazy::new(|| { + "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0" + .parse() + .unwrap() +}); + +pub(crate) fn schnorr_sign_partial( + ctx: &Secp256k1, + sighash: TapSighash, + comittee_pubkeys: Vec, + agg_nonce: &AggNonce, + secret_key: SecretKey, + secnonce: SecNonce, +) -> PartialSignature { + let mut pubkeys = iter::once(secret_key.public_key(ctx)) + .chain(comittee_pubkeys) + .collect::>(); + pubkeys.sort(); + let keyagg_ctx = KeyAggContext::new(pubkeys).unwrap(); + + musig2::sign_partial(&keyagg_ctx, secret_key, secnonce, agg_nonce, sighash).unwrap() +} + +#[cfg(test)] +mod tests { + use crate::UNSPENDABLE_KEY; + + #[test] + fn test_unspendable_key() { + let _ = *UNSPENDABLE_KEY; + } +} diff --git a/nero-core/src/operator/mod.rs b/nero-core/src/operator/mod.rs new file mode 100644 index 0000000..ea697d7 --- /dev/null +++ b/nero-core/src/operator/mod.rs @@ -0,0 +1,593 @@ +use bitcoin::{ + key::Secp256k1, relative::Height, secp256k1::PublicKey, taproot::LeafVersion, Amount, FeeRate, + Network, Transaction, TxIn, TxOut, +}; +use bitcoin_splitter::split::script::SplitableScript; +use musig2::{ + aggregate_partial_signatures, + secp::Point, + secp256k1::{All, SecretKey}, + AggNonce, CompactSignature, PartialSignature, PubNonce, SecNonce, SecNonceBuilder, +}; + +use crate::{ + assert::{Assert, SignedAssert}, + challenge::{Challenge, SignedChallenge}, + claim::{Claim, FundedClaim}, + context::Context, + disprove::{Disprove, SignedDisprove}, + payout::{Payout, PayoutOptimistic, SignedPayout, SignedPayoutOptimistic}, + treepp::*, +}; + +pub struct OperatorConfig { + /// Network which this session is working. + pub network: Network, + /// Stacked amount mentioned in paper as $d$. + pub staked_amount: Amount, + /// $x$ - the input of the program flow asserts. + pub input: Script, + /// Claim transaction challenge period. + pub claim_challenge_period: Height, + /// Assert transaction challenge period. + pub assert_challenge_period: Height, + /// Public keys of comitte for emulating covenants. + pub comittee: Vec, + /// Operator's wallet address + pub operator_script_pubkey: Script, + /// Seed for random generator. + pub seed: Seed, +} + +/// Operator controls signing and creation of transaction for particular +/// BitVM2 session. +pub struct Operator { + /// New secret key generated for this session. + secret_key: SecretKey, + + /// Context holds all created before the session data. + context: Context, +} + +impl Operator +where + S: SplitableScript, +{ + pub fn new(config: OperatorConfig) -> Self + where + Seed: Sized + Default + AsMut<[u8]> + Copy, + Rng: rand::SeedableRng + rand::Rng, + { + // FIXME(Velnbur): This RNG is generated from seed twice later. Should be + // fixed later. + let mut rng = Rng::from_seed(config.seed); + let secp_ctx = Secp256k1::new(); + + let (seckey, pubkey) = secp_ctx.generate_keypair(&mut rng); + + let ctx = Context::compute_setup::( + secp_ctx, + config.staked_amount, + config.input, + config.assert_challenge_period, + config.claim_challenge_period, + pubkey, + config.operator_script_pubkey, + config.comittee, + config.seed, + ); + + Self { + secret_key: seckey, + context: ctx, + } + } + + /// Create new operator with one random spendable disprove script. + pub fn new_distorted(config: OperatorConfig) -> Self + where + Seed: Sized + Default + AsMut<[u8]> + Copy, + Rng: rand::SeedableRng + rand::Rng, + { + // FIXME(Velnbur): This RNG is generated from seed twice later. Should be + // fixed later. + let mut rng = Rng::from_seed(config.seed); + let secp_ctx = Secp256k1::new(); + + let (seckey, pubkey) = secp_ctx.generate_keypair(&mut rng); + + let ctx = Context::compute_setup_distorted::( + secp_ctx, + config.staked_amount, + config.input, + config.assert_challenge_period, + config.claim_challenge_period, + pubkey, + config.operator_script_pubkey, + config.comittee, + config.seed, + ); + + Self { + secret_key: seckey, + context: ctx, + } + } + + pub fn context(&self) -> &Context { + &self.context + } + + pub fn secret_key(&self) -> SecretKey { + self.secret_key + } +} + +/* STAGE 1: */ + +/// Initial state before the claim transaction is funded. +/// +/// From claim transaction id, will be built the next transcations, so +/// it's required to fund it from outside with some wallet before the +/// creation of other transactions as with additional input the claim +/// transaction id changes. +pub struct UnfundedOperator { + inner: Operator, + claim: Claim, +} + +impl UnfundedOperator { + pub fn from_operator(operator: Operator, fee_rate: FeeRate) -> Self { + let claim = Claim::from_context(&operator.context, fee_rate); + + Self { + inner: operator, + claim, + } + } + + /// Returns unsigned claim transcation without any inputs for funding + /// through external wallet. + pub fn unsigned_claim_tx(&self) -> Transaction { + self.claim.to_unsigned_tx(&self.inner.context.secp) + } +} + +/* STAGE 3: */ + +/// After challenge transaction is signed by wallet, operator and comitte +/// can exchange nonces for partial schnorr signatures. +pub struct NoncesAggregationOperator { + inner: Operator, + // Funded, signed by wallet claim transcation. + claim: FundedClaim, + challenge_tx: SignedChallenge, + /// Secnonce generated from musig2 signature. + operator_secnonce: SecNonce, +} + +impl NoncesAggregationOperator { + pub fn from_unfunded_operator( + operator: UnfundedOperator, + funding_input: Vec, + change_output: Option, + fee_rate: FeeRate, + rng: &mut Rng, + ) -> Self + where + Rng: rand::RngCore + rand::CryptoRng, + { + let context = &operator.inner.context; + let agg_pubkey = context.comittee_aggpubkey::(); + + let funded_claim = FundedClaim::new( + operator.claim, + funding_input, + change_output, + context.input.clone(), + ); + let claim_txid = funded_claim.compute_txid(&context.secp); + + let assert_tx_weight = context.assert_tx_weight; + + let challenge_tx = Challenge::new( + context.operator_script_pubkey.clone(), + claim_txid, + fee_rate + .checked_mul_by_weight(context.largest_disprove_weight + assert_tx_weight) + .unwrap(), + ); + + let secnonce = SecNonceBuilder::new(rng) + .with_seckey(operator.inner.secret_key) + .with_aggregated_pubkey(agg_pubkey) + .build(); + + Self { + challenge_tx: challenge_tx.sign( + &context.secp, + &funded_claim.challenge_output(&context.secp), + operator.inner.secret_key, + ), + inner: operator.inner, + claim: funded_claim, + operator_secnonce: secnonce, + } + } + + /// Public nonce of the operator. + pub fn public_nonce(&self) -> PubNonce { + self.operator_secnonce.public_nonce() + } +} + +/* STAGE 4: */ + +/// After nonces are exchanged, operator can create partial signatures for +/// transaction and wait for other signatures from comittee. +pub struct SignaturesAggOperator { + inner: Operator, + // Funded, signed by wallet claim transcation. + claim_tx: FundedClaim, + // Challenge is signed by oparator + challenge_tx: SignedChallenge, + /// Aggregated nonce for schnorr signing created from operator's secret + /// nonce and comittee public nonces. + aggnonce: AggNonce, + + /* partial signatures created by operator */ + partial_assert_sig: PartialSignature, + partial_payout_optimistic_sig: PartialSignature, + partial_payout_sig: PartialSignature, + partial_disprove_sigs: Vec, + + /* Txs lower are waiting for signing from comittee. */ + assert_tx: Assert, + payout_optimistic_tx: PayoutOptimistic, + payout_tx: Payout, + disprove_txs: Vec, +} + +impl SignaturesAggOperator +where + S: SplitableScript, +{ + /// Construct next stage of operator by including funding input to claim + /// transaction. + pub fn from_nonces_agg_operator( + operator: NoncesAggregationOperator, + mut nonces: Vec, + fee_rate: FeeRate, + ) -> Self { + let context = &operator.inner.context; + let claim_txid = operator.claim.compute_txid(&context.secp); + nonces.push(operator.public_nonce()); + let aggnonce = AggNonce::sum(nonces); + + // Create and sign Optimistic Payout + let payout_optimistic_tx = PayoutOptimistic::from_context(context, claim_txid, fee_rate); + let partial_payout_optimistic_sig = payout_optimistic_tx.sign_partial_from_claim( + &context.secp, + &operator.claim, + context.comittee.clone(), + &aggnonce, + operator.inner.secret_key, + operator.operator_secnonce.clone(), + ); + + let assert_tx = Assert::from_context(context, claim_txid, fee_rate); + let partial_assert_sig = assert_tx.sign_partial_from_claim( + &context.secp, + &operator.claim, + context.comittee.clone(), + &aggnonce, + operator.inner.secret_key, + operator.operator_secnonce.clone(), + ); + let assert_txid = assert_tx.compute_txid(&context.secp); + + // Create and sign payout transaction. + let payout_tx = Payout::from_context(context, assert_txid, fee_rate); + let partial_payout_sig = payout_tx.sign_partial_from_assert( + &context.secp, + &assert_tx, + context.comittee.clone(), + &aggnonce, + operator.inner.secret_key, + operator.operator_secnonce.clone(), + ); + + // Create and sign disprove transaction. + let assert_output = assert_tx.output(&context.secp); + let taproot = assert_tx.taproot(&context.secp); + let (partial_disprove_sigs, disprove_txs): (Vec<_>, Vec<_>) = context + .disprove_scripts + .iter() + .map(|script| { + let script_pubkey = script.to_script_pubkey(); + let tx = Disprove::new( + script, + assert_txid, + taproot + // TODO(Velnbur): another place which clones a + // large chunk of memory just for getting a + // control block. We should create a PR in + // rust-bitcoin to avoid that. + .control_block(&(script_pubkey, LeafVersion::TapScript)) + .unwrap(), + ); + + ( + tx.sign_partial( + &context.secp, + &assert_output, + context.comittee.clone(), + &aggnonce, + operator.inner.secret_key, + operator.operator_secnonce.clone(), + ), + tx, + ) + }) + .unzip(); + + Self { + inner: operator.inner, + claim_tx: operator.claim, + challenge_tx: operator.challenge_tx, + assert_tx, + partial_assert_sig, + partial_payout_optimistic_sig, + partial_payout_sig, + partial_disprove_sigs, + payout_optimistic_tx, + payout_tx, + aggnonce, + disprove_txs, + } + } + + pub fn unsigned_assert_tx(&self) -> &Assert { + &self.assert_tx + } + + pub fn unsigned_payout_optimistic(&self) -> &PayoutOptimistic { + &self.payout_optimistic_tx + } + + pub fn unsigned_payout_optimistic_tx(&self) -> Transaction { + self.payout_optimistic_tx.to_unsigned_tx() + } + + pub fn unsigned_payout_tx(&self) -> &Payout { + &self.payout_tx + } + + pub fn unsigned_disprove_txs(&self) -> &[Disprove] { + &self.disprove_txs + } + + pub fn aggnonce(&self) -> &AggNonce { + &self.aggnonce + } + + pub fn context(&self) -> &Context { + self.inner.context() + } + + pub fn claim_tx(&self) -> &FundedClaim { + &self.claim_tx + } + + pub fn challenge_tx(&self) -> &SignedChallenge { + &self.challenge_tx + } +} + +/* STAGE 5: */ + +/// After receiving all partial signature from comittee operator can +/// construct final "signed" versions of transaction and publish the claim +/// one. +pub struct FinalOperator { + inner: Operator, + + claim_tx: FundedClaim, + + challenge_tx: SignedChallenge, + + assert_tx: SignedAssert, + + payout_optimistic_tx: SignedPayoutOptimistic, + + payout_tx: SignedPayout, + + disprove_txs: Vec, +} + +pub struct PartialSignatures { + /// Partial signatures for assert transaction. + pub partial_assert_sigs: Vec, + /// Partial signatures for payout optimitstic transaction. + pub partial_payout_optimistic_sigs: Vec, + /// Partial signatures for payout transaction. + pub partial_payout_sigs: Vec, + /// Partial signatures for disprove transactions. + pub partial_disprove_sigs: Vec>, +} + +impl PartialSignatures { + /// Merge operator signatures into partial signatures got from comittee. + pub fn merge_operator_sigs( + &mut self, + partial_assert_sig: PartialSignature, + partial_payout_optimistic_sig: PartialSignature, + partial_payout_sig: PartialSignature, + partial_disprove_sigs: Vec, + ) { + self.partial_assert_sigs.push(partial_assert_sig); + self.partial_payout_sigs.push(partial_payout_sig); + self.partial_payout_optimistic_sigs + .push(partial_payout_optimistic_sig); + + for (sigs, sig) in self + .partial_disprove_sigs + .iter_mut() + .zip(partial_disprove_sigs) + { + sigs.push(sig); + } + } +} + +impl FinalOperator { + /// Construct next stage of operator by including the partial signatures + /// for all transaciton. + pub fn from_signatures_agg_operator( + operator: SignaturesAggOperator, + mut signatures: PartialSignatures, + ) -> Self { + signatures.merge_operator_sigs( + operator.partial_assert_sig, + operator.partial_payout_optimistic_sig, + operator.partial_payout_sig, + operator.partial_disprove_sigs, + ); + // setup variables + let context = &operator.inner.context; + let keyaggctx = context.comittee_keyaggctx(); + + let claim_assert_script_control_block = + operator.claim_tx.assert_script_control_block(&context.secp); + + let claim_payout_script = operator.claim_tx.optimistic_payout_script(); + let claim_payout_control_block = operator + .claim_tx + .optimistic_payout_script_control_block(&context.secp); + + let assert_sighash = operator.assert_tx.sighash( + &context.secp, + &operator.claim_tx.assert_output(&context.secp), + operator.claim_tx.assert_script(), + ); + let assert_aggsig: CompactSignature = aggregate_partial_signatures( + &keyaggctx, + &operator.aggnonce, + signatures.partial_assert_sigs, + assert_sighash, + ) + .unwrap(); + let assert_txout = &operator.assert_tx.output(&context.secp); + let assert_tx = SignedAssert::new( + operator.assert_tx, + assert_aggsig, + operator.claim_tx.assert_script(), + claim_assert_script_control_block, + ); + + let payout_sighash = + operator + .payout_tx + .sighash(&context.secp, assert_txout, assert_tx.payout_script()); + let payout_aggsig: CompactSignature = aggregate_partial_signatures( + &keyaggctx, + &operator.aggnonce, + signatures.partial_payout_sigs, + payout_sighash, + ) + .unwrap(); + let payout_operator_sig = operator.payout_tx.sign_operator( + &context.secp, + assert_txout, + assert_tx.payout_script(), + &operator.inner.secret_key, + ); + let payout_tx = SignedPayout::new( + operator.payout_tx, + assert_tx.payout_script().clone(), + assert_tx.payout_script_control_block(&context.secp), + payout_aggsig, + payout_operator_sig, + ); + + let disprove_txs = operator + .disprove_txs + .into_iter() + .zip(signatures.partial_disprove_sigs) + .map(|(tx, sigs)| { + let sighash = tx.sighash(assert_txout); + + let covenants_sig: CompactSignature = + aggregate_partial_signatures(&keyaggctx, &operator.aggnonce, sigs, sighash) + .unwrap(); + + SignedDisprove::new(tx, covenants_sig) + }) + .collect::>(); + + let payout_optimistic_assert_output_sighash = operator.payout_optimistic_tx.assert_sighash( + &operator.claim_tx.assert_output(&context.secp), + &operator.claim_tx.challenge_output(&context.secp), + &claim_payout_script, + ); + let payout_optimistic_aggsig: CompactSignature = aggregate_partial_signatures( + &keyaggctx, + &operator.aggnonce, + signatures.partial_payout_optimistic_sigs, + payout_optimistic_assert_output_sighash, + ) + .unwrap(); + + let payout_optimistic_operator_sig = operator.payout_optimistic_tx.sign_challenge_input( + &operator.claim_tx.assert_output(&context.secp), + &operator.claim_tx.challenge_output(&context.secp), + &context.secp, + operator.inner.secret_key, + ); + let payout_optimistic_tx = SignedPayoutOptimistic::new( + operator.payout_optimistic_tx, + payout_optimistic_aggsig, + payout_optimistic_operator_sig, + &claim_payout_script, + claim_payout_control_block, + ); + + FinalOperator { + inner: operator.inner, + claim_tx: operator.claim_tx, + challenge_tx: operator.challenge_tx, + assert_tx, + payout_optimistic_tx, + payout_tx, + disprove_txs, + } + } + + pub fn disprove_txs(&self) -> &[SignedDisprove] { + &self.disprove_txs + } + + pub fn claim_tx(&self) -> &FundedClaim { + &self.claim_tx + } + + pub fn challenge_tx(&self) -> &SignedChallenge { + &self.challenge_tx + } + + pub fn assert_tx(&self) -> &SignedAssert { + &self.assert_tx + } + + pub fn payout_optimistic_tx(&self) -> &SignedPayoutOptimistic { + &self.payout_optimistic_tx + } + + pub fn payout_tx(&self) -> &SignedPayout { + &self.payout_tx + } + + pub fn inner(&self) -> &Operator { + &self.inner + } +} diff --git a/nero-core/src/payout/mod.rs b/nero-core/src/payout/mod.rs new file mode 100644 index 0000000..2d48fd5 --- /dev/null +++ b/nero-core/src/payout/mod.rs @@ -0,0 +1,508 @@ +use bitcoin::{ + absolute::LockTime, + key::{Keypair, Parity, Secp256k1, Verification}, + relative::Height, + sighash::{Prevouts, SighashCache}, + taproot::{ControlBlock, LeafVersion, Signature}, + transaction::Version, + Amount, FeeRate, OutPoint, Sequence, TapLeafHash, TapSighashType, TapTweakHash, Transaction, + TxIn, TxOut, Txid, Weight, Witness, XOnlyPublicKey, +}; +use bitcoin_splitter::split::script::SplitableScript; +use musig2::{ + secp256k1::{schnorr, PublicKey, SecretKey, Signing}, + AggNonce, PartialSignature, SecNonce, +}; + +use crate::{ + assert::Assert, + claim::{scripts::OptimisticPayoutScript, FundedClaim}, + context::Context, + schnorr_sign_partial, + scripts::OP_CHECKCOVENANT, + treepp::*, +}; + +/// Assuming that mean block mining time is 10 minutes: +pub const LOCKTIME: u16 = 6 /* hour */ * 24 /* day */ * 14 /* two weeks */; + +pub const PAYOUT_APPROX_WEIGHT: Weight = Weight::from_vb_unchecked(2000); +pub const PAYOUT_OPTIMISITC_APPROX_WEIGHT: Weight = Weight::from_vb_unchecked(780); + +/// Script by which Operator spends the Assert transaction after timelock. +#[derive(Debug, Clone)] +pub struct PayoutScript { + /// Comittee public keys + pub comittee_aggpubkey: XOnlyPublicKey, + /// Public key of the operator + pub operator_pubkey: XOnlyPublicKey, + + /// Specified locktime after which assert transaction is spendable + /// by payout script, default value is [`LOCKTIME`]. + pub locktime: Height, +} + +impl PayoutScript { + pub fn new(operator_pubkey: XOnlyPublicKey, comittee_aggpubkey: XOnlyPublicKey) -> Self { + Self { + operator_pubkey, + comittee_aggpubkey, + locktime: Height::from(LOCKTIME), + } + } + + pub fn with_locktime( + operator_pubkey: XOnlyPublicKey, + comittee_aggpubkey: XOnlyPublicKey, + locktime: Height, + ) -> Self { + Self { + operator_pubkey, + comittee_aggpubkey, + locktime, + } + } + + pub fn to_script(&self) -> Script { + script! { + { self.locktime.value() as u32 } + OP_CSV + OP_DROP + { self.operator_pubkey } + OP_CHECKSIGVERIFY + { OP_CHECKCOVENANT(self.comittee_aggpubkey) } + } + } +} + +pub struct PayoutOptimistic { + /// Claim transaction id. + claim_txid: Txid, + + /// Operator's pubkey for output. + operator_script_pubkey: Script, + + /// Claim transaction challenge period. + claim_challenge_period: Height, + + /// Stacked amount mentioned in paper as $d$. + staked_amount: Amount, +} + +impl PayoutOptimistic { + pub fn from_context(ctx: &Context, claim_txid: Txid, fee_rate: FeeRate) -> Self + where + S: SplitableScript, + C: Verification, + { + Self { + operator_script_pubkey: ctx.operator_script_pubkey.clone(), + claim_challenge_period: ctx.claim_challenge_period, + staked_amount: ctx.staked_amount + + fee_rate + .checked_mul_by_weight( + ctx.assert_tx_weight + ctx.largest_disprove_weight + - PAYOUT_OPTIMISITC_APPROX_WEIGHT, + ) + .unwrap(), + claim_txid, + } + } + + pub fn to_unsigned_tx(&self) -> Transaction { + Transaction { + version: Version::TWO, + lock_time: LockTime::ZERO, + input: vec![ + TxIn { + previous_output: OutPoint::new(self.claim_txid, 0), + script_sig: Script::new(), + sequence: Sequence::from_height(self.claim_challenge_period.value()), + witness: Witness::new(), + }, + TxIn { + previous_output: OutPoint::new(self.claim_txid, 1), + script_sig: Script::new(), + sequence: Sequence::ZERO, + witness: Witness::new(), + }, + ], + output: vec![TxOut { + value: self.staked_amount, + script_pubkey: self.operator_script_pubkey.clone(), + }], + } + } + + pub fn sign_partial_from_claim( + &self, + ctx: &Secp256k1, + claim: &FundedClaim, + comittee_pubkeys: Vec, + agg_nonce: &AggNonce, + secret_key: SecretKey, + secnonce: SecNonce, + ) -> PartialSignature { + let tx = claim.to_tx(ctx); + let claim_assert_output = &tx.output[0]; + let claim_challenge_output = &tx.output[1]; + let claim_assert_script = claim.optimistic_payout_script(); + + self.sign_partial( + ctx, + claim_assert_output, + claim_challenge_output, + &claim_assert_script, + comittee_pubkeys, + agg_nonce, + secret_key, + secnonce, + ) + } + + #[allow(clippy::too_many_arguments)] + pub fn sign_partial( + &self, + ctx: &Secp256k1, + claim_assert_output: &TxOut, + claim_challenge_output: &TxOut, + claim_assert_script: &OptimisticPayoutScript, + comittee_pubkeys: Vec, + agg_nonce: &AggNonce, + secret_key: SecretKey, + secnonce: SecNonce, + ) -> PartialSignature { + let sighash = self.assert_sighash( + claim_assert_output, + claim_challenge_output, + claim_assert_script, + ); + + schnorr_sign_partial( + ctx, + sighash, + comittee_pubkeys, + agg_nonce, + secret_key, + secnonce, + ) + } + + /// Return sighash for assert output of Claim transaction. + pub(crate) fn assert_sighash( + &self, + claim_assert_txout: &TxOut, + claim_challenge_txout: &TxOut, + claim_payout_script: &OptimisticPayoutScript, + ) -> bitcoin::TapSighash { + let leaf_hash = + TapLeafHash::from_script(&claim_payout_script.to_script(), LeafVersion::TapScript); + let unsigned_tx = self.to_unsigned_tx(); + SighashCache::new(&unsigned_tx) + .taproot_script_spend_signature_hash( + /* Payout optimisitc assert input is the first one */ 0, + &Prevouts::All(&[claim_assert_txout, claim_challenge_txout]), + leaf_hash, + TapSighashType::All, + ) + .unwrap() + } + + pub fn sign_challenge_input( + &self, + claim_assert_txout: &TxOut, + claim_challenge_txout: &TxOut, + ctx: &Secp256k1, + mut secret_key: SecretKey, + ) -> schnorr::Signature { + let unsigned_tx = self.to_unsigned_tx(); + let sighash = SighashCache::new(&unsigned_tx) + .taproot_key_spend_signature_hash( + /* Payout optimisitc challenge input is the second one */ 1, + &Prevouts::All(&[claim_assert_txout, claim_challenge_txout]), + TapSighashType::All, + ) + .unwrap(); + + let (xonly, parity) = secret_key.public_key(ctx).x_only_public_key(); + if parity == Parity::Odd { + secret_key = secret_key.negate(); + } + let tweak = TapTweakHash::from_key_and_tweak(xonly, None); + secret_key = secret_key.add_tweak(&tweak.to_scalar()).unwrap(); + + ctx.sign_schnorr(&sighash.into(), &Keypair::from_secret_key(ctx, &secret_key)) + } +} + +pub struct SignedPayoutOptimistic { + /// Unsigned payout transaction + inner: PayoutOptimistic, + + /// Multisig created with comittee. + covenants_sig: Signature, + + /// Operator's spending witness created by wallet. + operator_sig: Signature, + + /// Script by which transaction will be spent + script: Script, + + /// Control block with inclusion proof to payout script + script_control_block: ControlBlock, +} + +impl SignedPayoutOptimistic { + pub fn new( + inner: PayoutOptimistic, + covenants_sig: impl Into, + operator_sig: impl Into, + script: &OptimisticPayoutScript, + script_control_block: ControlBlock, + ) -> Self { + Self { + inner, + covenants_sig: Signature { + signature: covenants_sig.into(), + sighash_type: TapSighashType::All, + }, + operator_sig: Signature { + signature: operator_sig.into(), + sighash_type: TapSighashType::All, + }, + script: script.to_script(), + script_control_block, + } + } + + pub fn to_tx(&self) -> Transaction { + let mut unsigned_tx = self.inner.to_unsigned_tx(); + + /* Fill assert output */ + let witness = &mut unsigned_tx.input[0].witness; + witness.push(self.covenants_sig.serialize()); + witness.push(&self.script); + witness.push(self.script_control_block.serialize()); + + /* Fill challenge output */ + let witness = &mut unsigned_tx.input[1].witness; + witness.push(self.operator_sig.serialize()); + + unsigned_tx + } +} + +pub struct Payout { + /// Assert transaction id. + assert_txid: Txid, + + /// Operator's pubkey for output. + operator_pubkey: XOnlyPublicKey, + + /// Assert transaction challenge period. + assert_challenge_period: Height, + + /// Stacked amount mentioned in paper as $d$. + staked_amount: Amount, +} + +impl Payout { + pub fn new( + assert_txid: Txid, + operator_pubkey: XOnlyPublicKey, + assert_challenge_period: Height, + staked_amount: Amount, + ) -> Self { + Self { + assert_txid, + operator_pubkey, + assert_challenge_period, + staked_amount, + } + } + + pub fn from_context(ctx: &Context, assert_txid: Txid, fee_rate: FeeRate) -> Self + where + S: SplitableScript, + C: Verification, + { + Self { + operator_pubkey: ctx.operator_pubkey.into(), + assert_challenge_period: ctx.assert_challenge_period, + // we spend in assert transaction part of the amount, so + // only left with "stacked_amount" and amount which was + // supposed to be spent by one of the disproves. + staked_amount: ctx.staked_amount + + fee_rate + .checked_mul_by_weight(ctx.largest_disprove_weight) + .unwrap() + - fee_rate + .checked_mul_by_weight(ctx.payout_tx_weight) + .unwrap(), + assert_txid, + } + } + + pub fn to_unsigned_tx(&self, ctx: &Secp256k1) -> Transaction + where + C: Verification, + { + Transaction { + version: Version::TWO, + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint::new(self.assert_txid, 0), + script_sig: Script::new(), + sequence: Sequence::from_height(self.assert_challenge_period.value()), + witness: Witness::new(), + }], + output: vec![TxOut { + value: self.staked_amount, + script_pubkey: Script::new_p2tr(ctx, self.operator_pubkey, None), + }], + } + } + + pub fn sign_partial_from_assert( + &self, + ctx: &Secp256k1, + assert: &Assert, + comittee_pubkeys: Vec, + agg_nonce: &AggNonce, + secret_key: SecretKey, + secnonce: SecNonce, + ) -> PartialSignature { + let assert_output = assert.output(ctx); + + self.sign_partial( + ctx, + &assert_output, + assert.payout_script(), + comittee_pubkeys, + agg_nonce, + secret_key, + secnonce, + ) + } + + #[allow(clippy::too_many_arguments)] + pub fn sign_partial( + &self, + ctx: &Secp256k1, + assert_output: &TxOut, + assert_payout: &PayoutScript, + comittee_pubkeys: Vec, + agg_nonce: &AggNonce, + secret_key: SecretKey, + secnonce: SecNonce, + ) -> PartialSignature { + let sighash = self.sighash(ctx, assert_output, assert_payout); + + schnorr_sign_partial( + ctx, + sighash, + comittee_pubkeys, + agg_nonce, + secret_key, + secnonce, + ) + } + + pub fn sign_operator( + &self, + ctx: &Secp256k1, + assert_txout: &TxOut, + assert_payout: &PayoutScript, + seckey: &SecretKey, + ) -> Signature { + let unsigned_tx = self.to_unsigned_tx(ctx); + let leaf_hash = + TapLeafHash::from_script(&assert_payout.to_script(), LeafVersion::TapScript); + let sighash_type = TapSighashType::SinglePlusAnyoneCanPay; + + let sighash = SighashCache::new(&unsigned_tx) + .taproot_script_spend_signature_hash( + /* Payout is signed fully and should have only one input */ 0, + &Prevouts::All(&[assert_txout]), + leaf_hash, + sighash_type, + ) + .unwrap(); + + let signature = ctx.sign_schnorr(&sighash.into(), &Keypair::from_secret_key(ctx, seckey)); + + Signature { + signature, + sighash_type, + } + } + + pub(crate) fn sighash( + &self, + ctx: &Secp256k1, + assert_output: &TxOut, + assert_payout_script: &PayoutScript, + ) -> bitcoin::TapSighash { + let unsigned_tx = self.to_unsigned_tx(ctx); + let leaf_hash = + TapLeafHash::from_script(&assert_payout_script.to_script(), LeafVersion::TapScript); + + SighashCache::new(&unsigned_tx) + .taproot_script_spend_signature_hash( + /* assert output with taproot should be first */ 0, + &Prevouts::All(&[assert_output]), + leaf_hash, + TapSighashType::All, + ) + .unwrap() + } +} + +pub struct SignedPayout { + inner: Payout, + + payout_script: PayoutScript, + payout_control_block: ControlBlock, + + /// Covenant signature created with comittee + covenants_sig: Signature, + /// Operator's signature. + operators_sig: Signature, +} + +impl SignedPayout { + pub fn new( + inner: Payout, + payout_script: PayoutScript, + payout_control_block: ControlBlock, + covenants_sig: impl Into, + operators_sig: Signature, + ) -> Self { + Self { + inner, + payout_script, + payout_control_block, + covenants_sig: Signature { + signature: covenants_sig.into(), + sighash_type: TapSighashType::All, + }, + operators_sig, + } + } + + pub fn to_tx(&self, ctx: &Secp256k1) -> Transaction { + let mut unsigned_tx = self.inner.to_unsigned_tx(ctx); + + let witness = &mut unsigned_tx.input[0].witness; + + /* Push stack elements */ + witness.push(self.covenants_sig.serialize()); + witness.push(self.operators_sig.serialize()); + /* script */ + witness.push(self.payout_script.to_script()); + /* control block */ + witness.push(self.payout_control_block.serialize()); + + unsigned_tx + } +} diff --git a/nero-core/src/scripts.rs b/nero-core/src/scripts.rs new file mode 100644 index 0000000..fc2c798 --- /dev/null +++ b/nero-core/src/scripts.rs @@ -0,0 +1,107 @@ +//! Shared between transaction scripts + +#![allow(non_snake_case)] + +use bitcoin::XOnlyPublicKey; + +use crate::treepp::*; + +/// `CheckCovenant` from BitVM2 paper which accepts aggregated pubkey and +/// expects signature from top of the stack. +pub fn OP_CHECKCOVENANTVERIFY(agg_pubkey: XOnlyPublicKey) -> Script { + script! { + { agg_pubkey } + OP_CHECKSIGVERIFY + } +} + +/// `CheckCovenant` from BitVM2 paper which accepts aggregated pubkey and +/// expects signature from top of the stack. +pub fn OP_CHECKCOVENANT(agg_pubkey: XOnlyPublicKey) -> Script { + script! { + { agg_pubkey } + OP_CHECKSIG + } +} + +#[cfg(test)] +mod tests { + use crate::{ + claim::scripts::AssertScript, + disprove::{extract_signed_states, signing::SignedIntermediateState}, + treepp::*, + }; + use bitcoin::{key::Secp256k1, taproot::LeafVersion, TapLeafHash, XOnlyPublicKey}; + use bitcoin_scriptexec::Stack; + use bitcoin_splitter::split::{ + intermediate_state::IntermediateState, + script::{IOPair, SplitableScript as _}, + }; + use bitcoin_testscripts::int_mul_windowed::U32MulScript; + use bitcoin_utils::{comittee_signature, debug::execute_script_with_leaf}; + use rand::thread_rng; + + use crate::disprove::form_disprove_scripts; + + #[test] + fn test_simple_singed_states_assert_script() { + let secp_ctx = Secp256k1::new(); + let (seckey, pubkey) = secp_ctx.generate_keypair(&mut thread_rng()); + let xonly: XOnlyPublicKey = pubkey.into(); + + let mut stack = Stack::new(); + stack.pushnum(8); + + let signed_states = &[SignedIntermediateState::sign(IntermediateState { + stack, + altstack: Stack::new(), + })]; + + let assert_script = AssertScript::new(xonly, signed_states.iter()).into_script(); + let leaf_hash = TapLeafHash::from_script(&assert_script, LeafVersion::TapScript); + let sig = comittee_signature(&assert_script, &secp_ctx, seckey); + + let verify_script = script! { + for state in signed_states.iter().rev() { + { state.to_script_sig() } + } + { sig.serialize().to_vec() } + { assert_script } + }; + + let result = execute_script_with_leaf(verify_script, leaf_hash); + + assert!(result.success, "Script failed:\n{result}"); + } + + #[test] + fn test_u32mul_signed_states_assert_script() { + // First, we generate the pair of input and output scripts + let IOPair { input, output: _ } = U32MulScript::generate_valid_io_pair(); + + let secp_ctx = Secp256k1::new(); + let (seckey, pubkey) = secp_ctx.generate_keypair(&mut thread_rng()); + let xonly: XOnlyPublicKey = pubkey.into(); + + let disprove_scripts = form_disprove_scripts::(input, xonly); + + let signed_states = extract_signed_states(&disprove_scripts); + + let assert_script = AssertScript::new(xonly, signed_states).into_script(); + let leaf_hash = TapLeafHash::from_script(&assert_script, LeafVersion::TapScript); + let sig = comittee_signature(&assert_script, &secp_ctx, seckey); + + let signed_states = extract_signed_states(&disprove_scripts); + let verify_script = script! { + for state in signed_states.rev() { + { state.to_script_sig() } + } + { sig.serialize().to_vec() } + { assert_script } + }; + + let result = execute_script_with_leaf(verify_script, leaf_hash); + + assert!(result.success, "Script failed:\n{result}"); + } +} diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..8fe2675 --- /dev/null +++ b/shell.nix @@ -0,0 +1,16 @@ +{ +rust-overlay ? builtins.fetchTarball + "https://github.com/oxalica/rust-overlay/archive/0043c3f92304823cc2c0a4354b0feaa61dfb4cd9.tar.gz" +, pkgs ? import { overlays = [ (import rust-overlay) ]; } }: +let + rust-toolchain = pkgs.rust-bin.stable."1.79.0".default.override { + extensions = ["clippy" "rust-analyzer" "rust-src"]; + }; +in pkgs.mkShell { + buildInputs = [ + rust-toolchain + ] ++ (with pkgs.darwin.apple_sdk.frameworks; [ + Security + SystemConfiguration + ]); +}