Skip to content

Commit

Permalink
btcstaking: full validation of unbonding/slashing BTC delegation (#60)
Browse files Browse the repository at this point in the history
Part of #7 

This PR implements the full validation of unbonded/slashed BTC
delegations.
  • Loading branch information
SebastianElvis authored Sep 16, 2024
1 parent ffd40bb commit d53c5f6
Show file tree
Hide file tree
Showing 41 changed files with 399 additions and 191 deletions.
16 changes: 15 additions & 1 deletion contracts/btc-staking/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,11 @@ pub(crate) mod tests {
};
use cw_controllers::AdminResponse;
use hex::ToHex;
use test_utils::{get_btc_delegation, get_finality_provider, get_pub_rand_commit};
use k256::schnorr::{Signature, SigningKey};
use test_utils::{
get_btc_del_unbonding_sig_bytes, get_btc_delegation, get_finality_provider,
get_fp_sk_bytes, get_pub_rand_commit,
};

pub(crate) const CREATOR: &str = "creator";
pub(crate) const INIT_ADMIN: &str = "initial_admin";
Expand Down Expand Up @@ -402,6 +406,11 @@ pub(crate) mod tests {
new_active_btc_delegation(del)
}

pub(crate) fn get_btc_del_unbonding_sig(del_id: i32, fp_ids: &[i32]) -> Signature {
let sig_bytes = get_btc_del_unbonding_sig_bytes(del_id, fp_ids.to_vec());
Signature::try_from(sig_bytes.as_slice()).unwrap()
}

/// Get public randomness public key, commitment, and signature information
///
/// Signature is a Schnorr signature over the commitment
Expand All @@ -423,6 +432,11 @@ pub(crate) mod tests {
new_finality_provider(fp)
}

pub(crate) fn create_new_fp_sk(id: i32) -> SigningKey {
let fp_sk_bytes = get_fp_sk_bytes(id);
SigningKey::from_bytes(&fp_sk_bytes).unwrap()
}

#[test]
fn instantiate_without_admin() {
let mut deps = mock_dependencies();
Expand Down
2 changes: 2 additions & 0 deletions contracts/btc-staking/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ pub enum ContractError {
FinalityProviderNotFound(String),
#[error("Staking tx hash already exists: {0}")]
DelegationAlreadyExists(String),
#[error("BTC delegation is not active: {0}")]
DelegationIsNotActive(String),
#[error("Invalid Btc tx: {0}")]
InvalidBtcTx(String),
#[error("Empty signature from the delegator")]
Expand Down
12 changes: 8 additions & 4 deletions contracts/btc-staking/src/queries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,11 +242,13 @@ mod tests {
use cosmwasm_std::testing::message_info;
use cosmwasm_std::testing::{mock_dependencies, mock_env};
use cosmwasm_std::StdError::NotFound;
use cosmwasm_std::{from_json, Binary, Storage};
use cosmwasm_std::{from_json, Storage};

use babylon_apis::btc_staking_api::{FinalityProvider, UnbondedBtcDelegation};

use crate::contract::tests::{create_new_finality_provider, get_params};
use crate::contract::tests::{
create_new_finality_provider, get_btc_del_unbonding_sig, get_params,
};
use crate::contract::{execute, instantiate};
use crate::error::ContractError;
use crate::finality::tests::mock_env_height;
Expand Down Expand Up @@ -457,13 +459,14 @@ mod tests {
// Unbond the second delegation
// Compute staking tx hash
let staking_tx_hash_hex = staking_tx_hash(&del2.into()).to_string();
let unbonding_sig = crate::contract::tests::get_btc_del_unbonding_sig(2, &[1]);
let msg = ExecuteMsg::BtcStaking {
new_fp: vec![],
active_del: vec![],
slashed_del: vec![],
unbonded_del: vec![UnbondedBtcDelegation {
staking_tx_hash: staking_tx_hash_hex,
unbonding_tx_sig: Binary::new(vec![0x01, 0x02, 0x03]),
unbonding_tx_sig: unbonding_sig.to_bytes().into(),
}],
};
execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap();
Expand Down Expand Up @@ -613,13 +616,14 @@ mod tests {
// Unbond the first delegation
// Compute staking tx hash
let staking_tx_hash_hex = staking_tx_hash(&del1.into()).to_string();
let unbonding_sig = get_btc_del_unbonding_sig(1, &[1]);
let msg = ExecuteMsg::BtcStaking {
new_fp: vec![],
active_del: vec![],
slashed_del: vec![],
unbonded_del: vec![UnbondedBtcDelegation {
staking_tx_hash: staking_tx_hash_hex,
unbonding_tx_sig: Binary::new(vec![0x01, 0x02, 0x03]),
unbonding_tx_sig: unbonding_sig.to_bytes().into(),
}],
};
execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap();
Expand Down
68 changes: 42 additions & 26 deletions contracts/btc-staking/src/staking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ use crate::state::staking::{
FP_DELEGATIONS, FP_SET, TOTAL_POWER,
};
use crate::state::BTC_HEIGHT;
use crate::validation::{verify_active_delegation, verify_new_fp};
use crate::validation::{
verify_active_delegation, verify_new_fp, verify_slashed_delegation, verify_undelegation,
};
use babylon_apis::btc_staking_api::{
ActiveBtcDelegation, FinalityProvider, NewFinalityProvider, SlashedBtcDelegation,
UnbondedBtcDelegation,
Expand Down Expand Up @@ -79,10 +81,10 @@ pub fn handle_new_fp(
new_fp.btc_pk_hex.clone(),
));
}
// validate the finality provider data
// basic validations on the finality provider data
new_fp.validate()?;

// verify the finality provider registration request
// verify the finality provider registration request (full or lite)
verify_new_fp(new_fp)?;

// get DB object
Expand Down Expand Up @@ -147,7 +149,7 @@ pub fn handle_active_delegation(
));
}

// full validations on the active delegation
// verify the active delegation (full or lite)
verify_active_delegation(&params, active_delegation, &staking_tx)?;

// All good, construct BTCDelegation and insert BTC delegation
Expand Down Expand Up @@ -224,9 +226,16 @@ fn handle_undelegation(
let staking_tx_hash = Txid::from_str(&undelegation.staking_tx_hash)?;
let mut btc_del = DELEGATIONS.load(storage, staking_tx_hash.as_ref())?;

// TODO: Ensure the BTC delegation is active
// Ensure the BTC delegation is active
if !btc_del.is_active() {
return Err(ContractError::DelegationIsNotActive(
staking_tx_hash.to_string(),
));
}

// TODO: Verify the signature on the unbonding tx is from the delegator
// verify the early unbonded delegation (full or lite)
let params = PARAMS.load(storage)?;
verify_undelegation(&params, &btc_del, &undelegation.unbonding_tx_sig)?;

// Add the signature to the BTC delegation's undelegation and set back
btc_undelegate(
Expand Down Expand Up @@ -268,7 +277,16 @@ fn handle_slashed_delegation(
let staking_tx_hash = Txid::from_str(&delegation.staking_tx_hash)?;
let mut btc_del = DELEGATIONS.load(storage, staking_tx_hash.as_ref())?;

// TODO: Ensure the BTC delegation is active
// Ensure the BTC delegation is active
if !btc_del.is_active() {
return Err(ContractError::DelegationIsNotActive(
staking_tx_hash.to_string(),
));
}

// verify the slashed delegation (full or lite)
let recovered_fp_sk_hex = delegation.recovered_fp_btc_sk.clone();
verify_slashed_delegation(&btc_del, &recovered_fp_sk_hex)?;

// Discount the voting power from the affected finality providers
let affected_fps = DELEGATION_FPS.load(storage, staking_tx_hash.as_ref())?;
Expand Down Expand Up @@ -402,12 +420,12 @@ pub(crate) fn slash_finality_provider(
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use cosmwasm_std::Binary;

use cosmwasm_std::testing::{message_info, mock_dependencies, mock_env};

use crate::contract::tests::{
create_new_finality_provider, get_active_btc_delegation, get_params, CREATOR, INIT_ADMIN,
create_new_finality_provider, create_new_fp_sk, get_active_btc_delegation,
get_btc_del_unbonding_sig, get_derived_btc_delegation, get_params, CREATOR, INIT_ADMIN,
};
use crate::contract::{execute, instantiate};
use crate::msg::{ExecuteMsg, InstantiateMsg};
Expand Down Expand Up @@ -582,14 +600,11 @@ pub(crate) mod tests {
let params = get_params();
PARAMS.save(deps.as_mut().storage, &params).unwrap();

// Build valid active delegation
let active_delegation = get_active_btc_delegation();

// Register one FP first
let mut new_fp = create_new_finality_provider(1);
new_fp
.btc_pk_hex
.clone_from(&active_delegation.fp_btc_pk_list[0]);
let new_fp = create_new_finality_provider(1);

// Build valid active delegation
let active_delegation = get_derived_btc_delegation(1, &[1]);

let msg = ExecuteMsg::BtcStaking {
new_fp: vec![new_fp.clone()],
Expand Down Expand Up @@ -623,10 +638,12 @@ pub(crate) mod tests {
}
);

let unbonding_sig = get_btc_del_unbonding_sig(1, &[1]);

// Now send the undelegation message
let undelegation = UnbondedBtcDelegation {
staking_tx_hash: staking_tx_hash_hex.clone(),
unbonding_tx_sig: Binary::new(vec![0x01, 0x02, 0x03]), // TODO: Use a proper signature
unbonding_tx_sig: unbonding_sig.to_bytes().into(),
};

let msg = ExecuteMsg::BtcStaking {
Expand All @@ -648,7 +665,7 @@ pub(crate) mod tests {
BtcUndelegationInfo {
unbonding_tx: active_delegation_undelegation.unbonding_tx.into(),
slashing_tx: active_delegation_undelegation.slashing_tx.into(),
delegator_unbonding_sig: vec![0x01, 0x02, 0x03],
delegator_unbonding_sig: unbonding_sig.to_bytes().into(),
delegator_slashing_sig: active_delegation_undelegation
.delegator_slashing_sig
.into(),
Expand Down Expand Up @@ -682,14 +699,11 @@ pub(crate) mod tests {
let params = get_params();
PARAMS.save(deps.as_mut().storage, &params).unwrap();

// Build valid active delegation
let active_delegation = get_active_btc_delegation();

// Register one FP first
let mut new_fp = create_new_finality_provider(1);
new_fp
.btc_pk_hex
.clone_from(&active_delegation.fp_btc_pk_list[0]);
let new_fp = create_new_finality_provider(1);

// Build valid active delegation
let active_delegation = get_derived_btc_delegation(1, &[1]);

let msg = ExecuteMsg::BtcStaking {
new_fp: vec![new_fp.clone()],
Expand All @@ -716,9 +730,11 @@ pub(crate) mod tests {
assert_eq!(fp.power, btc_del.total_sat);

// Now send the slashed delegation message
let fp_sk = create_new_fp_sk(1);
let fp_sk_hex = hex::encode(fp_sk.to_bytes());
let slashed = SlashedBtcDelegation {
staking_tx_hash: staking_tx_hash_hex.clone(),
recovered_fp_btc_sk: "deadbeef".to_string(), // Currently unused
recovered_fp_btc_sk: fp_sk_hex,
};

let msg = ExecuteMsg::BtcStaking {
Expand Down
Loading

0 comments on commit d53c5f6

Please sign in to comment.