From d51889dc9be611268565ce95ca17007b9380de0d Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Wed, 7 Aug 2024 09:09:37 +0200 Subject: [PATCH 01/18] Add slashed btc delegation basic validation --- packages/apis/src/btc_staking_api.rs | 2 +- packages/apis/src/error.rs | 2 ++ packages/apis/src/validate.rs | 16 +++++++++++++++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/apis/src/btc_staking_api.rs b/packages/apis/src/btc_staking_api.rs index b9689b40..3aa8aff9 100644 --- a/packages/apis/src/btc_staking_api.rs +++ b/packages/apis/src/btc_staking_api.rs @@ -234,7 +234,7 @@ pub struct CovenantAdaptorSignatures { pub adaptor_sigs: Vec, } -/// BTCUndelegationInfo provides all necessary info about the undeleagation +/// BTCUndelegationInfo provides all necessary info about the undelegation #[cw_serde] pub struct BtcUndelegationInfo { /// unbonding_tx is the transaction which will transfer the funds from staking diff --git a/packages/apis/src/error.rs b/packages/apis/src/error.rs index ebd4570e..a2279575 100644 --- a/packages/apis/src/error.rs +++ b/packages/apis/src/error.rs @@ -15,6 +15,8 @@ pub enum StakingApiError { InvalidBtcTx(String), #[error("Empty Btc public key")] EmptyBtcPk, + #[error("Empty Btc private key")] + EmptyBtcSk, #[error("Empty proof of possession")] MissingPop, #[error("Empty chain id")] diff --git a/packages/apis/src/validate.rs b/packages/apis/src/validate.rs index 658c4de7..85083b4b 100644 --- a/packages/apis/src/validate.rs +++ b/packages/apis/src/validate.rs @@ -4,7 +4,7 @@ use cosmwasm_std::StdError; use crate::btc_staking_api::{ ActiveBtcDelegation, FinalityProviderDescription, NewFinalityProvider, ProofOfPossessionBtc, - UnbondedBtcDelegation, HASH_SIZE, + SlashedBtcDelegation, UnbondedBtcDelegation, HASH_SIZE, }; use crate::error::StakingApiError; @@ -162,3 +162,17 @@ impl Validate for UnbondedBtcDelegation { Ok(()) } } + +impl Validate for SlashedBtcDelegation { + fn validate(&self) -> Result<(), StakingApiError> { + if self.staking_tx_hash.len() != HASH_SIZE * 2 { + return Err(StakingApiError::InvalidStakingTxHash(HASH_SIZE * 2)); + } + + if self.recovered_fp_btc_sk.is_empty() { + return Err(StakingApiError::EmptyBtcSk); + } + + Ok(()) + } +} From f0acaa711d991f9637f2027f3245ce9d329edfb2 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Wed, 7 Aug 2024 09:11:32 +0200 Subject: [PATCH 02/18] Add processing of slashed delegations --- contracts/btc-staking/src/staking.rs | 78 ++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 5 deletions(-) diff --git a/contracts/btc-staking/src/staking.rs b/contracts/btc-staking/src/staking.rs index d2acb1ba..6de07a86 100644 --- a/contracts/btc-staking/src/staking.rs +++ b/contracts/btc-staking/src/staking.rs @@ -30,7 +30,7 @@ pub fn handle_btc_staking( info: &MessageInfo, new_fps: &[NewFinalityProvider], active_delegations: &[ActiveBtcDelegation], - _slashed_delegations: &[SlashedBtcDelegation], + slashed_delegations: &[SlashedBtcDelegation], unbonded_delegations: &[UnbondedBtcDelegation], ) -> Result, ContractError> { let config = CONFIG.load(deps.storage)?; @@ -47,9 +47,10 @@ pub fn handle_btc_staking( handle_active_delegation(deps.storage, env.block.height, del)?; } - // TODO: Process FPs slashing - - // TODO?: Process slashed delegations (needs routing from `babylon-contract`) + // Process slashed delegations + for del in slashed_delegations { + handle_slashed_delegation(deps.storage, env.block.height, del)?; + } // Process undelegations for undel in unbonded_delegations { @@ -262,7 +263,7 @@ pub fn handle_active_delegation( Ok(()) } -/// handle_undelegation handles undelegation from an active delegation. +/// handle_undelegation handles undelegation from an active delegation /// fn handle_undelegation( storage: &mut dyn Storage, @@ -305,6 +306,44 @@ fn handle_undelegation( Ok(()) } +/// handle_slashed_delegation handles undelegation due to slashing from an active delegation +/// +fn handle_slashed_delegation( + storage: &mut dyn Storage, + height: u64, + delegation: &SlashedBtcDelegation, +) -> Result<(), ContractError> { + // Basic stateless checks + delegation.validate()?; + + 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 + + let delegator_slashing_sig = btc_del.delegator_slashing_sig.clone(); + btc_undelegate_slashed( + storage, + &staking_tx_hash, + &mut btc_del, + delegator_slashing_sig.as_slice(), + )?; + + // Discount the voting power from the affected finality providers + let affected_fps = DELEGATION_FPS.load(storage, staking_tx_hash.as_ref())?; + let fps = fps(); + for fp in affected_fps { + fps.update(storage, &fp, height, |fp_state| { + let mut fp_state = + fp_state.ok_or(ContractError::FinalityProviderNotFound(fp.clone()))?; // should never happen + fp_state.power = fp_state.power.saturating_sub(btc_del.total_sat); + Ok::<_, ContractError>(fp_state) + })?; + } + + Ok(()) +} + /// btc_undelegate adds the signature of the unbonding tx signed by the staker to the given BTC /// delegation fn btc_undelegate( @@ -327,6 +366,35 @@ fn btc_undelegate( Ok(()) } +/// btc_undelegate_slashed adds the signature of the slashing tx signed by the staker to the given +/// BTC delegation +fn btc_undelegate_slashed( + storage: &mut dyn Storage, + staking_tx_hash: &Txid, + btc_del: &mut ActiveBtcDelegation, + slashing_tx_sig: &[u8], +) -> Result<(), ContractError> { + match &mut btc_del.undelegation_info { + Some(undelegation_info) => { + undelegation_info.delegator_slashing_sig = slashing_tx_sig.to_vec().into(); + } + None => { + return Err(ContractError::MissingUnbondingInfo); + } + } + + // Set BTC delegation back to KV store + DELEGATIONS.save(storage, staking_tx_hash.as_ref(), btc_del)?; + + // TODO? Notify subscriber about this slashed BTC delegation + // - Who are subscribers in this context? + // - How to notify them? Emit event? + + // TODO? Record event that the BTC delegation becomes unbonded due to slashing at this height + + Ok(()) +} + /// `compute_active_finality_providers` sorts all finality providers, counts the total voting /// power of top finality providers, and records them in the contract state pub fn compute_active_finality_providers( From 5d210e23123a901306a55c5357477fa556a7fff4 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Wed, 7 Aug 2024 10:15:17 +0200 Subject: [PATCH 03/18] Add slashing events --- contracts/btc-staking/src/staking.rs | 31 ++++++++++++++++++---------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/contracts/btc-staking/src/staking.rs b/contracts/btc-staking/src/staking.rs index 6de07a86..a4cfdef0 100644 --- a/contracts/btc-staking/src/staking.rs +++ b/contracts/btc-staking/src/staking.rs @@ -6,7 +6,7 @@ use bitcoin::consensus::deserialize; use bitcoin::hashes::Hash; use bitcoin::{Transaction, Txid}; -use cosmwasm_std::{DepsMut, Env, MessageInfo, Order, Response, Storage}; +use cosmwasm_std::{DepsMut, Env, Event, MessageInfo, Order, Response, Storage}; use crate::error::ContractError; use crate::msg::FinalityProviderInfo; @@ -38,28 +38,32 @@ pub fn handle_btc_staking( return Err(ContractError::Unauthorized); } + let mut res = Response::new(); + for fp in new_fps { handle_new_fp(deps.storage, fp, env.block.height)?; + // TODO: Add event } // Process active delegations for del in active_delegations { handle_active_delegation(deps.storage, env.block.height, del)?; + // TODO: Add event } // Process slashed delegations for del in slashed_delegations { - handle_slashed_delegation(deps.storage, env.block.height, del)?; + let ev = handle_slashed_delegation(deps.storage, env.block.height, del)?; + res = res.add_event(ev); } // Process undelegations for undel in unbonded_delegations { handle_undelegation(deps.storage, env.block.height, undel)?; + // TODO: Add event } - // TODO: Add events - - Ok(Response::new()) + Ok(res) } /// handle_bew_fp handles registering a new finality provider @@ -312,7 +316,7 @@ fn handle_slashed_delegation( storage: &mut dyn Storage, height: u64, delegation: &SlashedBtcDelegation, -) -> Result<(), ContractError> { +) -> Result { // Basic stateless checks delegation.validate()?; @@ -322,9 +326,10 @@ fn handle_slashed_delegation( // TODO: Ensure the BTC delegation is active let delegator_slashing_sig = btc_del.delegator_slashing_sig.clone(); - btc_undelegate_slashed( + let slashing_event = btc_undelegate_slashed( storage, &staking_tx_hash, + height, &mut btc_del, delegator_slashing_sig.as_slice(), )?; @@ -341,7 +346,7 @@ fn handle_slashed_delegation( })?; } - Ok(()) + Ok(slashing_event) } /// btc_undelegate adds the signature of the unbonding tx signed by the staker to the given BTC @@ -371,9 +376,10 @@ fn btc_undelegate( fn btc_undelegate_slashed( storage: &mut dyn Storage, staking_tx_hash: &Txid, + height: u64, btc_del: &mut ActiveBtcDelegation, slashing_tx_sig: &[u8], -) -> Result<(), ContractError> { +) -> Result { match &mut btc_del.undelegation_info { Some(undelegation_info) => { undelegation_info.delegator_slashing_sig = slashing_tx_sig.to_vec().into(); @@ -390,9 +396,12 @@ fn btc_undelegate_slashed( // - Who are subscribers in this context? // - How to notify them? Emit event? - // TODO? Record event that the BTC delegation becomes unbonded due to slashing at this height + // Record event that the BTC delegation becomes unbonded due to slashing at this height + let ev = Event::new("btc_undelegation_slashed") + .add_attribute("staking_tx_hash", staking_tx_hash.to_string()) + .add_attribute("height", height.to_string()); - Ok(()) + Ok(ev) } /// `compute_active_finality_providers` sorts all finality providers, counts the total voting From d3a46f0a3088e43c2801b56fd3477101b5f9860e Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Wed, 7 Aug 2024 11:55:21 +0200 Subject: [PATCH 04/18] Fix: delegation test data helper --- contracts/btc-staking/src/contract.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/btc-staking/src/contract.rs b/contracts/btc-staking/src/contract.rs index 9b42582f..2037ead5 100644 --- a/contracts/btc-staking/src/contract.rs +++ b/contracts/btc-staking/src/contract.rs @@ -316,7 +316,7 @@ pub(crate) mod tests { total_sat: del.total_sat, staking_tx: Binary::new(del.staking_tx.to_vec()), slashing_tx: Binary::new(del.slashing_tx.to_vec()), - delegator_slashing_sig: Binary::new(vec![]), + delegator_slashing_sig: Binary::new(del.delegator_sig.to_vec()), covenant_sigs: del .covenant_sigs .iter() @@ -334,7 +334,7 @@ pub(crate) mod tests { undelegation_info: BtcUndelegationInfo { unbonding_tx: Binary::new(btc_undelegation.unbonding_tx.to_vec()), slashing_tx: Binary::new(btc_undelegation.slashing_tx.to_vec()), - delegator_unbonding_sig: Binary::new(vec![]), + delegator_unbonding_sig: Binary::new(btc_undelegation.delegator_unbonding_sig.to_vec()), delegator_slashing_sig: Binary::new( btc_undelegation.delegator_slashing_sig.to_vec(), ), From c9eda7276963ecde6f90233e72f49ba8abec0bef Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Wed, 7 Aug 2024 13:51:36 +0200 Subject: [PATCH 05/18] Fix: Re-use unbonding_sig field for slashed delegations as well --- contracts/btc-staking/src/staking.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/btc-staking/src/staking.rs b/contracts/btc-staking/src/staking.rs index a4cfdef0..a8b6854d 100644 --- a/contracts/btc-staking/src/staking.rs +++ b/contracts/btc-staking/src/staking.rs @@ -382,7 +382,8 @@ fn btc_undelegate_slashed( ) -> Result { match &mut btc_del.undelegation_info { Some(undelegation_info) => { - undelegation_info.delegator_slashing_sig = slashing_tx_sig.to_vec().into(); + // FIXME? Use another field for signaling unbonding due to slashing + undelegation_info.delegator_unbonding_sig = slashing_tx_sig.to_vec().into(); } None => { return Err(ContractError::MissingUnbondingInfo); From c1bbe9843283d601d74f75cd058126a77c2a0604 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Wed, 7 Aug 2024 14:01:16 +0200 Subject: [PATCH 06/18] Simplify / unify logic between unbonding and unbonding due to slashing --- contracts/btc-staking/src/contract.rs | 4 +- contracts/btc-staking/src/staking.rs | 54 +++++++-------------------- 2 files changed, 17 insertions(+), 41 deletions(-) diff --git a/contracts/btc-staking/src/contract.rs b/contracts/btc-staking/src/contract.rs index 2037ead5..c4457450 100644 --- a/contracts/btc-staking/src/contract.rs +++ b/contracts/btc-staking/src/contract.rs @@ -334,7 +334,9 @@ pub(crate) mod tests { undelegation_info: BtcUndelegationInfo { unbonding_tx: Binary::new(btc_undelegation.unbonding_tx.to_vec()), slashing_tx: Binary::new(btc_undelegation.slashing_tx.to_vec()), - delegator_unbonding_sig: Binary::new(btc_undelegation.delegator_unbonding_sig.to_vec()), + delegator_unbonding_sig: Binary::new( + btc_undelegation.delegator_unbonding_sig.to_vec(), + ), delegator_slashing_sig: Binary::new( btc_undelegation.delegator_slashing_sig.to_vec(), ), diff --git a/contracts/btc-staking/src/staking.rs b/contracts/btc-staking/src/staking.rs index a8b6854d..8845b380 100644 --- a/contracts/btc-staking/src/staking.rs +++ b/contracts/btc-staking/src/staking.rs @@ -59,8 +59,8 @@ pub fn handle_btc_staking( // Process undelegations for undel in unbonded_delegations { - handle_undelegation(deps.storage, env.block.height, undel)?; - // TODO: Add event + let ev = handle_undelegation(deps.storage, env.block.height, undel)?; + res = res.add_event(ev); } Ok(res) @@ -273,7 +273,7 @@ fn handle_undelegation( storage: &mut dyn Storage, height: u64, undelegation: &UnbondedBtcDelegation, -) -> Result<(), ContractError> { +) -> Result { // Basic stateless checks undelegation.validate()?; @@ -306,8 +306,12 @@ fn handle_undelegation( Ok::<_, ContractError>(fp_state) })?; } + // Record event that the BTC delegation becomes unbonded + let unbonding_event = Event::new("btc_undelegation") + .add_attribute("staking_tx_hash", staking_tx_hash.to_string()) + .add_attribute("height", height.to_string()); - Ok(()) + Ok(unbonding_event) } /// handle_slashed_delegation handles undelegation due to slashing from an active delegation @@ -325,11 +329,11 @@ fn handle_slashed_delegation( // TODO: Ensure the BTC delegation is active + // Mark the delegation as undelegated due to slashing let delegator_slashing_sig = btc_del.delegator_slashing_sig.clone(); - let slashing_event = btc_undelegate_slashed( + btc_undelegate( storage, &staking_tx_hash, - height, &mut btc_del, delegator_slashing_sig.as_slice(), )?; @@ -345,6 +349,10 @@ fn handle_slashed_delegation( Ok::<_, ContractError>(fp_state) })?; } + // Record event that the BTC delegation becomes unbonded due to slashing at this height + let slashing_event = Event::new("btc_undelegation_slashed") + .add_attribute("staking_tx_hash", staking_tx_hash.to_string()) + .add_attribute("height", height.to_string()); Ok(slashing_event) } @@ -371,40 +379,6 @@ fn btc_undelegate( Ok(()) } -/// btc_undelegate_slashed adds the signature of the slashing tx signed by the staker to the given -/// BTC delegation -fn btc_undelegate_slashed( - storage: &mut dyn Storage, - staking_tx_hash: &Txid, - height: u64, - btc_del: &mut ActiveBtcDelegation, - slashing_tx_sig: &[u8], -) -> Result { - match &mut btc_del.undelegation_info { - Some(undelegation_info) => { - // FIXME? Use another field for signaling unbonding due to slashing - undelegation_info.delegator_unbonding_sig = slashing_tx_sig.to_vec().into(); - } - None => { - return Err(ContractError::MissingUnbondingInfo); - } - } - - // Set BTC delegation back to KV store - DELEGATIONS.save(storage, staking_tx_hash.as_ref(), btc_del)?; - - // TODO? Notify subscriber about this slashed BTC delegation - // - Who are subscribers in this context? - // - How to notify them? Emit event? - - // Record event that the BTC delegation becomes unbonded due to slashing at this height - let ev = Event::new("btc_undelegation_slashed") - .add_attribute("staking_tx_hash", staking_tx_hash.to_string()) - .add_attribute("height", height.to_string()); - - Ok(ev) -} - /// `compute_active_finality_providers` sorts all finality providers, counts the total voting /// power of top finality providers, and records them in the contract state pub fn compute_active_finality_providers( From 90791628b4ec1e6f43f901854de887d3ba09ff57 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Mon, 12 Aug 2024 11:33:14 +0200 Subject: [PATCH 07/18] Add is_slashed to BtcDelegation for ergonomics --- contracts/btc-staking/src/state/staking.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/contracts/btc-staking/src/state/staking.rs b/contracts/btc-staking/src/state/staking.rs index 82645bdc..27ea0cda 100644 --- a/contracts/btc-staking/src/state/staking.rs +++ b/contracts/btc-staking/src/state/staking.rs @@ -51,19 +51,24 @@ impl BtcDelegation { pub fn is_active(&self) -> bool { // TODO: Implement full delegation status checks (needs BTC height) // self.get_status(btc_height, w) == BTCDelegationStatus::ACTIVE - !self.is_unbonded_early() + !self.is_unbonded_early() && !self.is_slashed() } fn is_unbonded_early(&self) -> bool { !self.undelegation_info.delegator_unbonding_sig.is_empty() } + fn is_slashed(&self) -> bool { + self.undelegation_info.delegator_unbonding_sig == self.delegator_slashing_sig + } + pub fn get_status(&self, btc_height: u64, w: u64) -> BTCDelegationStatus { // Manually unbonded, staking tx time-lock has not begun, is less than w BTC blocks left, or // has expired if self.is_unbonded_early() || btc_height < self.start_height || btc_height + w > self.end_height + || self.is_slashed() { BTCDelegationStatus::UNBONDED } else { From 4c58994fb917972a1a7a4d0639e42e26f6b13be0 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Wed, 7 Aug 2024 14:08:49 +0200 Subject: [PATCH 08/18] Add extra check for robustness --- contracts/btc-staking/src/state/staking.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/btc-staking/src/state/staking.rs b/contracts/btc-staking/src/state/staking.rs index 27ea0cda..e4294b38 100644 --- a/contracts/btc-staking/src/state/staking.rs +++ b/contracts/btc-staking/src/state/staking.rs @@ -59,7 +59,8 @@ impl BtcDelegation { } fn is_slashed(&self) -> bool { - self.undelegation_info.delegator_unbonding_sig == self.delegator_slashing_sig + !self.undelegation_info.delegator_unbonding_sig.is_empty() + && self.undelegation_info.delegator_unbonding_sig == self.delegator_slashing_sig } pub fn get_status(&self, btc_height: u64, w: u64) -> BTCDelegationStatus { From 9785bc62f4b99af80fb068538d3740d7fa2f20f7 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Wed, 7 Aug 2024 15:52:07 +0200 Subject: [PATCH 09/18] Better TODO --- contracts/btc-staking/src/staking.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/btc-staking/src/staking.rs b/contracts/btc-staking/src/staking.rs index 8845b380..4661afe1 100644 --- a/contracts/btc-staking/src/staking.rs +++ b/contracts/btc-staking/src/staking.rs @@ -409,7 +409,7 @@ pub fn compute_active_finality_providers( .unzip(); // TODO: Online FPs verification - // TODO: Filter out offline / jailed FPs + // TODO: Filter out slashed / offline / jailed FPs // Save the new set of active finality providers // TODO: Purge old (height - finality depth) FP_SET entries to avoid bloating the storage FP_SET.save(storage, env.block.height, &finality_providers)?; From d942a08e500519ac65c0d43d40b65f84156532d7 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Thu, 8 Aug 2024 10:07:39 +0200 Subject: [PATCH 10/18] Don't attempt empty tests --- contracts/babylon/Cargo.toml | 2 ++ contracts/btc-staking/Cargo.toml | 2 ++ contracts/op-finality-gadget/Cargo.toml | 2 ++ 3 files changed, 6 insertions(+) diff --git a/contracts/babylon/Cargo.toml b/contracts/babylon/Cargo.toml index ebffdd28..12588565 100644 --- a/contracts/babylon/Cargo.toml +++ b/contracts/babylon/Cargo.toml @@ -13,11 +13,13 @@ publish = false crate-type = ["cdylib", "rlib"] # See https://bheisler.github.io/criterion.rs/book/faq.html#cargo-bench-gives-unrecognized-option-errors-for-valid-command-line-options bench = false +doctest = false [[bin]] name = "schema" path = "src/bin/schema.rs" bench = false +test = false [features] # Add feature "cranelift" to default if you need 32 bit or ARM support diff --git a/contracts/btc-staking/Cargo.toml b/contracts/btc-staking/Cargo.toml index d132d8f3..8ff77f90 100644 --- a/contracts/btc-staking/Cargo.toml +++ b/contracts/btc-staking/Cargo.toml @@ -11,10 +11,12 @@ publish = false [lib] crate-type = ["cdylib", "rlib"] +doctest = false [[bin]] name = "btcstaking-schema" path = "src/bin/schema.rs" +test = false [features] # Add feature "cranelift" to default if you need 32 bit or ARM support diff --git a/contracts/op-finality-gadget/Cargo.toml b/contracts/op-finality-gadget/Cargo.toml index c100c90c..0bf067e4 100644 --- a/contracts/op-finality-gadget/Cargo.toml +++ b/contracts/op-finality-gadget/Cargo.toml @@ -11,10 +11,12 @@ publish.workspace = true [lib] crate-type = ["cdylib", "rlib"] +doctest = false [[bin]] name = "op-finality-gadget-schema" path = "src/bin/schema.rs" +test = false [features] library = [] From 4e0a0b5e9279522422dd3f83314f24224993e1d1 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Thu, 8 Aug 2024 10:12:28 +0200 Subject: [PATCH 11/18] Slashed delegations test --- contracts/btc-staking/src/staking.rs | 110 ++++++++++++++++++++++++++- 1 file changed, 106 insertions(+), 4 deletions(-) diff --git a/contracts/btc-staking/src/staking.rs b/contracts/btc-staking/src/staking.rs index 4661afe1..469eda23 100644 --- a/contracts/btc-staking/src/staking.rs +++ b/contracts/btc-staking/src/staking.rs @@ -484,7 +484,7 @@ pub(crate) mod tests { } #[test] - fn test_btc_staking_add_fp_unauthorized() { + fn test_add_fp_unauthorized() { let mut deps = mock_dependencies(); let info = message_info(&deps.api.addr_make(CREATOR), &[]); let init_admin = deps.api.addr_make(INIT_ADMIN); @@ -516,7 +516,7 @@ pub(crate) mod tests { } #[test] - fn test_btc_staking_add_fp_admin() { + fn test_add_fp_admin() { let mut deps = mock_dependencies(); let info = message_info(&deps.api.addr_make(CREATOR), &[]); let init_admin = deps.api.addr_make(INIT_ADMIN); @@ -562,7 +562,7 @@ pub(crate) mod tests { } #[test] - fn btc_staking_active_delegation_happy_path() { + fn active_delegation_happy_path() { let mut deps = mock_dependencies(); let info = message_info(&deps.api.addr_make(CREATOR), &[]); @@ -624,7 +624,7 @@ pub(crate) mod tests { } #[test] - fn btc_staking_undelegation_works() { + fn undelegation_works() { let mut deps = mock_dependencies(); let info = message_info(&deps.api.addr_make(CREATOR), &[]); @@ -719,4 +719,106 @@ pub(crate) mod tests { .unwrap(); assert_eq!(fp.power, 0); } + + #[test] + fn slashed_delegation_works() { + let mut deps = mock_dependencies(); + let info = message_info(&deps.api.addr_make(CREATOR), &[]); + + instantiate( + deps.as_mut(), + mock_env(), + info.clone(), + InstantiateMsg { + params: None, + admin: None, + }, + ) + .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 msg = ExecuteMsg::BtcStaking { + new_fp: vec![new_fp.clone()], + active_del: vec![active_delegation.clone()], + slashed_del: vec![], + unbonded_del: vec![], + }; + + let res = execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); + assert_eq!(0, res.messages.len()); + + // Check the delegation is active (it has no unbonding or slashing tx signature) + let active_delegation_undelegation = active_delegation.undelegation_info.clone().unwrap(); + // Compute the staking tx hash + let staking_tx_hash_hex = staking_tx_hash(&active_delegation).to_string(); + + let btc_del = queries::delegation(deps.as_ref(), staking_tx_hash_hex.clone()).unwrap(); + let btc_undelegation = btc_del.undelegation_info.unwrap(); + assert_eq!( + btc_undelegation, + BtcUndelegationInfo { + unbonding_tx: active_delegation_undelegation.unbonding_tx, + slashing_tx: active_delegation_undelegation.slashing_tx, + delegator_unbonding_sig: Binary::new(vec![]), + delegator_slashing_sig: active_delegation_undelegation.delegator_slashing_sig, + covenant_unbonding_sig_list: vec![], + covenant_slashing_sigs: vec![], + } + ); + + // Now send the slashed delegation message + let slashed = SlashedBtcDelegation { + staking_tx_hash: staking_tx_hash_hex.clone(), + recovered_fp_btc_sk: "deadbeef".to_string(), // Currently unused + }; + + let msg = ExecuteMsg::BtcStaking { + new_fp: vec![], + active_del: vec![], + unbonded_del: vec![], + slashed_del: vec![slashed.clone()], + }; + + let res = execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); + assert_eq!(0, res.messages.len()); + // Check events + assert_eq!(res.events.len(), 1); + assert_eq!(res.events[0].ty.as_str(), "btc_undelegation_slashed"); + assert_eq!(res.events[0].attributes.len(), 2); + assert_eq!(res.events[0].attributes[0].key.as_str(), "staking_tx_hash"); + assert_eq!( + res.events[0].attributes[0].value.as_str(), + staking_tx_hash_hex + ); + assert_eq!(res.events[0].attributes[1].key.as_str(), "height"); + + // Check the delegation is not active any more (updated with the unbonding tx signature) + let active_delegation_undelegation = active_delegation.undelegation_info.unwrap(); + let btc_del = queries::delegation(deps.as_ref(), staking_tx_hash_hex).unwrap(); + let btc_undelegation = btc_del.undelegation_info.unwrap(); + assert_eq!( + btc_undelegation, + BtcUndelegationInfo { + unbonding_tx: active_delegation_undelegation.unbonding_tx, + slashing_tx: active_delegation_undelegation.slashing_tx, + delegator_unbonding_sig: active_delegation.delegator_slashing_sig, // The slashing sig is now the unbonding sig + delegator_slashing_sig: active_delegation_undelegation.delegator_slashing_sig, + covenant_unbonding_sig_list: vec![], + covenant_slashing_sigs: vec![], + } + ); + + // Check the finality provider power has been updated + let fp = queries::finality_provider_info(deps.as_ref(), new_fp.btc_pk_hex.clone(), None) + .unwrap(); + assert_eq!(fp.power, 0); + } } From c0feaa86848f70629e9b0b2fbfc4b418ced4742c Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Thu, 8 Aug 2024 17:05:32 +0200 Subject: [PATCH 12/18] Fix: Route slashed delegations message --- contracts/babylon/src/ibc.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/contracts/babylon/src/ibc.rs b/contracts/babylon/src/ibc.rs index 55e51e01..7d2d965f 100644 --- a/contracts/babylon/src/ibc.rs +++ b/contracts/babylon/src/ibc.rs @@ -140,6 +140,7 @@ pub fn ibc_packet_receive( pub(crate) mod ibc_packet { use super::*; use crate::state::config::CONFIG; + use babylon_apis::btc_staking_api::SlashedBtcDelegation; use babylon_apis::ibc_consumer::{consumer_packet_data, ConsumerPacketData}; use babylon_apis::{ btc_staking_api::{ @@ -285,7 +286,14 @@ pub(crate) mod ibc_packet { }) }) .collect::>()?, - slashed_del: vec![], // FIXME: Route this + slashed_del: btc_staking + .slashed_del + .iter() + .map(|d| SlashedBtcDelegation { + staking_tx_hash: d.staking_tx_hash.clone(), + recovered_fp_btc_sk: d.recovered_fp_btc_sk.clone(), + }) + .collect(), unbonded_del: btc_staking .unbonded_del .iter() From 7a406b390454f57b48acda1a24497f0547be00a8 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Thu, 8 Aug 2024 17:06:22 +0200 Subject: [PATCH 13/18] Update schemas --- contracts/btc-staking/schema/btc-staking.json | 4 ++-- contracts/btc-staking/schema/raw/execute.json | 2 +- contracts/btc-staking/schema/raw/response_to_delegation.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/btc-staking/schema/btc-staking.json b/contracts/btc-staking/schema/btc-staking.json index 5cf86a0f..760a9765 100644 --- a/contracts/btc-staking/schema/btc-staking.json +++ b/contracts/btc-staking/schema/btc-staking.json @@ -343,7 +343,7 @@ "type": "string" }, "BtcUndelegationInfo": { - "description": "BTCUndelegationInfo provides all necessary info about the undeleagation", + "description": "BTCUndelegationInfo provides all necessary info about the undelegation", "type": "object", "required": [ "covenant_slashing_sigs", @@ -1396,7 +1396,7 @@ "type": "string" }, "BtcUndelegationInfo": { - "description": "BTCUndelegationInfo provides all necessary info about the undeleagation", + "description": "BTCUndelegationInfo provides all necessary info about the undelegation", "type": "object", "required": [ "covenant_slashing_sigs", diff --git a/contracts/btc-staking/schema/raw/execute.json b/contracts/btc-staking/schema/raw/execute.json index bc7ba6d1..67164ca2 100644 --- a/contracts/btc-staking/schema/raw/execute.json +++ b/contracts/btc-staking/schema/raw/execute.json @@ -290,7 +290,7 @@ "type": "string" }, "BtcUndelegationInfo": { - "description": "BTCUndelegationInfo provides all necessary info about the undeleagation", + "description": "BTCUndelegationInfo provides all necessary info about the undelegation", "type": "object", "required": [ "covenant_slashing_sigs", diff --git a/contracts/btc-staking/schema/raw/response_to_delegation.json b/contracts/btc-staking/schema/raw/response_to_delegation.json index b30ac050..f20b76c1 100644 --- a/contracts/btc-staking/schema/raw/response_to_delegation.json +++ b/contracts/btc-staking/schema/raw/response_to_delegation.json @@ -118,7 +118,7 @@ "type": "string" }, "BtcUndelegationInfo": { - "description": "BTCUndelegationInfo provides all necessary info about the undeleagation", + "description": "BTCUndelegationInfo provides all necessary info about the undelegation", "type": "object", "required": [ "covenant_slashing_sigs", From e2e0b10480de42cd9d0fc2f0e5d789f65878ce2e Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Mon, 12 Aug 2024 11:45:42 +0200 Subject: [PATCH 14/18] Adapt to new btc delegation format / struct --- contracts/btc-staking/src/staking.rs | 35 ++++++++++++++++------------ 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/contracts/btc-staking/src/staking.rs b/contracts/btc-staking/src/staking.rs index 469eda23..20fe422c 100644 --- a/contracts/btc-staking/src/staking.rs +++ b/contracts/btc-staking/src/staking.rs @@ -477,9 +477,9 @@ pub(crate) mod tests { use crate::queries; use crate::state::staking::BtcUndelegationInfo; - // Compute staking tx hash of an active delegation + // Compute staking tx hash of a delegation pub(crate) fn staking_tx_hash(del: &BtcDelegation) -> Txid { - let staking_tx: Transaction = bitcoin::consensus::deserialize(&del.staking_tx).unwrap(); + let staking_tx: Transaction = deserialize(&del.staking_tx).unwrap(); staking_tx.txid() } @@ -756,19 +756,22 @@ pub(crate) mod tests { assert_eq!(0, res.messages.len()); // Check the delegation is active (it has no unbonding or slashing tx signature) - let active_delegation_undelegation = active_delegation.undelegation_info.clone().unwrap(); + let active_delegation_undelegation = active_delegation.undelegation_info.clone(); // Compute the staking tx hash - let staking_tx_hash_hex = staking_tx_hash(&active_delegation).to_string(); + let delegation = BtcDelegation::from(&active_delegation); + let staking_tx_hash_hex = staking_tx_hash(&delegation).to_string(); let btc_del = queries::delegation(deps.as_ref(), staking_tx_hash_hex.clone()).unwrap(); - let btc_undelegation = btc_del.undelegation_info.unwrap(); + let btc_undelegation = btc_del.undelegation_info; assert_eq!( btc_undelegation, BtcUndelegationInfo { - unbonding_tx: active_delegation_undelegation.unbonding_tx, - slashing_tx: active_delegation_undelegation.slashing_tx, - delegator_unbonding_sig: Binary::new(vec![]), - delegator_slashing_sig: active_delegation_undelegation.delegator_slashing_sig, + unbonding_tx: active_delegation_undelegation.unbonding_tx.into(), + slashing_tx: active_delegation_undelegation.slashing_tx.into(), + delegator_unbonding_sig: vec![], + delegator_slashing_sig: active_delegation_undelegation + .delegator_slashing_sig + .into(), covenant_unbonding_sig_list: vec![], covenant_slashing_sigs: vec![], } @@ -801,16 +804,18 @@ pub(crate) mod tests { assert_eq!(res.events[0].attributes[1].key.as_str(), "height"); // Check the delegation is not active any more (updated with the unbonding tx signature) - let active_delegation_undelegation = active_delegation.undelegation_info.unwrap(); + let active_delegation_undelegation = active_delegation.undelegation_info; let btc_del = queries::delegation(deps.as_ref(), staking_tx_hash_hex).unwrap(); - let btc_undelegation = btc_del.undelegation_info.unwrap(); + let btc_undelegation = btc_del.undelegation_info; assert_eq!( btc_undelegation, BtcUndelegationInfo { - unbonding_tx: active_delegation_undelegation.unbonding_tx, - slashing_tx: active_delegation_undelegation.slashing_tx, - delegator_unbonding_sig: active_delegation.delegator_slashing_sig, // The slashing sig is now the unbonding sig - delegator_slashing_sig: active_delegation_undelegation.delegator_slashing_sig, + unbonding_tx: active_delegation_undelegation.unbonding_tx.into(), + slashing_tx: active_delegation_undelegation.slashing_tx.into(), + delegator_unbonding_sig: active_delegation.delegator_slashing_sig.into(), // The slashing sig is now the unbonding sig + delegator_slashing_sig: active_delegation_undelegation + .delegator_slashing_sig + .into(), covenant_unbonding_sig_list: vec![], covenant_slashing_sigs: vec![], } From d2d4abff4c693b9a244121a6c907badbde01f6f6 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Mon, 12 Aug 2024 11:57:15 +0200 Subject: [PATCH 15/18] Add slashed field to btc delegation state Use to indicate slashed delegations Adapt tests --- contracts/btc-staking/src/staking.rs | 21 ++++++++++----------- contracts/btc-staking/src/state/staking.rs | 6 ++++-- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/contracts/btc-staking/src/staking.rs b/contracts/btc-staking/src/staking.rs index 20fe422c..0954d8c1 100644 --- a/contracts/btc-staking/src/staking.rs +++ b/contracts/btc-staking/src/staking.rs @@ -329,15 +329,6 @@ fn handle_slashed_delegation( // TODO: Ensure the BTC delegation is active - // Mark the delegation as undelegated due to slashing - let delegator_slashing_sig = btc_del.delegator_slashing_sig.clone(); - btc_undelegate( - storage, - &staking_tx_hash, - &mut btc_del, - delegator_slashing_sig.as_slice(), - )?; - // Discount the voting power from the affected finality providers let affected_fps = DELEGATION_FPS.load(storage, staking_tx_hash.as_ref())?; let fps = fps(); @@ -349,6 +340,11 @@ fn handle_slashed_delegation( Ok::<_, ContractError>(fp_state) })?; } + + // Mark the delegation as slashed + btc_del.slashed = true; + DELEGATIONS.save(storage, staking_tx_hash.as_ref(), &btc_del)?; + // Record event that the BTC delegation becomes unbonded due to slashing at this height let slashing_event = Event::new("btc_undelegation_slashed") .add_attribute("staking_tx_hash", staking_tx_hash.to_string()) @@ -803,16 +799,19 @@ pub(crate) mod tests { ); assert_eq!(res.events[0].attributes[1].key.as_str(), "height"); - // Check the delegation is not active any more (updated with the unbonding tx signature) + // Check the delegation is not active any more (slashed) let active_delegation_undelegation = active_delegation.undelegation_info; let btc_del = queries::delegation(deps.as_ref(), staking_tx_hash_hex).unwrap(); + assert!(btc_del.slashed); + + // Check the undelegation info in passing let btc_undelegation = btc_del.undelegation_info; assert_eq!( btc_undelegation, BtcUndelegationInfo { unbonding_tx: active_delegation_undelegation.unbonding_tx.into(), slashing_tx: active_delegation_undelegation.slashing_tx.into(), - delegator_unbonding_sig: active_delegation.delegator_slashing_sig.into(), // The slashing sig is now the unbonding sig + delegator_unbonding_sig: vec![], delegator_slashing_sig: active_delegation_undelegation .delegator_slashing_sig .into(), diff --git a/contracts/btc-staking/src/state/staking.rs b/contracts/btc-staking/src/state/staking.rs index e4294b38..fb842e89 100644 --- a/contracts/btc-staking/src/state/staking.rs +++ b/contracts/btc-staking/src/state/staking.rs @@ -45,6 +45,8 @@ pub struct BtcDelegation { pub undelegation_info: BtcUndelegationInfo, /// params version used to validate the delegation pub params_version: u32, + /// slashed is used to indicate whether a given delegation is related to a slashed FP + pub slashed: bool, } impl BtcDelegation { @@ -59,8 +61,7 @@ impl BtcDelegation { } fn is_slashed(&self) -> bool { - !self.undelegation_info.delegator_unbonding_sig.is_empty() - && self.undelegation_info.delegator_unbonding_sig == self.delegator_slashing_sig + self.slashed } pub fn get_status(&self, btc_height: u64, w: u64) -> BTCDelegationStatus { @@ -101,6 +102,7 @@ impl From for BtcDelegation { unbonding_time: active_delegation.unbonding_time, undelegation_info: active_delegation.undelegation_info.into(), params_version: active_delegation.params_version, + slashed: false, } } } From 55b0408dfbd90ede5bb1dbd0f9fac4f9465356ba Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Tue, 13 Aug 2024 08:38:10 +0200 Subject: [PATCH 16/18] Improve slashed delegation test --- contracts/btc-staking/src/staking.rs | 48 ++++++++-------------------- 1 file changed, 13 insertions(+), 35 deletions(-) diff --git a/contracts/btc-staking/src/staking.rs b/contracts/btc-staking/src/staking.rs index 0954d8c1..99466305 100644 --- a/contracts/btc-staking/src/staking.rs +++ b/contracts/btc-staking/src/staking.rs @@ -751,27 +751,19 @@ pub(crate) mod tests { let res = execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); assert_eq!(0, res.messages.len()); - // Check the delegation is active (it has no unbonding or slashing tx signature) - let active_delegation_undelegation = active_delegation.undelegation_info.clone(); + // Check the delegation is active (it has no unbonding sig or is slashed) // Compute the staking tx hash let delegation = BtcDelegation::from(&active_delegation); let staking_tx_hash_hex = staking_tx_hash(&delegation).to_string(); - + // Query the delegation let btc_del = queries::delegation(deps.as_ref(), staking_tx_hash_hex.clone()).unwrap(); - let btc_undelegation = btc_del.undelegation_info; - assert_eq!( - btc_undelegation, - BtcUndelegationInfo { - unbonding_tx: active_delegation_undelegation.unbonding_tx.into(), - slashing_tx: active_delegation_undelegation.slashing_tx.into(), - delegator_unbonding_sig: vec![], - delegator_slashing_sig: active_delegation_undelegation - .delegator_slashing_sig - .into(), - covenant_unbonding_sig_list: vec![], - covenant_slashing_sigs: vec![], - } - ); + assert!(&btc_del.undelegation_info.delegator_unbonding_sig.is_empty()); + assert!(!btc_del.slashed); + + // Check the finality provider has power + let fp = queries::finality_provider_info(deps.as_ref(), new_fp.btc_pk_hex.clone(), None) + .unwrap(); + assert_eq!(fp.power, btc_del.total_sat); // Now send the slashed delegation message let slashed = SlashedBtcDelegation { @@ -800,27 +792,13 @@ pub(crate) mod tests { assert_eq!(res.events[0].attributes[1].key.as_str(), "height"); // Check the delegation is not active any more (slashed) - let active_delegation_undelegation = active_delegation.undelegation_info; let btc_del = queries::delegation(deps.as_ref(), staking_tx_hash_hex).unwrap(); assert!(btc_del.slashed); + // Check the unbonding sig is still empty + assert!(btc_del.undelegation_info.delegator_unbonding_sig.is_empty()); - // Check the undelegation info in passing - let btc_undelegation = btc_del.undelegation_info; - assert_eq!( - btc_undelegation, - BtcUndelegationInfo { - unbonding_tx: active_delegation_undelegation.unbonding_tx.into(), - slashing_tx: active_delegation_undelegation.slashing_tx.into(), - delegator_unbonding_sig: vec![], - delegator_slashing_sig: active_delegation_undelegation - .delegator_slashing_sig - .into(), - covenant_unbonding_sig_list: vec![], - covenant_slashing_sigs: vec![], - } - ); - - // Check the finality provider power has been updated + // Check the finality provider power has been zeroed (it has only this delegation that was + // slashed) let fp = queries::finality_provider_info(deps.as_ref(), new_fp.btc_pk_hex.clone(), None) .unwrap(); assert_eq!(fp.power, 0); From ce52c7421378e58c441c28f91a19d97226273a84 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Tue, 13 Aug 2024 10:18:51 +0200 Subject: [PATCH 17/18] cargo clippy Improve TODO --- contracts/btc-staking/src/contract.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/btc-staking/src/contract.rs b/contracts/btc-staking/src/contract.rs index c4457450..9978752a 100644 --- a/contracts/btc-staking/src/contract.rs +++ b/contracts/btc-staking/src/contract.rs @@ -246,9 +246,9 @@ fn get_btc_tip_height(deps: &DepsMut) -> Result { let babylon_addr = CONFIG.load(deps.storage)?.babylon; // Query the Babylon contract - // TODO: use raw query + // TODO: use a raw query for performance / efficiency let query_msg = BabylonQueryMsg::BtcTipHeader {}; - let tip: BtcHeaderResponse = deps.querier.query_wasm_smart(&babylon_addr, &query_msg)?; + let tip: BtcHeaderResponse = deps.querier.query_wasm_smart(babylon_addr, &query_msg)?; Ok(tip.height) } From e403a83e78f789d96129c9c8fba0e7b8d4e4e701 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Tue, 13 Aug 2024 10:19:11 +0200 Subject: [PATCH 18/18] Update schema --- contracts/btc-staking/schema/btc-staking.json | 5 +++++ .../btc-staking/schema/raw/response_to_delegations.json | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/contracts/btc-staking/schema/btc-staking.json b/contracts/btc-staking/schema/btc-staking.json index 760a9765..e59535a9 100644 --- a/contracts/btc-staking/schema/btc-staking.json +++ b/contracts/btc-staking/schema/btc-staking.json @@ -1527,6 +1527,7 @@ "end_height", "fp_btc_pk_list", "params_version", + "slashed", "slashing_tx", "staker_addr", "staking_output_idx", @@ -1576,6 +1577,10 @@ "format": "uint32", "minimum": 0.0 }, + "slashed": { + "description": "slashed is used to indicate whether a given delegation is related to a slashed FP", + "type": "boolean" + }, "slashing_tx": { "description": "slashing_tx is the slashing tx", "type": "array", diff --git a/contracts/btc-staking/schema/raw/response_to_delegations.json b/contracts/btc-staking/schema/raw/response_to_delegations.json index 06f7949a..9cb87334 100644 --- a/contracts/btc-staking/schema/raw/response_to_delegations.json +++ b/contracts/btc-staking/schema/raw/response_to_delegations.json @@ -24,6 +24,7 @@ "end_height", "fp_btc_pk_list", "params_version", + "slashed", "slashing_tx", "staker_addr", "staking_output_idx", @@ -73,6 +74,10 @@ "format": "uint32", "minimum": 0.0 }, + "slashed": { + "description": "slashed is used to indicate whether a given delegation is related to a slashed FP", + "type": "boolean" + }, "slashing_tx": { "description": "slashing_tx is the slashing tx", "type": "array",