From 840d444c8cc063afc1d823d2c801b3b954b20ace Mon Sep 17 00:00:00 2001 From: Jose Storopoli Date: Tue, 24 Sep 2024 11:11:34 -0300 Subject: [PATCH] feat(bridge-exec)!: aggregate_[deposit|withdraw]_sig --- Cargo.lock | 1 + crates/bridge-exec/Cargo.toml | 1 + .../src/deposit_handler/handler.rs | 181 +++++++++++++++++- .../src/withdrawal_handler/handler.rs | 180 +++++++++++++---- crates/db/src/entities/bridge_tx_state.rs | 24 ++- crates/primitives/src/bridge.rs | 13 +- 6 files changed, 355 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 755b7241e..da9898f31 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3836,6 +3836,7 @@ dependencies = [ name = "express-bridge-exec" version = "0.1.0" dependencies = [ + "alpen-express-btcio", "alpen-express-primitives", "alpen-express-rpc-api", "async-trait", diff --git a/crates/bridge-exec/Cargo.toml b/crates/bridge-exec/Cargo.toml index b9e180a05..1e6dbdc8f 100644 --- a/crates/bridge-exec/Cargo.toml +++ b/crates/bridge-exec/Cargo.toml @@ -13,6 +13,7 @@ rust.unused_must_use = "deny" rustdoc.all = "warn" [dependencies] +alpen-express-btcio = { workspace = true } alpen-express-primitives = { workspace = true } alpen-express-rpc-api = { workspace = true, features = ["client"] } express-bridge-sig-manager = { workspace = true } diff --git a/crates/bridge-exec/src/deposit_handler/handler.rs b/crates/bridge-exec/src/deposit_handler/handler.rs index b2b085fc9..f41cd2527 100644 --- a/crates/bridge-exec/src/deposit_handler/handler.rs +++ b/crates/bridge-exec/src/deposit_handler/handler.rs @@ -8,7 +8,7 @@ use alpen_express_primitives::{ relay::{types::Scope, util::MessageSigner}, }; use alpen_express_rpc_api::AlpenApiClient; -use bitcoin::{secp256k1::SECP256K1, Txid}; +use bitcoin::{secp256k1::SECP256K1, Transaction, Txid}; use express_bridge_sig_manager::manager::SignatureManager; use express_bridge_tx_builder::{deposit::DepositInfo, prelude::*, TxKind}; use jsonrpsee::tokio::time::{sleep, Duration}; @@ -180,12 +180,175 @@ pub async fn sign_deposit_tx( } } -/// Add the signature to the already accumulated set of signatures for a deposit transaction and -/// produce the aggregated signature if all operators have signed. Also update the database -/// entry with the signatures accumulated so far. -// -// TODO: this method will also accept a `BridgeMessage` that holds the signature attached to a -// particular deposit info by other operators. -pub async fn aggregate_signature() -> DepositExecResult> { - unimplemented!() +/// Pools and aggregates signatures for the deposit transaction +/// into a fully-signed ready-to-be-broadcasted Bitcoin [`Transaction`]. +/// +/// Also broadcasts to the bridge transaction database. +/// +/// # Arguments +/// +/// - `txid`: a [`Txid`] that is in the bridge transaction database. +/// - `l1_rpc_client`: anything that is able to sign transactions and messages; i.e. holds private +/// keys. +/// - `l2_rpc_client`: anything that can communicate with the [`AlpenApiClient`]. +/// - `sig_manager`: a stateful [`SignatureManager`]. +/// - `tx_build_context`: stateful [`TxBuildContext`]. +/// +/// # Notes +/// +/// Both the [`SignatureManager`] and the [`TxBuildContext`] can be reused +/// for multiple signing sessions if the operators' +/// [`PublickeyTable`](alpen_express_primitives::bridge::PublickeyTable) +/// does _not_ change. +/// +/// We don't need mutexes since all functions to [`SignatureManager`] and +/// [`TxBuildContext`] takes non-mutable references. +pub async fn aggregate_deposit_sig( + txid: &Txid, + l1_rpc_client: &Arc, + l2_rpc_client: &Arc, + sig_manager: &Arc, + tx_build_context: &Arc, +) -> DepositExecResult { + // setup logging + let span = span!( + Level::INFO, + "starting deposit transaction signature aggregation" + ); + let _guard = span.enter(); + + let operator_pubkeys = tx_build_context.pubkey_table(); + let own_index = tx_build_context.own_index(); + let own_pubkey = operator_pubkeys + .0 + .get(&own_index) + .expect("could not find operator's pubkey in public key table"); + + event!( + Level::INFO, + event = "got the basic self information", + %txid, + %own_index, + %own_pubkey, + ); + + let tx_state = sig_manager + .get_tx_state(txid) + .await + .map_err(|e| DepositExecError::Signing(e.to_string()))?; + + event!( + Level::DEBUG, + event = "got transaction state from bridge database", + %txid, + ?tx_state, + ); + + // Fully signed and in the database, nothing to do here... + if tx_state.is_fully_signed() { + event!( + Level::INFO, + event = "deposit transaction fully signed and in the bridge database", + %txid, + ); + let tx = sig_manager + .finalize_transaction(txid) + .await + .map_err(|e| DepositExecError::Signing(e.to_string()))?; + return Ok(tx); + } + + // Not fully signed, then partially sign transaction, construct, and sign a message + let xpriv = l1_rpc_client.get_xpriv().await?; + if let Some(xpriv) = xpriv { + let keypair = xpriv.to_keypair(SECP256K1); + + // First check if it needs this operator's partial signature + let needs_our_sig = tx_state.collected_sigs().get(&own_index).is_none(); + if needs_our_sig { + sig_manager + .add_own_partial_sig(txid) + .await + .map_err(|e| DepositExecError::Signing(e.to_string()))?; + event!( + Level::INFO, + event = "added own's partial signature to the bridge transaction database", + %txid, + ); + } + + // Now, get this operator's partial sig + let partial_sig = sig_manager + .get_own_partial_sig(txid) + .await + .map_err(|e| DepositExecError::Signing(e.to_string()))? + .expect("should've been signed"); + + event!( + Level::INFO, + event = "got own's partial signature from the bridge transaction database", + ?partial_sig, + ); + + // submit_message RPC call + let bitcoin_txid: BitcoinTxid = (*txid).into(); + let scope = Scope::V0DepositSig(bitcoin_txid); + event!( + Level::DEBUG, + event = "create the deposit partial signature scope", + ?scope + ); + let message = MessageSigner::new(own_index, keypair.secret_key().into()) + .sign_scope(&scope, &partial_sig) + .map_err(|e| DepositExecError::Signing(e.to_string()))?; + event!( + Level::DEBUG, + event = "create the deposit partial signature message", + ?message, + ); + let raw_message: Vec = message + .try_into() + .expect("could not serialize bridge message into raw bytes"); + + l2_rpc_client.submit_bridge_msg(raw_message.into()).await?; + event!( + Level::INFO, + event = "broadcasted the withdrawal partial signature message", + ); + + // Wait for all the partial signatures to be broadcasted by other operators. + // Collect all partial signature. + loop { + event!( + Level::DEBUG, + event = "trying to get all partial signatures from the bridge transaction database, waiting for other operators' signatures", + ); + let got_all_sigs = sig_manager + .get_tx_state(txid) + .await + .map_err(|e| DepositExecError::Signing(e.to_string()))? + .is_fully_signed(); + if got_all_sigs { + event!( + Level::INFO, + event = "got all partial signatures from the bridge transaction database", + %got_all_sigs + ); + break; + } else { + // TODO: this is hardcoded, maybe move this to a user-configurable Config + sleep(Duration::from_millis(100)).await; + continue; + } + } + let tx = sig_manager + .finalize_transaction(txid) + .await + .map_err(|e| DepositExecError::Signing(e.to_string()))?; + Ok(tx) + } else { + Err(DepositExecError::Signing( + "Could not get wallet's xpriv".to_string(), + )) + } } diff --git a/crates/bridge-exec/src/withdrawal_handler/handler.rs b/crates/bridge-exec/src/withdrawal_handler/handler.rs index 810d6db16..a60db7b69 100644 --- a/crates/bridge-exec/src/withdrawal_handler/handler.rs +++ b/crates/bridge-exec/src/withdrawal_handler/handler.rs @@ -8,7 +8,7 @@ use alpen_express_primitives::{ relay::{types::Scope, util::MessageSigner}, }; use alpen_express_rpc_api::AlpenApiClient; -use bitcoin::{secp256k1::SECP256K1, Txid}; +use bitcoin::{secp256k1::SECP256K1, Transaction, Txid}; use express_bridge_sig_manager::manager::SignatureManager; use express_bridge_tx_builder::{prelude::*, withdrawal::CooperativeWithdrawalInfo, TxKind}; use jsonrpsee::tokio::time::{sleep, Duration}; @@ -179,14 +179,36 @@ pub async fn sign_withdrawal_tx( } } -/// Aggregate the received signature with the ones already accumulated. +/// Pools and aggregates signatures for the withdrawal transaction +/// into a fully-signed ready-to-be-broadcasted Bitcoin [`Transaction`]. /// -/// This is executed by the bridge operator that is assigned the given withdrawal. -// TODO: pass in a database client once the database traits have been implemented. -pub async fn aggregate_withdrawal_sig( - _withdrawal_info: &CooperativeWithdrawalInfo, - _sig: &OperatorPartialSig, -) -> WithdrawalExecResult> { +/// Also broadcasts to the bridge transaction database. +/// +/// # Arguments +/// +/// - `txid`: a [`Txid`] that is in the bridge transaction database. +/// - `l1_rpc_client`: anything that is able to sign transactions and messages; i.e. holds private +/// keys. +/// - `l2_rpc_client`: anything that can communicate with the [`AlpenApiClient`]. +/// - `sig_manager`: a stateful [`SignatureManager`]. +/// - `tx_build_context`: stateful [`TxBuildContext`]. +/// +/// # Notes +/// +/// Both the [`SignatureManager`] and the [`TxBuildContext`] can be reused +/// for multiple signing sessions if the operators' +/// [`PublickeyTable`](alpen_express_primitives::bridge::PublickeyTable) +/// does _not_ change. +/// +/// We don't need mutexes since all functions to [`SignatureManager`] and +/// [`TxBuildContext`] takes non-mutable references. +pub async fn aggregate_withdraw_sig( + txid: &Txid, + l1_rpc_client: &Arc, + l2_rpc_client: &Arc, + sig_manager: &Arc, + tx_build_context: &Arc, +) -> WithdrawalExecResult { // setup logging let span = span!( Level::INFO, @@ -194,46 +216,138 @@ pub async fn aggregate_withdrawal_sig( ); let _guard = span.enter(); - // aggregates using MuSig2 the OperatorPartialSig into the BridgeStateOps - // checks if is fully complete - let mut tx_state = get_tx_state_by_txid(db_ops, txid).await?; + let operator_pubkeys = tx_build_context.pubkey_table(); + let own_index = tx_build_context.own_index(); + let own_pubkey = operator_pubkeys + .0 + .get(&own_index) + .expect("could not find operator's pubkey in public key table"); event!( - Level::DEBUG, - event = "got an updated transaction state", + Level::INFO, + event = "got the basic self information", %txid, - ?tx_state + %own_index, + %own_pubkey, ); - let is_fully_signed = tx_state - .add_signature(*sig) - .map_err(|e| WithdrawalExecError::Execution(e.to_string()))?; + let tx_state = sig_manager + .get_tx_state(txid) + .await + .map_err(|e| WithdrawalExecError::Signing(e.to_string()))?; event!( - Level::INFO, - event = "transaction is or isn't fully signed", - %is_fully_signed + Level::DEBUG, + event = "got transaction state from bridge database", + %txid, + ?tx_state, ); - if is_fully_signed { - // get a new up-to-date transaction state - let tx_state = get_tx_state_by_txid(db_ops, txid).await?; - let sig = tx_state - .aggregate_signature() - .map_err(|e| WithdrawalExecError::Execution(e.to_string()))?; + // Fully signed and in the database, nothing to do here... + if tx_state.is_fully_signed() { + event!( + Level::INFO, + event = "withdrawal transaction fully signed and in the bridge database", + %txid, + ); + let tx = sig_manager + .finalize_transaction(txid) + .await + .map_err(|e| WithdrawalExecError::Signing(e.to_string()))?; + return Ok(tx); + } + + // Not fully signed, then partially sign transaction, construct, and sign a message + let xpriv = l1_rpc_client.get_xpriv().await?; + if let Some(xpriv) = xpriv { + let keypair = xpriv.to_keypair(SECP256K1); + + // First check if it needs this operator's partial signature + let needs_our_sig = tx_state.collected_sigs().get(&own_index).is_none(); + if needs_our_sig { + sig_manager + .add_own_partial_sig(txid) + .await + .map_err(|e| WithdrawalExecError::Signing(e.to_string()))?; + event!( + Level::INFO, + event = "added own's partial signature to the bridge transaction database", + %txid, + ); + } + + // Now, get this operator's partial sig + let partial_sig = sig_manager + .get_own_partial_sig(txid) + .await + .map_err(|e| WithdrawalExecError::Signing(e.to_string()))? + .expect("should've been signed"); event!( Level::INFO, - event = "aggregated final signature", - %sig + event = "got own's partial signature from the bridge transaction database", + ?partial_sig, ); - Ok(Some(sig)) - } else { + // submit_message RPC call + let bitcoin_txid: BitcoinTxid = (*txid).into(); + let scope = Scope::V0WithdrawalSig(bitcoin_txid); + event!( + Level::DEBUG, + event = "create the withdrawal partial signature scope", + ?scope + ); + let message = MessageSigner::new(own_index, keypair.secret_key().into()) + .sign_scope(&scope, &partial_sig) + .map_err(|e| WithdrawalExecError::Signing(e.to_string()))?; + event!( + Level::DEBUG, + event = "create the withdrawal partial signature message", + ?message, + ); + let raw_message: Vec = message + .try_into() + .expect("could not serialize bridge message into raw bytes"); + + l2_rpc_client.submit_bridge_msg(raw_message.into()).await?; event!( - Level::WARN, - event = "could not aggregate final signature, missing partial sigs", + Level::INFO, + event = "broadcasted the withdrawal partial signature message", ); - Ok(None) + + // Wait for all the partial signatures to be broadcasted by other operators. + // Collect all partial signature. + loop { + event!( + Level::DEBUG, + event = "trying to get all partial signatures from the bridge transaction database, waiting for other operators' signatures", + ); + let got_all_sigs = sig_manager + .get_tx_state(txid) + .await + .map_err(|e| WithdrawalExecError::Signing(e.to_string()))? + .is_fully_signed(); + if got_all_sigs { + event!( + Level::INFO, + event = "got all partial signatures from the bridge transaction database", + %got_all_sigs + ); + break; + } else { + // TODO: this is hardcoded, maybe move this to a user-configurable Config + sleep(Duration::from_millis(100)).await; + continue; + } + } + let tx = sig_manager + .finalize_transaction(txid) + .await + .map_err(|e| WithdrawalExecError::Signing(e.to_string()))?; + Ok(tx) + } else { + Err(WithdrawalExecError::Signing( + "Could not get wallet's xpriv".to_string(), + )) } } diff --git a/crates/db/src/entities/bridge_tx_state.rs b/crates/db/src/entities/bridge_tx_state.rs index 3016b0ff1..d13cdafd1 100644 --- a/crates/db/src/entities/bridge_tx_state.rs +++ b/crates/db/src/entities/bridge_tx_state.rs @@ -11,9 +11,12 @@ use alpen_express_primitives::{ l1::{BitcoinPsbt, TaprootSpendPath}, }; use arbitrary::Arbitrary; -use bitcoin::{Transaction, TxOut, Txid}; +use bitcoin::{hashes::Hash, Transaction, TxOut, Txid}; use borsh::{BorshDeserialize, BorshSerialize}; -use musig2::{PartialSignature, PubNonce}; +use musig2::{ + aggregate_partial_signatures, secp256k1::schnorr::Signature, AggNonce, KeyAggContext, + PartialSignature, PubNonce, +}; use super::errors::{BridgeTxStateError, EntityResult}; @@ -210,6 +213,23 @@ impl BridgeTxState { pub fn ordered_sigs(&self) -> impl Iterator + '_ { self.collected_sigs.values().map(|v| *v.inner()) } + + /// Aggregates and extracts the underlying partial signatures as a [`Signature`]. + pub fn aggregate_signature(&self) -> EntityResult { + let message = *self.compute_txid().as_byte_array(); + let key_agg_context: KeyAggContext = self + .pubkey_table + .clone() + .try_into() + .map_err(|_| BridgeTxStateError::Unauthorized)?; + let pub_nonces = self.collected_nonces.values().map(|n| n.inner().clone()); + let aggregate_nonce = AggNonce::sum(pub_nonces); + let partial_sigs = self.collected_sigs.values().map(|s| *s.inner()); + let sig: Signature = + aggregate_partial_signatures(&key_agg_context, &aggregate_nonce, partial_sigs, message) + .map_err(|_| BridgeTxStateError::Unauthorized)?; + Ok(sig) + } } #[cfg(test)] diff --git a/crates/primitives/src/bridge.rs b/crates/primitives/src/bridge.rs index d2be70867..2c74dd5de 100644 --- a/crates/primitives/src/bridge.rs +++ b/crates/primitives/src/bridge.rs @@ -11,7 +11,10 @@ use bitcoin::{ secp256k1::{PublicKey, SecretKey}, }; use borsh::{BorshDeserialize, BorshSerialize}; -use musig2::{BinaryEncoding, NonceSeed, PartialSignature, PubNonce, SecNonce}; +use musig2::{ + errors::KeyAggError, BinaryEncoding, KeyAggContext, NonceSeed, PartialSignature, PubNonce, + SecNonce, +}; use serde::{Deserialize, Serialize}; use crate::{ @@ -45,6 +48,14 @@ impl From for Vec { } } +impl TryFrom for KeyAggContext { + type Error = KeyAggError; + + fn try_from(value: PublickeyTable) -> Result { + KeyAggContext::new(Into::>::into(value)) + } +} + impl BorshSerialize for PublickeyTable { fn serialize(&self, writer: &mut W) -> std::io::Result<()> { // Serialize the length of the BTreeMap