Skip to content

Commit

Permalink
btcstaking: verify covenant signatures and undelegation data (#62)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
SebastianElvis authored Sep 16, 2024
1 parent d53c5f6 commit 6ab5929
Show file tree
Hide file tree
Showing 6 changed files with 196 additions and 17 deletions.
2 changes: 2 additions & 0 deletions contracts/btc-staking/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down
192 changes: 177 additions & 15 deletions contracts/btc-staking/src/validation/mod.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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,
Expand All @@ -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::<Result<Vec<AdaptorSignature>, 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:
Expand All @@ -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::<Result<Vec<AdaptorSignature>, 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
Expand Down
2 changes: 2 additions & 0 deletions packages/btcstaking/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.")]
Expand Down
2 changes: 1 addition & 1 deletion packages/btcstaking/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
mod adaptor_sig;
pub mod adaptor_sig;
pub mod error;
pub mod scripts_utils;
pub mod sig_verify;
Expand Down
2 changes: 2 additions & 0 deletions packages/btcstaking/src/scripts_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,8 @@ impl BabylonScriptPaths {
slashing_path_script,
})
}

// TODO: implement a function for aggregating all scripts to a single ScriptBuf
}

#[cfg(test)]
Expand Down
13 changes: 12 additions & 1 deletion packages/btcstaking/src/sig_verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<u32> {
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,
Expand Down

0 comments on commit 6ab5929

Please sign in to comment.