Skip to content

Commit

Permalink
[Actor, TxHandler] add signing of txhandler through tagged signatures (
Browse files Browse the repository at this point in the history
…#511)

* prototype

* prototype2

* add winternitz and preimagereveal

* refactor with script kind for easy matching

* feat(actor): remove txhandler clone by refactoring sighash calc

Added a new scriptkind parser for code quality with tests
Implemented sighash calculation closure to avoid cloning and ensure immutability of the TxHandler inputs.

* test: actor correctly signs

* chore(actor): rename winternitz partial sign

* fix: tests

* Remove TypeId runtime for SpendableScript to get ScriptKind (#512)

* change option

* fix: errors from merge, add calculate_sighash

* fix: add spend info to assert end txhandler

* fix: winternitz commit script, add tests

* chore: cargo fmt

* chore: clippy

* chore: rename functions and clippy

* chore: clippy

---------

Co-authored-by: atacann <[email protected]>
Co-authored-by: Roman <[email protected]>
  • Loading branch information
3 people authored Feb 10, 2025
1 parent aa0864a commit 4541246
Show file tree
Hide file tree
Showing 15 changed files with 1,201 additions and 333 deletions.
585 changes: 460 additions & 125 deletions core/src/actor.rs

Large diffs are not rendered by default.

622 changes: 574 additions & 48 deletions core/src/builder/script.rs

Large diffs are not rendered by default.

65 changes: 30 additions & 35 deletions core/src/builder/sighash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::errors::BridgeError;
use crate::rpc::clementine::tagged_signature::SignatureId;
use crate::{builder, database::Database, EVMAddress};
use async_stream::try_stream;
use bitcoin::{address::NetworkUnchecked, Address, Amount, OutPoint};
use bitcoin::{address::NetworkUnchecked, Address, Amount, OutPoint, TapSighashType};
use bitcoin::{TapSighash, Txid, XOnlyPublicKey};
use futures_core::stream::Stream;

Expand Down Expand Up @@ -119,7 +119,6 @@ pub fn create_nofn_sighash_stream(
bridge_amount_sats: Amount,
network: bitcoin::Network,
) -> impl Stream<Item = Result<(TapSighash, SignatureInfo), BridgeError>> {
use bitcoin::TapSighashType::All as SighashAll;
try_stream! {
// Create move_tx handler. This is unique for each deposit tx.
let move_txhandler = builder::transaction::create_move_to_vault_txhandler(
Expand Down Expand Up @@ -199,9 +198,9 @@ pub fn create_nofn_sighash_stream(
)?;

// Yields the sighash for the challenge_tx.input[0], which spends kickoff_tx.input[1] using SinglePlusAnyoneCanPay.
yield (challenge_tx.calculate_pubkey_spend_sighash(
yield (challenge_tx.calculate_sighash(
0,
Some(bitcoin::sighash::TapSighashType::SinglePlusAnyoneCanPay)
bitcoin::sighash::TapSighashType::SinglePlusAnyoneCanPay
)?, partial.complete(challenge_tx.get_signature_id(0)?));

// Creates the start_happy_reimburse_tx handler.
Expand All @@ -211,9 +210,9 @@ pub fn create_nofn_sighash_stream(
network
)?;
// Yields the sighash for the start_happy_reimburse_tx.input[1], which spends kickoff_tx.output[3].
yield (start_happy_reimburse_txhandler.calculate_pubkey_spend_sighash(
yield (start_happy_reimburse_txhandler.calculate_sighash(
1,
None
TapSighashType::Default
)?, partial.complete(start_happy_reimburse_txhandler.get_signature_id(1)?));

// Creates the happy_reimburse_tx handler.
Expand All @@ -226,9 +225,9 @@ pub fn create_nofn_sighash_stream(
)?;

// Yields the sighash for the happy_reimburse_tx.input[0], which spends move_to_vault_tx.output[0].
yield (happy_reimburse_txhandler.calculate_pubkey_spend_sighash(
yield (happy_reimburse_txhandler.calculate_sighash(
0,
None
TapSighashType::Default
)?, partial.complete(happy_reimburse_txhandler.get_signature_id(0)?));

// Collect the challenge Winternitz pubkeys for this specific kickoff_utxo.
Expand All @@ -244,9 +243,9 @@ pub fn create_nofn_sighash_stream(
)?;

// Yields the sighash for the watchtower_challenge_kickoff_tx.input[0], which spends kickoff_tx.input[0].
yield (watchtower_challenge_kickoff_txhandler.calculate_pubkey_spend_sighash(
yield (watchtower_challenge_kickoff_txhandler.calculate_sighash(
0,
None,
TapSighashType::Default,
)?, partial.complete(watchtower_challenge_kickoff_txhandler.get_signature_id(0)?));

// Creates the kickoff_timeout_tx handler.
Expand All @@ -256,10 +255,9 @@ pub fn create_nofn_sighash_stream(
)?;

// Yields the sighash for the kickoff_timeout_tx.input[0], which spends kickoff_tx.output[3].
yield (kickoff_timeout_txhandler.calculate_script_spend_sighash_indexed(
0,
yield (kickoff_timeout_txhandler.calculate_sighash(
0,
SighashAll
TapSighashType::Default
)?, partial.complete(kickoff_timeout_txhandler.get_signature_id(0)?));

let public_hashes = db.get_operators_challenge_ack_hashes(None, operator_idx as i32, sequential_collateral_tx_idx as i32, kickoff_idx as i32).await?.ok_or(BridgeError::WatchtowerPublicHashesNotFound(operator_idx as i32, sequential_collateral_tx_idx as i32, kickoff_idx as i32))?;
Expand Down Expand Up @@ -288,16 +286,15 @@ pub fn create_nofn_sighash_stream(
)?;

// Yields the sighash for the operator_challenge_NACK_tx.input[0], which spends watchtower_challenge_tx.output[0].
yield (operator_challenge_nack_txhandler.calculate_script_spend_sighash_indexed(
yield (operator_challenge_nack_txhandler.calculate_sighash(
0,
1,
SighashAll,
TapSighashType::Default,
)?, partial.complete(operator_challenge_nack_txhandler.get_signature_id(0)?));

// Yields the sighash for the operator_challenge_NACK_tx.input[1], which spends kickoff_tx.output[2].
yield (operator_challenge_nack_txhandler.calculate_pubkey_spend_sighash(
yield (operator_challenge_nack_txhandler.calculate_sighash(
1,
None,
TapSighashType::Default
)?, partial.complete(operator_challenge_nack_txhandler.get_signature_id(1)?));
}

Expand All @@ -321,9 +318,9 @@ pub fn create_nofn_sighash_stream(
)?;

// Yields the sighash for the assert_end_tx, which spends kickoff_tx.output[3].
yield (assert_end_txhandler.calculate_pubkey_spend_sighash(
yield (assert_end_txhandler.calculate_sighash(
PARALLEL_ASSERT_TX_CHAIN_SIZE,
None,
TapSighashType::Default,
)?, partial.complete(assert_end_txhandler.get_signature_id(PARALLEL_ASSERT_TX_CHAIN_SIZE)?));

// Creates the disprove_timeout_tx handler.
Expand All @@ -334,16 +331,15 @@ pub fn create_nofn_sighash_stream(
)?;

// Yields the sighash for the disprove_timeout_tx.input[0], which spends assert_end_tx.output[0].
yield (disprove_timeout_txhandler.calculate_pubkey_spend_sighash(
yield (disprove_timeout_txhandler.calculate_sighash(
0,
None,
TapSighashType::Default
)?, partial.complete(disprove_timeout_txhandler.get_signature_id(0)?));

// Yields the disprove_timeout_tx.input[1], which spends assert_end_tx.output[1].
yield (disprove_timeout_txhandler.calculate_script_spend_sighash_indexed(
yield (disprove_timeout_txhandler.calculate_sighash(
1,
0,
SighashAll,
TapSighashType::Default,
)?, partial.complete(disprove_timeout_txhandler.get_signature_id(1)?));

// Creates the already_disproved_tx handler.
Expand All @@ -353,10 +349,9 @@ pub fn create_nofn_sighash_stream(
)?;

// Yields the sighash for the already_disproved_tx.input[0], which spends assert_end_tx.output[1].
yield (already_disproved_txhandler.calculate_script_spend_sighash_indexed(
yield (already_disproved_txhandler.calculate_sighash(
0,
1,
SighashAll,
TapSighashType::Default,
)?, partial.complete(already_disproved_txhandler.get_signature_id(0)?));

// Creates the reimburse_tx handler.
Expand All @@ -369,7 +364,7 @@ pub fn create_nofn_sighash_stream(
)?;

// Yields the sighash for the reimburse_tx.input[0], which spends move_to_vault_tx.output[0].
yield (reimburse_txhandler.calculate_pubkey_spend_sighash(0, None)?, partial.complete(reimburse_txhandler.get_signature_id(0)?));
yield (reimburse_txhandler.calculate_sighash(0, TapSighashType::Default)?, partial.complete(reimburse_txhandler.get_signature_id(0)?));
}

input_txid = *reimburse_generator_txhandler.get_txid();
Expand Down Expand Up @@ -465,9 +460,9 @@ pub fn create_operator_sighash_stream(
)?;

// Yields the sighash for the kickoff_timeout_tx.input[0], which spends kickoff_tx.output[3].
yield (kickoff_timeout_txhandler.calculate_pubkey_spend_sighash(
yield (kickoff_timeout_txhandler.calculate_sighash(
1,
None,
TapSighashType::Default,
)?, partial.complete(kickoff_timeout_txhandler.get_signature_id(1)?));

let (assert_tx_addrs, root_hash, _public_input_wots) = db.get_bitvm_setup(None, operator_idx as i32, sequential_collateral_idx as i32, kickoff_utxo_idx as i32).await?.ok_or(BridgeError::BitvmSetupNotFound(operator_idx as i32, sequential_collateral_idx as i32, kickoff_utxo_idx as i32))?;
Expand Down Expand Up @@ -496,9 +491,9 @@ pub fn create_operator_sighash_stream(
)?;

// Yields the sighash for the already_disproved_tx.input[0], which spends assert_end_tx.output[1].
yield (already_disproved_txhandler.calculate_pubkey_spend_sighash(
yield (already_disproved_txhandler.calculate_sighash(
1,
None,
TapSighashType::Default,
)?, partial.complete(already_disproved_txhandler.get_signature_id(1)?));

let disprove_txhandler = builder::transaction::create_disprove_txhandler(
Expand All @@ -507,9 +502,9 @@ pub fn create_operator_sighash_stream(
)?;

// Yields the sighash for the disprove_tx.input[1], which spends sequential_collateral_tx.output[0].
yield (disprove_txhandler.calculate_pubkey_spend_sighash(
yield (disprove_txhandler.calculate_sighash(
1,
None,
TapSighashType::Default,
)?, partial.complete(disprove_txhandler.get_signature_id(1)?));
}

Expand Down
5 changes: 1 addition & 4 deletions core/src/builder/transaction/challenge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ use crate::constants::{BLOCKS_PER_WEEK, OPERATOR_CHALLENGE_AMOUNT};
use crate::errors::BridgeError;
use crate::rpc::clementine::{NormalSignatureKind, WatchtowerSignatureKind};
use bitcoin::{Amount, ScriptBuf, Sequence, TxOut, XOnlyPublicKey};
use bitvm::signatures::winternitz;
use std::sync::Arc;

/// Creates a [`TxHandler`] for the `watchtower_challenge_kickoff_tx`. This transaction can be sent by anyone.
Expand All @@ -27,13 +26,11 @@ pub fn create_watchtower_challenge_kickoff_txhandler(
DEFAULT_SEQUENCE,
);

let wots_params = winternitz::Parameters::new(240, 4);

for i in 0..num_watchtowers {
let winternitz_commit = Arc::new(WinternitzCommit::new(
watchtower_challenge_winternitz_pks[i as usize].clone(),
wots_params.clone(),
watchtower_xonly_pks[i as usize],
240,
));
builder = builder.add_output(UnspentTxOut::from_scripts(
Amount::from_sat(2000), // TODO: Hand calculate this
Expand Down
4 changes: 4 additions & 0 deletions core/src/builder/transaction/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,10 @@ impl SpentTxIn {
&self.spendable
}

pub fn get_spend_path(&self) -> SpendPath {
self.spend_path
}

pub fn get_witness(&self) -> &Option<Witness> {
&self.witness
}
Expand Down
24 changes: 14 additions & 10 deletions core/src/builder/transaction/operator_assert.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::builder;
use crate::builder::script::TimelockScript;
use crate::builder::script::{SpendableScript, TimelockScript};
pub use crate::builder::transaction::txhandler::TxHandler;
pub use crate::builder::transaction::*;
use crate::constants::{BLOCKS_PER_WEEK, MIN_TAPROOT_AMOUNT, PARALLEL_ASSERT_TX_CHAIN_SIZE};
Expand Down Expand Up @@ -176,8 +176,10 @@ pub fn create_assert_end_txhandler(
);

let disprove_taproot_spend_info = TaprootBuilder::new()
.add_hidden_node(0, TapNodeHash::from_byte_array(*root_hash))
.expect("empty taptree will accept a node at depth 0")
.add_hidden_node(1, TapNodeHash::from_byte_array(*root_hash))
.expect("empty taptree will accept a node at depth 1")
.add_leaf(1, CheckSig(nofn_xonly_pk).to_script_buf())
.expect("taptree with one node at depth 1 will accept a script node")
.finalize(&SECP, nofn_xonly_pk) // TODO: we should convert this to script spend but we only have partial access to the taptree
.expect("finalize always succeeds for taptree with single node at depth 0");

Expand All @@ -196,20 +198,22 @@ pub fn create_assert_end_txhandler(

// Add outputs
Ok(builder
.add_output(UnspentTxOut::from_partial(TxOut {
value: MIN_TAPROOT_AMOUNT,
script_pubkey: disprove_address.script_pubkey().clone(),
}))
.add_output(UnspentTxOut::new(
TxOut {
value: MIN_TAPROOT_AMOUNT,
script_pubkey: disprove_address.script_pubkey().clone(),
},
vec![Arc::new(CheckSig::new(nofn_xonly_pk))],
Some(disprove_taproot_spend_info), // not disprove_taproot_spend_info as it will cause check to fail because we do not store all scripts
))
.add_output(UnspentTxOut::from_scripts(
MIN_TAPROOT_AMOUNT,
vec![nofn_1week, nofn_2week],
None,
network,
))
.add_output(UnspentTxOut::new(
.add_output(UnspentTxOut::from_partial(
builder::transaction::anchor_output(),
vec![],
None,
))
.finalize())
}
Expand Down
Loading

0 comments on commit 4541246

Please sign in to comment.