From 6ab59294ee45433b8aa5ffc41e3644e513d84e30 Mon Sep 17 00:00:00 2001 From: Runchao Han Date: Mon, 16 Sep 2024 09:43:39 +0800 Subject: [PATCH] btcstaking: verify covenant signatures and undelegation data (#62) Part of #7 This PR adds more verification rules to a newly active BTC delegation: - ensure that all covenant signatures over {slashing, unbonding, unbonding slashing} txs are valid - ensure that the unbonding tx is spending the staking tx - ensure that the unbonding tx and unbonding slashing tx are consistent --- contracts/btc-staking/src/error.rs | 2 + contracts/btc-staking/src/validation/mod.rs | 192 ++++++++++++++++++-- packages/btcstaking/src/error.rs | 2 + packages/btcstaking/src/lib.rs | 2 +- packages/btcstaking/src/scripts_utils.rs | 2 + packages/btcstaking/src/sig_verify.rs | 13 +- 6 files changed, 196 insertions(+), 17 deletions(-) diff --git a/contracts/btc-staking/src/error.rs b/contracts/btc-staking/src/error.rs index 9a744c77..66293217 100644 --- a/contracts/btc-staking/src/error.rs +++ b/contracts/btc-staking/src/error.rs @@ -52,6 +52,8 @@ pub enum ContractError { DelegationAlreadyExists(String), #[error("BTC delegation is not active: {0}")] DelegationIsNotActive(String), + #[error("Invalid covenant signature: {0}")] + InvalidCovenantSig(String), #[error("Invalid Btc tx: {0}")] InvalidBtcTx(String), #[error("Empty signature from the delegator")] diff --git a/contracts/btc-staking/src/validation/mod.rs b/contracts/btc-staking/src/validation/mod.rs index 67545176..757034dd 100644 --- a/contracts/btc-staking/src/validation/mod.rs +++ b/contracts/btc-staking/src/validation/mod.rs @@ -1,6 +1,8 @@ use crate::state::config::Params; use crate::{error::ContractError, state::staking::BtcDelegation}; use babylon_apis::btc_staking_api::{ActiveBtcDelegation, NewFinalityProvider}; +use babylon_btcstaking::adaptor_sig::AdaptorSignature; +use babylon_btcstaking::sig_verify::enc_verify_transaction_sig_with_output; use bitcoin::Transaction; use cosmwasm_std::Binary; @@ -202,8 +204,7 @@ pub fn verify_active_delegation( let staker_sig = k256::schnorr::Signature::try_from(active_delegation.delegator_slashing_sig.as_slice()) .map_err(|e| ContractError::SecP256K1Error(e.to_string()))?; - - // Verify the signature + // Verify the staker's signature babylon_btcstaking::sig_verify::verify_transaction_sig_with_output( &slashing_tx, staking_output, @@ -213,7 +214,41 @@ pub fn verify_active_delegation( ) .map_err(|e| ContractError::SecP256K1Error(e.to_string()))?; - // TODO: verify covenant signatures + /* + Verify covenant signatures over slashing tx + */ + for cov_sig in active_delegation.covenant_sigs.iter() { + let cov_pk = VerifyingKey::from_bytes(&cov_sig.cov_pk) + .map_err(|e| ContractError::SecP256K1Error(e.to_string()))?; + // Check if the covenant public key is in the params.covenant_pks + if !params + .covenant_pks + .contains(&hex::encode(cov_sig.cov_pk.as_slice())) + { + return Err(ContractError::InvalidCovenantSig( + "Covenant public key not found in params".to_string(), + )); + } + let sigs = cov_sig + .adaptor_sigs + .iter() + .map(|sig| { + AdaptorSignature::new(sig.as_slice()) + .map_err(|e| ContractError::SecP256K1Error(e.to_string())) + }) + .collect::, ContractError>>()?; + for (idx, sig) in sigs.iter().enumerate() { + enc_verify_transaction_sig_with_output( + &slashing_tx, + staking_output, + slashing_path_script.as_script(), + &cov_pk, + &fp_pks[idx], + &sig, + ) + .map_err(|e| ContractError::SecP256K1Error(e.to_string()))?; + } + } // TODO: Check unbonding time (staking time from unbonding tx) is larger than min unbonding time // which is larger value from: @@ -225,28 +260,155 @@ pub fn verify_active_delegation( // - is smaller than math.MaxUint16 (due to check in req.ValidateBasic()) /* - TODO: Early unbonding logic + Early unbonding logic */ - // TODO: Deserialize provided transactions + // decode unbonding tx + let unbonding_tx = &active_delegation.undelegation_info.unbonding_tx; + let unbonding_tx: Transaction = deserialize(unbonding_tx) + .map_err(|_| ContractError::InvalidBtcTx(unbonding_tx.encode_hex()))?; + // decode unbonding slashing tx + let unbonding_slashing_tx = &active_delegation.undelegation_info.slashing_tx; + let unbonding_slashing_tx: Transaction = deserialize(unbonding_slashing_tx) + .map_err(|_| ContractError::InvalidBtcTx(unbonding_slashing_tx.encode_hex()))?; + + // Check that the unbonding tx input is pointing to staking tx + if unbonding_tx.input[0].previous_output.txid != staking_tx.txid() + || unbonding_tx.input[0].previous_output.vout != active_delegation.staking_output_idx + { + return Err(ContractError::InvalidBtcTx( + "Unbonding transaction must spend staking output".to_string(), + )); + } - // TODO: Check that the unbonding tx input is pointing to staking tx + // TODO: Check unbonding tx fees against staking tx + // - Fee is greater than 0. + // - Unbonding output value is at least `MinUnbondingValue` percentage of staking output value. - // TODO: Check that staking tx output index matches unbonding tx output index + let babylon_unbonding_script_paths = + babylon_btcstaking::scripts_utils::BabylonScriptPaths::new( + &staker_pk, + &fp_pks, + &cov_pks, + params.covenant_quorum as usize, + staking_time, + )?; - // TODO: Build unbonding info + // TODO: Ensure the unbonding tx has valid unbonding output, and + // get the unbonding output index + let unbonding_output_idx = 0; + let unbonding_output = &unbonding_tx.output[unbonding_output_idx as usize]; - // TODO: Get unbonding output index + let unbonding_time = active_delegation.unbonding_time as u16; - // TODO: Check that slashing tx and unbonding tx are valid and consistent + // Check that unbonding tx and unbonding slashing tx are consistent + babylon_btcstaking::tx_verify::check_transactions( + &unbonding_slashing_tx, + &unbonding_tx, + unbonding_output_idx, + params.min_slashing_tx_fee_sat, + slashing_rate, + &slashing_address, + &staker_pk, + unbonding_time, + )?; - // TODO: Check staker signature against slashing path of the unbonding tx + /* + Check staker signature against slashing path of the unbonding tx + */ + // get unbonding slashing path script + let unbonding_slashing_path_script = babylon_unbonding_script_paths.slashing_path_script; + // get the staker's signature on the unbonding slashing tx + let unbonding_slashing_sig = active_delegation + .undelegation_info + .delegator_slashing_sig + .as_slice(); + let unbonding_slashing_sig = k256::schnorr::Signature::try_from(unbonding_slashing_sig) + .map_err(|e| ContractError::SecP256K1Error(e.to_string()))?; + // Verify the staker's signature + babylon_btcstaking::sig_verify::verify_transaction_sig_with_output( + &unbonding_slashing_tx, + &unbonding_tx.output[unbonding_output_idx as usize], + unbonding_slashing_path_script.as_script(), + &staker_pk, + &unbonding_slashing_sig, + ) + .map_err(|e| ContractError::SecP256K1Error(e.to_string()))?; - // TODO: Verify covenant signatures over unbonding slashing tx + /* + verify covenant signatures over unbonding tx + */ + let unbonding_path_script = babylon_script_paths.unbonding_path_script; + for cov_sig in active_delegation + .undelegation_info + .covenant_unbonding_sig_list + .iter() + { + // get covenant public key + let cov_pk = VerifyingKey::from_bytes(&cov_sig.pk) + .map_err(|e| ContractError::SecP256K1Error(e.to_string()))?; + // ensure covenant public key is in the params + if !params + .covenant_pks + .contains(&hex::encode(cov_pk.to_bytes())) + { + return Err(ContractError::InvalidCovenantSig( + "Covenant public key not found in params".to_string(), + )); + } + // get covenant signature + let sig = Signature::try_from(cov_sig.sig.as_slice()) + .map_err(|e| ContractError::SecP256K1Error(e.to_string()))?; + // Verify the covenant member's signature + babylon_btcstaking::sig_verify::verify_transaction_sig_with_output( + &staking_tx, + &staking_output, + unbonding_path_script.as_script(), + &cov_pk, + &sig, + ) + .map_err(|e| ContractError::SecP256K1Error(e.to_string()))?; + } - // TODO: Check unbonding tx fees against staking tx - // - Fee is greater than 0. - // - Unbonding output value is at least `MinUnbondingValue` percentage of staking output value. + /* + Verify covenant signatures over unbonding slashing tx + */ + for cov_sig in active_delegation + .undelegation_info + .covenant_slashing_sigs + .iter() + { + let cov_pk = VerifyingKey::from_bytes(&cov_sig.cov_pk) + .map_err(|e| ContractError::SecP256K1Error(e.to_string()))?; + // Check if the covenant public key is in the params.covenant_pks + if !params + .covenant_pks + .contains(&hex::encode(cov_sig.cov_pk.as_slice())) + { + return Err(ContractError::InvalidCovenantSig( + "Covenant public key not found in params".to_string(), + )); + } + let sigs = cov_sig + .adaptor_sigs + .iter() + .map(|sig| { + AdaptorSignature::new(sig.as_slice()) + .map_err(|e| ContractError::SecP256K1Error(e.to_string())) + }) + .collect::, ContractError>>()?; + for (idx, sig) in sigs.iter().enumerate() { + enc_verify_transaction_sig_with_output( + &unbonding_slashing_tx, + unbonding_output, + &unbonding_slashing_path_script.as_script(), + &cov_pk, + &fp_pks[idx], + &sig, + ) + .map_err(|e| ContractError::SecP256K1Error(e.to_string()))?; + } + } } // make static analyser happy with unused parameters diff --git a/packages/btcstaking/src/error.rs b/packages/btcstaking/src/error.rs index 7bef9e94..94221a98 100644 --- a/packages/btcstaking/src/error.rs +++ b/packages/btcstaking/src/error.rs @@ -34,6 +34,8 @@ pub enum Error { TxInputCountMismatch(usize, usize), #[error("Tx output count mismatch: expected {0}, got {1}")] TxOutputCountMismatch(usize, usize), + #[error("Tx output index not found")] + TxOutputIndexNotFound {}, #[error("Invalid schnorr signature: {0}")] InvalidSchnorrSignature(String), #[error("Transaction is replaceable.")] diff --git a/packages/btcstaking/src/lib.rs b/packages/btcstaking/src/lib.rs index eb9026e4..947aa6f5 100644 --- a/packages/btcstaking/src/lib.rs +++ b/packages/btcstaking/src/lib.rs @@ -1,4 +1,4 @@ -mod adaptor_sig; +pub mod adaptor_sig; pub mod error; pub mod scripts_utils; pub mod sig_verify; diff --git a/packages/btcstaking/src/scripts_utils.rs b/packages/btcstaking/src/scripts_utils.rs index 8f467b6b..182f6529 100644 --- a/packages/btcstaking/src/scripts_utils.rs +++ b/packages/btcstaking/src/scripts_utils.rs @@ -236,6 +236,8 @@ impl BabylonScriptPaths { slashing_path_script, }) } + + // TODO: implement a function for aggregating all scripts to a single ScriptBuf } #[cfg(test)] diff --git a/packages/btcstaking/src/sig_verify.rs b/packages/btcstaking/src/sig_verify.rs index 15609639..584aed61 100644 --- a/packages/btcstaking/src/sig_verify.rs +++ b/packages/btcstaking/src/sig_verify.rs @@ -4,8 +4,8 @@ use crate::Result; use babylon_bitcoin::schnorr; use bitcoin::hashes::Hash; use bitcoin::sighash::{Prevouts, SighashCache}; -use bitcoin::Transaction; use bitcoin::{Script, TxOut}; +use bitcoin::{ScriptBuf, Transaction}; use k256::schnorr::Signature as SchnorrSignature; use k256::schnorr::VerifyingKey; @@ -37,6 +37,17 @@ fn calc_sighash( Ok(sighash.to_raw_hash().to_byte_array()) } +pub fn get_output_idx(tx: &Transaction, pk_script: ScriptBuf) -> Result { + let output_idx = tx + .output + .iter() + .position(|output| output.script_pubkey == *pk_script); + match output_idx { + Some(idx) => Ok(idx as u32), + None => Err(Error::TxOutputIndexNotFound {}), + } +} + /// verify_transaction_sig_with_output verifies the validity of a Schnorr signature for a given transaction pub fn verify_transaction_sig_with_output( transaction: &Transaction,