Skip to content

Commit

Permalink
staking: verifying staker's signature over slashing tx (#56)
Browse files Browse the repository at this point in the history
Part of #7 

This PR adds verifications on staker's signature over the slashing tx in
BTC delegation requests. This includes:

- replacing rust secp256k1 with k256 for verifying Schnorr signatures
and key pairs. This prevents bloating wasm binary size
- adding an assertion ensuring the full validation version of btc
staking contract is less than 1 MB. This checks whether rust-bitcoin
stuff bloats the contract size or not.
- verifying staker's Schnorr signature over its slashing tx in
`handle_btc_delegation` (the version with `full-validation` feature)
- fixing `datagen` to use 1st FP's secret key for generating pub rand
commit and finality sig
  • Loading branch information
SebastianElvis authored Sep 9, 2024
1 parent 187839b commit 21e3fc1
Show file tree
Hide file tree
Showing 43 changed files with 439 additions and 242 deletions.
6 changes: 6 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ cw-multi-test = "2.0.1"
cw-storage-plus = "2.0.0"
cw-utils = "2.0.0"
derivative = "2"
digest = "0.10"
hex = "0.4.3"
ics23 = { version = "0.9.0", default-features = false, features = [
"host-functions",
Expand Down
13 changes: 7 additions & 6 deletions contracts/babylon/tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,15 @@ use babylon_contract::ibc::IBC_VERSION;
use babylon_contract::msg::btc_header::{BtcHeader, BtcHeadersResponse};
use babylon_contract::msg::contract::{ExecuteMsg, InstantiateMsg};

static WASM: &[u8] = include_bytes!("../../../artifacts/babylon_contract.wasm");
static BABYLON_CONTRACT_WASM: &[u8] = include_bytes!("../../../artifacts/babylon_contract.wasm");
/// Wasm size limit: https://github.com/CosmWasm/wasmd/blob/main/x/wasm/types/validation.go#L24-L25
const MAX_WASM_SIZE: usize = 800 * 1024; // 800 KB

const CREATOR: &str = "creator";

#[track_caller]
fn setup() -> Instance<MockApi, MockStorage, MockQuerier> {
let mut deps = mock_instance_with_gas_limit(WASM, 2_250_000_000_000);
let mut deps = mock_instance_with_gas_limit(BABYLON_CONTRACT_WASM, 2_250_000_000_000);
let msg = InstantiateMsg {
network: babylon_bitcoin::chain_params::Network::Regtest,
babylon_tag: "01020304".to_string(),
Expand Down Expand Up @@ -80,15 +80,16 @@ fn get_fork_msg_test_headers() -> Vec<BtcHeader> {
#[test]
fn wasm_size_limit_check() {
assert!(
WASM.len() < MAX_WASM_SIZE,
"Wasm file too large: {}",
WASM.len()
BABYLON_CONTRACT_WASM.len() < MAX_WASM_SIZE,
"Babylon contract wasm binary is too large: {} (target: {})",
BABYLON_CONTRACT_WASM.len(),
MAX_WASM_SIZE
);
}

#[test]
fn instantiate_works() {
let mut deps = mock_instance(WASM, &[]);
let mut deps = mock_instance(BABYLON_CONTRACT_WASM, &[]);

let msg = InstantiateMsg {
network: babylon_bitcoin::chain_params::Network::Regtest,
Expand Down
2 changes: 2 additions & 0 deletions contracts/btc-staking/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,8 @@ pub(crate) mod tests {

fn new_params(params: ProtoParams) -> Params {
Params {
covenant_pks: params.covenant_pks.iter().map(hex::encode).collect(),
covenant_quorum: params.covenant_quorum,
btc_network: Network::Regtest, // TODO: fix this
max_active_finality_providers: params.max_active_finality_providers,
min_pub_rand: 10, // TODO: fix this
Expand Down
32 changes: 20 additions & 12 deletions contracts/btc-staking/src/finality.rs
Original file line number Diff line number Diff line change
Expand Up @@ -589,9 +589,11 @@ pub(crate) mod tests {
// Read public randomness commitment test data
let (pk_hex, pub_rand, pubrand_signature) = get_public_randomness_commitment();

// Register one FP with a valid pubkey first
let mut new_fp = create_new_finality_provider(1);
new_fp.btc_pk_hex.clone_from(&pk_hex);
// Register one FP
// NOTE: the test data ensures that pub rand commit / finality sig are
// signed by the 1st FP
let new_fp = create_new_finality_provider(1);
assert_eq!(new_fp.btc_pk_hex, pk_hex);

let msg = ExecuteMsg::BtcStaking {
new_fp: vec![new_fp.clone()],
Expand Down Expand Up @@ -643,9 +645,11 @@ pub(crate) mod tests {
)
.unwrap();

// Register one FP with a valid pubkey first
let mut new_fp = create_new_finality_provider(1);
new_fp.btc_pk_hex.clone_from(&pk_hex);
// Register one FP
// NOTE: the test data ensures that pub rand commit / finality sig are
// signed by the 1st FP
let new_fp = create_new_finality_provider(1);
assert_eq!(new_fp.btc_pk_hex, pk_hex);

let msg = ExecuteMsg::BtcStaking {
new_fp: vec![new_fp.clone()],
Expand Down Expand Up @@ -781,9 +785,11 @@ pub(crate) mod tests {
)
.unwrap();

// Register one FP with a valid pubkey first
let mut new_fp = create_new_finality_provider(1);
new_fp.btc_pk_hex.clone_from(&pk_hex);
// Register one FP
// NOTE: the test data ensures that pub rand commit / finality sig are
// signed by the 1st FP
let new_fp = create_new_finality_provider(1);
assert_eq!(new_fp.btc_pk_hex, pk_hex);

let msg = ExecuteMsg::BtcStaking {
new_fp: vec![new_fp.clone()],
Expand Down Expand Up @@ -942,9 +948,11 @@ pub(crate) mod tests {
)
.unwrap();

// Register one FP with a valid pubkey first
let mut new_fp = create_new_finality_provider(1);
new_fp.btc_pk_hex.clone_from(&pk_hex);
// Register one FP
// NOTE: the test data ensures that pub rand commit / finality sig are
// signed by the 1st FP
let new_fp = create_new_finality_provider(1);
assert_eq!(new_fp.btc_pk_hex, pk_hex);

let msg = ExecuteMsg::BtcStaking {
new_fp: vec![new_fp.clone()],
Expand Down
87 changes: 75 additions & 12 deletions contracts/btc-staking/src/staking.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use hex::ToHex;
use std::str::FromStr;

use bitcoin::absolute::LockTime;
use bitcoin::consensus::deserialize;
use bitcoin::hashes::Hash;
use bitcoin::{Transaction, Txid};
use cosmwasm_std::{DepsMut, Env, Event, MessageInfo, Order, Response, Storage};
use hex::ToHex;

use std::str::FromStr;

use crate::error::ContractError;
use crate::msg::FinalityProviderInfo;
Expand All @@ -24,8 +24,7 @@ use babylon_bindings::BabylonMsg;

#[cfg(feature = "full-validation")]
use bitcoin::Address;
#[cfg(feature = "full-validation")]
use bitcoin::XOnlyPublicKey;
use k256::schnorr::VerifyingKey;

/// handle_btc_staking handles the BTC staking operations
pub fn handle_btc_staking(
Expand Down Expand Up @@ -86,6 +85,9 @@ pub fn handle_new_fp(
new_fp.validate()?;
// get DB object
let fp = FinalityProvider::from(new_fp);

// TODO: Verify proof of possession

// save to DB
FPS.save(storage, &fp.btc_pk_hex, &fp)?;
// Set its voting power to zero
Expand All @@ -106,6 +108,36 @@ fn verify_active_delegation(
active_delegation: &ActiveBtcDelegation,
staking_tx: &Transaction,
) -> Result<(), ContractError> {
// get staker's public key

let staker_pk_bytes = hex::decode(&active_delegation.btc_pk_hex)
.map_err(|e| ContractError::SecP256K1Error(e.to_string()))?;
let staker_pk = VerifyingKey::from_bytes(&staker_pk_bytes)
.map_err(|e| ContractError::SecP256K1Error(e.to_string()))?;

// get all FP's public keys
let fp_pks: Vec<VerifyingKey> = active_delegation
.fp_btc_pk_list
.iter()
.map(|pk_hex| {
let pk_bytes =
hex::decode(pk_hex).map_err(|e| ContractError::SecP256K1Error(e.to_string()))?;
VerifyingKey::from_bytes(&pk_bytes)
.map_err(|e| ContractError::SecP256K1Error(e.to_string()))
})
.collect::<Result<Vec<VerifyingKey>, ContractError>>()?;
// get all covenant members' public keys
let cov_pks: Vec<VerifyingKey> = params
.covenant_pks
.iter()
.map(|pk_hex| {
let pk_bytes =
hex::decode(pk_hex).map_err(|e| ContractError::SecP256K1Error(e.to_string()))?;
VerifyingKey::from_bytes(&pk_bytes)
.map_err(|e| ContractError::SecP256K1Error(e.to_string()))
})
.collect::<Result<Vec<VerifyingKey>, ContractError>>()?;

// Check if data provided in request, matches data to which staking tx is
// committed

Expand All @@ -128,8 +160,6 @@ fn verify_active_delegation(
.assume_checked();

// Check slashing tx and staking tx are valid and consistent
let staker_btc_pk = XOnlyPublicKey::from_str(&active_delegation.btc_pk_hex)
.map_err(|e| ContractError::SecP256K1Error(e.to_string()))?;
let slashing_rate = params
.slashing_rate
.parse::<f64>()
Expand All @@ -141,10 +171,45 @@ fn verify_active_delegation(
params.min_slashing_tx_fee_sat,
slashing_rate,
&slashing_address,
&staker_btc_pk,
&staker_pk,
active_delegation.unbonding_time as u16,
)?;

// TODO: Verify proof of possession

/*
verify staker signature against slashing path of the staking tx script
*/

// get the slashing path script
let staking_output = &staking_tx.output[active_delegation.staking_output_idx as usize];
let staking_time = (active_delegation.end_height - active_delegation.start_height) as u16;
let babylon_script_paths = babylon_btcstaking::scripts_utils::BabylonScriptPaths::new(
&staker_pk,
&fp_pks,
&cov_pks,
params.covenant_quorum as usize,
staking_time,
)?;
let slashing_path_script = babylon_script_paths.slashing_path_script;

// get the staker's signature on the slashing tx
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
babylon_btcstaking::sig_verify::verify_transaction_sig_with_output(
&slashing_tx,
staking_output,
slashing_path_script.as_script(),
&staker_pk,
&staker_sig,
)
.map_err(|e| ContractError::SecP256K1Error(e.to_string()))?;

// TODO: verify covenant signatures

// TODO: Check unbonding time (staking time from unbonding tx) is larger than min unbonding time
// which is larger value from:
// - MinUnbondingTime
Expand All @@ -154,10 +219,6 @@ fn verify_active_delegation(
// - is larger than min unbonding time
// - is smaller than math.MaxUint16 (due to check in req.ValidateBasic())

// TODO: Verify proof of possession

// TODO: Verify staker signature against slashing path of the staking tx script

/*
TODO: Early unbonding logic
*/
Expand All @@ -176,6 +237,8 @@ fn verify_active_delegation(

// TODO: Check staker signature against slashing path of the unbonding tx

// TODO: Verify covenant signatures over unbonding slashing 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.
Expand Down
6 changes: 6 additions & 0 deletions contracts/btc-staking/src/state/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,13 @@ pub struct Config {
#[derive(Derivative)]
#[derivative(Default)]
pub struct Params {
// covenant_pks is the list of public keys held by the covenant committee each PK
// follows encoding in BIP-340 spec on Bitcoin
pub covenant_pks: Vec<String>,
// covenant_quorum is the minimum number of signatures needed for the covenant multi-signature
pub covenant_quorum: u32,
#[derivative(Default(value = "Network::Regtest"))]
// ntc_network is the network the BTC staking protocol is running on
pub btc_network: Network,
// `min_commission_rate` is the chain-wide minimum commission rate that a finality provider
// can charge their delegators
Expand Down
17 changes: 15 additions & 2 deletions contracts/btc-staking/tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,31 @@ use cosmwasm_vm::testing::{instantiate, mock_env, mock_info, mock_instance};

use btc_staking::msg::InstantiateMsg;

// wasm binary lite version
static WASM: &[u8] = include_bytes!("../../../artifacts/btc_staking.wasm");
/// Wasm size limit: https://github.com/CosmWasm/wasmd/blob/main/x/wasm/types/validation.go#L24-L25
const MAX_WASM_SIZE: usize = 800 * 1024; // 800 KB

// wasm binary with full validation
// TODO: optimise to 800 KB
static WASM_FULL: &[u8] = include_bytes!("../../../artifacts/btc_staking-full-validation.wasm");
const MAX_WASM_SIZE_FULL: usize = 1024 * 1024; // 1 MB

const CREATOR: &str = "creator";

#[test]
fn wasm_size_limit_check() {
assert!(
WASM.len() < MAX_WASM_SIZE,
"Wasm file too large: {}",
WASM.len()
"BTC staking contract (lite version) wasm binary is too large: {} (target: {})",
WASM.len(),
MAX_WASM_SIZE
);
assert!(
WASM_FULL.len() < MAX_WASM_SIZE_FULL,
"BTC staking contract (with full validation) wasm binary is too large: {} (target: {})",
WASM_FULL.len(),
MAX_WASM_SIZE_FULL
);
}

Expand Down
17 changes: 16 additions & 1 deletion datagen/utils/btcstaking.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ const (
BTCSTAKING_PARAMS_FILENAME = "btcstaking_params.dat"
)

var (
fpSK *btcec.PrivateKey
fpPK *btcec.PublicKey
)

func GenParams(dir string) ([]*btcec.PrivateKey, uint32) {
t := &testing.T{}

Expand Down Expand Up @@ -63,7 +68,17 @@ func GenFinalityProviders(dir string, numFPs int) {
t := &testing.T{}

for i := 1; i <= numFPs; i++ {
fp, err := datagen.GenRandomFinalityProvider(r)
fpBTCSK, fpBTCPK, err := datagen.GenRandomBTCKeyPair(r)
require.NoError(t, err)

// set the first FP's BTC key pair as the global BTC key pair
// they will be used for generating public randomness and finality signatures
if i == 1 {
fpSK = fpBTCSK
fpPK = fpBTCPK
}

fp, err := datagen.GenRandomFinalityProviderWithBTCSK(r, fpBTCSK, "")
require.NoError(t, err)
fp.ConsumerId = fmt.Sprintf("consumer-%d", i)
fpBytes, err := fp.Marshal()
Expand Down
1 change: 0 additions & 1 deletion datagen/utils/finality.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,6 @@ func GenRandomEvidence(r *rand.Rand, sk *btcec.PrivateKey, height uint64) (*ftyp

func GenFinalityData(dir string) {
GenEOTSTestData(dir)
fpSK, _, _ := datagen.GenRandomBTCKeyPair(r)
randListInfo := GenCommitPubRandListMsg(commitPubRandHeight, commitPubRandAmount, pubRandIndex, fpSK, dir)
GenAddFinalitySig(commitPubRandHeight, pubRandIndex, randListInfo, fpSK, dir, 1)
// Conflicting signature / double signing
Expand Down
4 changes: 4 additions & 0 deletions packages/bitcoin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ edition.workspace = true

[dependencies]
bitcoin = { workspace = true }
digest = { workspace = true }
sha2 = { workspace = true }
serde = { workspace = true }
schemars = { workspace = true }
cosmwasm-std = { workspace = true }
k256 = { workspace = true }
thiserror = { workspace = true }

[dev-dependencies]
hex = { workspace = true }
9 changes: 9 additions & 0 deletions packages/bitcoin/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
use thiserror::Error;

#[derive(Error, Debug, PartialEq)]
pub enum Error {
#[error("Failed to parse public key")]
FailedToParsePublicKey(String),
#[error("Invalid schnorr signature")]
InvalidSchnorrSignature(String),
}
Loading

0 comments on commit 21e3fc1

Please sign in to comment.