From 166fda9484fdb57019e2bffda7827e50d072b3cf Mon Sep 17 00:00:00 2001 From: parketh Date: Wed, 11 Dec 2024 02:57:33 +0000 Subject: [PATCH 1/3] feat: op consumer chain slashing (#92) ## Summary This PR implements slashing for op stack consumer chain. It updates the cw contract as follows: - allow FPs to equivocate by submitting mutiple signatures for a block - add new state to index voted block hashes and equivocation evidences - add new handlers to slash finality providers and send notifying message to Babylon, similar to the Cosmos consumer chain implementation It also updates and add a new test case. We need to add additional integration tests to check the e2e flow with Babylon node. ## Test plan ```bash # build the contract cargo build cargo optimize # test it cargo test cargo integration ``` --- Cargo.lock | 12 +- Cargo.toml | 1 + contracts/op-finality-gadget/Cargo.toml | 10 + contracts/op-finality-gadget/src/contract.rs | 11 +- contracts/op-finality-gadget/src/error.rs | 12 + .../op-finality-gadget/src/exec/admin.rs | 3 +- .../op-finality-gadget/src/exec/finality.rs | 260 +++++++++++++----- contracts/op-finality-gadget/src/lib.rs | 5 +- contracts/op-finality-gadget/src/msg.rs | 12 + .../op-finality-gadget/src/state/finality.rs | 9 +- packages/bindings-test/src/multitest.rs | 4 + packages/bindings/Cargo.toml | 1 + packages/bindings/src/msg.rs | 10 +- packages/proto/babylon | 2 +- packages/proto/buf.gen.rust.yaml | 1 + packages/proto/src/gen/babylon.finality.v1.rs | 8 + 16 files changed, 285 insertions(+), 76 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bc7d2fab..69efd942 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -237,6 +237,7 @@ dependencies = [ name = "babylon-bindings" version = "0.4.0" dependencies = [ + "babylon-apis", "cosmwasm-schema", "cosmwasm-std", ] @@ -2109,17 +2110,26 @@ name = "op-finality-gadget" version = "0.11.0" dependencies = [ "anybuf", + "anyhow", "babylon-apis", + "babylon-bindings", + "babylon-bindings-test", + "babylon-bitcoin", "babylon-merkle", + "babylon-proto", + "btc-staking", "cosmwasm-schema", "cosmwasm-std", "cosmwasm-vm", "cw-controllers", + "cw-multi-test", "cw-storage-plus", "cw-utils", + "derivative", "eots", "hex", "k256", + "prost 0.11.9", "test-utils", "thiserror", ] diff --git a/Cargo.toml b/Cargo.toml index 69f65a24..3c66e6b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ integration = "./scripts/integration_test.sh" [workspace.dependencies] anybuf = { version = "0.5.0" } +babylon-apis = { path = "./packages/apis" } babylon-proto = { path = "./packages/proto" } babylon-bitcoin = { path = "./packages/bitcoin" } babylon-btcstaking = { path = "./packages/btcstaking" } diff --git a/contracts/op-finality-gadget/Cargo.toml b/contracts/op-finality-gadget/Cargo.toml index 0bf067e4..b59e334e 100644 --- a/contracts/op-finality-gadget/Cargo.toml +++ b/contracts/op-finality-gadget/Cargo.toml @@ -24,6 +24,9 @@ library = [] [dependencies] babylon-apis = { path = "../../packages/apis" } babylon-merkle = { path = "../../packages/merkle" } +babylon-bindings = { path = "../../packages/bindings" } +babylon-bitcoin = { path = "../../packages/bitcoin" } +babylon-proto = { path = "../../packages/proto" } cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } cw-controllers = { workspace = true } @@ -33,8 +36,15 @@ eots = { path = "../../packages/eots" } hex = { workspace = true } k256 = { workspace = true } thiserror = { workspace = true } +prost = { workspace = true } anybuf = { workspace = true } [dev-dependencies] +babylon-bindings-test = { path = "../../packages/bindings-test" } +btc-staking = { path = "../btc-staking", features = [ "library" ] } cosmwasm-vm = { workspace = true } test-utils = { path = "../../packages/test-utils" } +anyhow = { workspace = true } +derivative = { workspace = true } + +cw-multi-test = { workspace = true } \ No newline at end of file diff --git a/contracts/op-finality-gadget/src/contract.rs b/contracts/op-finality-gadget/src/contract.rs index 98eb0c88..714604d7 100644 --- a/contracts/op-finality-gadget/src/contract.rs +++ b/contracts/op-finality-gadget/src/contract.rs @@ -1,11 +1,14 @@ use crate::error::ContractError; use crate::exec::admin::set_enabled; -use crate::exec::finality::{handle_finality_signature, handle_public_randomness_commit}; +use crate::exec::finality::{ + handle_finality_signature, handle_public_randomness_commit, handle_slashing, +}; use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; use crate::queries::{ query_block_voters, query_config, query_first_pub_rand_commit, query_last_pub_rand_commit, }; use crate::state::config::{Config, ADMIN, CONFIG, IS_ENABLED}; +use babylon_bindings::BabylonMsg; use cosmwasm_std::{ to_json_binary, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, StdResult, }; @@ -16,7 +19,7 @@ pub fn instantiate( _env: Env, _info: MessageInfo, msg: InstantiateMsg, -) -> StdResult { +) -> StdResult> { let api = deps.api; ADMIN.set(deps.branch(), Some(api.addr_validate(&msg.admin)?))?; IS_ENABLED.save(deps.storage, &msg.is_enabled)?; @@ -51,7 +54,7 @@ pub fn execute( env: Env, info: MessageInfo, msg: ExecuteMsg, -) -> Result { +) -> Result, ContractError> { let api = deps.api; match msg { @@ -86,6 +89,7 @@ pub fn execute( &block_hash, &signature, ), + ExecuteMsg::Slashing { evidence } => handle_slashing(&evidence), ExecuteMsg::SetEnabled { enabled } => set_enabled(deps, info, enabled), ExecuteMsg::UpdateAdmin { admin } => ADMIN .execute_update_admin(deps, info, Some(api.addr_validate(&admin)?)) @@ -145,7 +149,6 @@ pub(crate) mod tests { let mut deps = mock_dependencies(); let init_admin = deps.api.addr_make(INIT_ADMIN); let new_admin = deps.api.addr_make(NEW_ADMIN); - // Create an InstantiateMsg with admin set to Some(INIT_ADMIN.into()) let instantiate_msg = InstantiateMsg { admin: init_admin.to_string(), // Admin provided diff --git a/contracts/op-finality-gadget/src/error.rs b/contracts/op-finality-gadget/src/error.rs index 051f9978..84fbc0c4 100644 --- a/contracts/op-finality-gadget/src/error.rs +++ b/contracts/op-finality-gadget/src/error.rs @@ -18,6 +18,16 @@ pub enum ContractError { HexError(#[from] FromHexError), #[error("The inclusion proof for height {0} does not correspond to the given height ({1})")] InvalidFinalitySigHeight(u64, u64), + #[error("Contract already has an open IBC channel")] + IbcChannelAlreadyOpen {}, + #[error("The contract only supports ordered channels")] + IbcUnorderedChannel {}, + #[error("Counterparty version must be `{version}`")] + IbcInvalidCounterPartyVersion { version: String }, + #[error("IBC method is not supported")] + IbcUnsupportedMethod {}, + #[error("IBC send timed out: dest: channel {0}, port {1}")] + IbcTimeout(String, String), #[error("The total amount of public randomnesses in the proof ({0}) does not match the amount of public committed randomness ({1})")] InvalidFinalitySigAmount(u64, u64), #[error("The start height ({0}) has overlap with the height of the highest public randomness committed ({1})")] @@ -30,6 +40,8 @@ pub enum ContractError { MissingPubRandCommit(String, u64), #[error("{0}")] SecP256K1Error(String), // TODO: inherit errors from k256 + #[error("Failed to extract secret key: {0}")] + SecretKeyExtractionError(String), #[error("{0}")] StdError(#[from] StdError), #[error("Failed to query block voters for block {0} with hash {1}. {2}")] diff --git a/contracts/op-finality-gadget/src/exec/admin.rs b/contracts/op-finality-gadget/src/exec/admin.rs index 0b0873a8..bf132cf4 100644 --- a/contracts/op-finality-gadget/src/exec/admin.rs +++ b/contracts/op-finality-gadget/src/exec/admin.rs @@ -1,3 +1,4 @@ +use babylon_bindings::BabylonMsg; use cosmwasm_std::{DepsMut, MessageInfo, Response}; use crate::{ @@ -14,7 +15,7 @@ pub fn set_enabled( deps: DepsMut, info: MessageInfo, enabled: bool, -) -> Result { +) -> Result, ContractError> { // Check caller is admin check_admin(&deps, info)?; // Check if the finality gadget is already in the desired state diff --git a/contracts/op-finality-gadget/src/exec/finality.rs b/contracts/op-finality-gadget/src/exec/finality.rs index a7f03e40..3a255014 100644 --- a/contracts/op-finality-gadget/src/exec/finality.rs +++ b/contracts/op-finality-gadget/src/exec/finality.rs @@ -1,17 +1,19 @@ use std::collections::HashSet; use crate::error::ContractError; +use crate::msg::ExecuteMsg; use crate::queries::query_last_pub_rand_commit; use crate::state::config::CONFIG; -use crate::state::finality::{BLOCK_VOTES, SIGNATURES}; +use crate::state::finality::{BLOCK_HASHES, BLOCK_VOTES, EVIDENCES, SIGNATURES}; use crate::state::public_randomness::{ get_pub_rand_commit_for_height, PUB_RAND_COMMITS, PUB_RAND_VALUES, }; use crate::utils::query_finality_provider; +use babylon_bindings::BabylonMsg; -use babylon_apis::finality_api::PubRandCommit; +use babylon_apis::finality_api::{Evidence, PubRandCommit}; use babylon_merkle::Proof; -use cosmwasm_std::{Deps, DepsMut, Env, Event, Response}; +use cosmwasm_std::{to_json_binary, Deps, DepsMut, Env, Event, Response, WasmMsg}; use k256::ecdsa::signature::Verifier; use k256::schnorr::{Signature, VerifyingKey}; use k256::sha2::{Digest, Sha256}; @@ -24,7 +26,7 @@ pub fn handle_public_randomness_commit( num_pub_rand: u64, commitment: &[u8], signature: &[u8], -) -> Result { +) -> Result, ContractError> { // Ensure the finality provider is registered check_fp_exist(deps.as_ref(), fp_pubkey_hex)?; @@ -111,14 +113,14 @@ pub(crate) fn verify_commitment_signature( #[allow(clippy::too_many_arguments)] pub fn handle_finality_signature( deps: DepsMut, - _env: Env, + env: Env, fp_btc_pk_hex: &str, height: u64, pub_rand: &[u8], proof: &Proof, block_hash: &[u8], signature: &[u8], -) -> Result { +) -> Result, ContractError> { // Ensure the finality provider exists check_fp_exist(deps.as_ref(), fp_btc_pk_hex)?; @@ -178,47 +180,45 @@ pub fn handle_finality_signature( // TODO?: Don't save public randomness values, to save storage space PUB_RAND_VALUES.save(deps.storage, (fp_btc_pk_hex, height), &pub_rand.to_vec())?; - // TODO: Verify whether the voted block is a fork or not - /* - indexedBlock, err := ms.GetBlock(ctx, req.BlockHeight) - if err != nil { - return nil, err - } - if !bytes.Equal(indexedBlock.AppHash, req.BlockAppHash) { - // the finality provider votes for a fork! + // Build the response + let mut res: Response = Response::new(); - // construct evidence - evidence := &types.Evidence{ - FpBtcPk: req.FpBtcPk, - BlockHeight: req.BlockHeight, - PubRand: req.PubRand, - CanonicalAppHash: indexedBlock.AppHash, - CanonicalFinalitySig: nil, - ForkAppHash: req.BlockAppHash, - ForkFinalitySig: signature, - } + // If this finality provider has signed the canonical block before, slash it via + // extracting its secret key, and emit an event + let canonical_sig: Option> = + SIGNATURES.may_load(deps.storage, (height, fp_btc_pk_hex))?; + let canonical_block_hash: Option> = + BLOCK_HASHES.may_load(deps.storage, (height, fp_btc_pk_hex))?; + if let (Some(canonical_sig), Some(canonical_block_hash)) = (canonical_sig, canonical_block_hash) + { + // the finality provider has voted for a fork before! + // If this evidence is at the same height as this signature, slash this finality provider - // if this finality provider has also signed canonical block, slash it - canonicalSig, err := ms.GetSig(ctx, req.BlockHeight, fpPK) - if err == nil { - //set canonical sig - evidence.CanonicalFinalitySig = canonicalSig - // slash this finality provider, including setting its voting power to - // zero, extracting its BTC SK, and emit an event - ms.slashFinalityProvider(ctx, req.FpBtcPk, evidence) - } + // construct evidence + let evidence = Evidence { + fp_btc_pk: hex::decode(fp_btc_pk_hex)?, + block_height: height, + pub_rand: pub_rand.to_vec(), + // TODO: we use block hash in place of app hash for now, to define new interface if needed + canonical_app_hash: canonical_block_hash, + canonical_finality_sig: canonical_sig, + fork_app_hash: block_hash.to_vec(), + fork_finality_sig: signature.to_vec(), + }; - // save evidence - ms.SetEvidence(ctx, evidence) + // set canonical sig to this evidence + EVIDENCES.save(deps.storage, (height, fp_btc_pk_hex), &evidence)?; - // NOTE: we should NOT return error here, otherwise the state change triggered in this tx - // (including the evidence) will be rolled back - return &types.MsgAddFinalitySigResponse{}, nil + // slash this finality provider, including setting its voting power to + // zero, extracting its BTC SK, and emit an event + let (msg, ev) = slash_finality_provider(&env, fp_btc_pk_hex, &evidence)?; + res = res.add_message(msg); + res = res.add_event(ev); } - */ // This signature is good, save the vote to the store SIGNATURES.save(deps.storage, (height, fp_btc_pk_hex), &signature.to_vec())?; + BLOCK_HASHES.save(deps.storage, (height, fp_btc_pk_hex), &block_hash.to_vec())?; // Check if the key (height, block_hash) exists let mut block_votes_fp_set = BLOCK_VOTES @@ -231,35 +231,14 @@ pub fn handle_finality_signature( // Save the updated set back to storage BLOCK_VOTES.save(deps.storage, (height, block_hash), &block_votes_fp_set)?; - // TODO: If this finality provider has signed the canonical block before, slash it via - // extracting its secret key, and emit an event - /* - if ms.HasEvidence(ctx, req.FpBtcPk, req.BlockHeight) { - // the finality provider has voted for a fork before! - // If this evidence is at the same height as this signature, slash this finality provider - - // get evidence - evidence, err := ms.GetEvidence(ctx, req.FpBtcPk, req.BlockHeight) - if err != nil { - panic(fmt.Errorf("failed to get evidence despite HasEvidence returns true")) - } - - // set canonical sig to this evidence - evidence.CanonicalFinalitySig = signature - ms.SetEvidence(ctx, evidence) - - // slash this finality provider, including setting its voting power to - // zero, extracting its BTC SK, and emit an event - ms.slashFinalityProvider(ctx, req.FpBtcPk, evidence) - } - */ - let event = Event::new("submit_finality_signature") .add_attribute("fp_pubkey_hex", fp_btc_pk_hex) .add_attribute("block_height", height.to_string()) .add_attribute("block_hash", hex::encode(block_hash)); - Ok(Response::new().add_event(event)) + res = res.add_event(event); + + Ok(res) } /// Verifies the finality signature message w.r.t. the public randomness commitment: @@ -328,13 +307,84 @@ fn check_fp_exist(deps: Deps, fp_pubkey_hex: &str) -> Result<(), ContractError> } } +/// `slash_finality_provider` slashes a finality provider with the given evidence including setting +/// its voting power to zero, extracting its BTC SK, and emitting an event +fn slash_finality_provider( + env: &Env, + fp_btc_pk_hex: &str, + evidence: &Evidence, +) -> Result<(WasmMsg, Event), ContractError> { + let pk = eots::PublicKey::from_hex(fp_btc_pk_hex)?; + let btc_sk = pk + .extract_secret_key( + &evidence.pub_rand, + &evidence.canonical_app_hash, + &evidence.canonical_finality_sig, + &evidence.fork_app_hash, + &evidence.fork_finality_sig, + ) + .map_err(|err| ContractError::SecretKeyExtractionError(err.to_string()))?; + + // Emit slashing event. + // Raises slashing event to babylon over IBC. + let msg = ExecuteMsg::Slashing { + evidence: evidence.clone(), + }; + let wasm_msg: WasmMsg = WasmMsg::Execute { + contract_addr: env.contract.address.to_string(), + msg: to_json_binary(&msg)?, + funds: vec![], + }; + + let ev = Event::new("slashed_finality_provider") + .add_attribute("module", "finality") + .add_attribute("finality_provider", fp_btc_pk_hex) + .add_attribute("block_height", evidence.block_height.to_string()) + .add_attribute( + "canonical_app_hash", + hex::encode(&evidence.canonical_app_hash), + ) + .add_attribute( + "canonical_finality_sig", + hex::encode(&evidence.canonical_finality_sig), + ) + .add_attribute("fork_app_hash", hex::encode(&evidence.fork_app_hash)) + .add_attribute( + "fork_finality_sig", + hex::encode(&evidence.fork_finality_sig), + ) + .add_attribute("secret_key", hex::encode(btc_sk.to_bytes())); + Ok((wasm_msg, ev)) +} + +pub(crate) fn handle_slashing(evidence: &Evidence) -> Result, ContractError> { + let mut res = Response::new(); + // Send msg to Babylon + + let msg = BabylonMsg::EquivocationEvidence { + evidence: Some(evidence.clone()), + }; + + // Convert to CosmosMsg + res = res + .add_message(msg) + .add_attribute("action", "equivocation_evidence"); + + Ok(res) +} + #[cfg(test)] pub(crate) mod tests { use super::*; + use cosmwasm_std::from_json; + use cosmwasm_std::testing::mock_env; + use std::collections::HashMap; use babylon_apis::finality_api::PubRandCommit; use hex::ToHex; - use test_utils::{get_add_finality_sig, get_pub_rand_commit, get_pub_rand_value}; + use test_utils::{ + get_add_finality_sig, get_add_finality_sig_2, get_pub_rand_commit, get_pub_rand_value, + }; /// Get public randomness public key, commitment, and signature information /// @@ -395,4 +445,86 @@ pub(crate) mod tests { ); assert!(res.is_ok()); } + + #[test] + fn verify_slashing_works() { + // Read test data + let (pk_hex, pub_rand, _) = get_public_randomness_commitment(); + let pub_rand_one = get_pub_rand_value(); + let add_finality_signature = get_add_finality_sig(); + let add_finality_signature_2 = get_add_finality_sig_2(); + let proof = add_finality_signature.proof.unwrap(); + + let initial_height = pub_rand.start_height; + let block_height = initial_height + proof.index.unsigned_abs(); + + // Create evidence struct + let evidence = Evidence { + fp_btc_pk: hex::decode(&pk_hex).unwrap(), + block_height, + pub_rand: pub_rand_one.to_vec(), + canonical_app_hash: add_finality_signature.block_app_hash.to_vec(), + canonical_finality_sig: add_finality_signature.finality_sig.to_vec(), + fork_app_hash: add_finality_signature_2.block_app_hash.to_vec(), + fork_finality_sig: add_finality_signature_2.finality_sig.to_vec(), + }; + + // Create mock environment + let env = mock_env(); // You'll need to add this mock helper + + // Test slash_finality_provider + let (wasm_msg, event) = slash_finality_provider(&env, &pk_hex, &evidence).unwrap(); + + // Verify the WasmMsg is correctly constructed + match wasm_msg { + WasmMsg::Execute { + contract_addr, + msg, + funds, + } => { + assert_eq!(contract_addr, env.contract.address.to_string()); + assert!(funds.is_empty()); + let msg_evidence = from_json::(&msg).unwrap(); + match msg_evidence { + ExecuteMsg::Slashing { + evidence: msg_evidence, + } => { + assert_eq!(evidence, msg_evidence); + } + _ => panic!("Expected Slashing msg"), + } + } + _ => panic!("Expected Execute msg"), + } + + // Verify the event attributes + assert_eq!(event.ty, "slashed_finality_provider"); + let attrs: HashMap<_, _> = event + .attributes + .iter() + .map(|a| (&a.key, &a.value)) + .collect(); + assert_eq!( + attrs.get(&"module".to_string()).unwrap().as_str(), + "finality" + ); + assert_eq!( + attrs + .get(&"finality_provider".to_string()) + .unwrap() + .as_str(), + &pk_hex + ); + assert_eq!( + attrs.get(&"block_height".to_string()).unwrap().as_str(), + &block_height.to_string() + ); + assert_eq!( + attrs + .get(&"canonical_app_hash".to_string()) + .unwrap() + .as_str(), + &hex::encode(&evidence.canonical_app_hash) + ); + } } diff --git a/contracts/op-finality-gadget/src/lib.rs b/contracts/op-finality-gadget/src/lib.rs index 9774c46f..cff1d9e9 100644 --- a/contracts/op-finality-gadget/src/lib.rs +++ b/contracts/op-finality-gadget/src/lib.rs @@ -1,3 +1,4 @@ +use babylon_bindings::BabylonMsg; use cosmwasm_std::{ entry_point, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, StdResult, }; @@ -18,7 +19,7 @@ pub fn instantiate( env: Env, info: MessageInfo, msg: InstantiateMsg, -) -> StdResult { +) -> StdResult> { contract::instantiate(deps, env, info, msg) } @@ -33,6 +34,6 @@ pub fn execute( env: Env, info: MessageInfo, msg: ExecuteMsg, -) -> Result { +) -> Result, ContractError> { contract::execute(deps, env, info, msg) } diff --git a/contracts/op-finality-gadget/src/msg.rs b/contracts/op-finality-gadget/src/msg.rs index fa6c6300..42436878 100644 --- a/contracts/op-finality-gadget/src/msg.rs +++ b/contracts/op-finality-gadget/src/msg.rs @@ -4,6 +4,7 @@ use { cw_controllers::AdminResponse, std::collections::HashSet, }; +use babylon_apis::finality_api::Evidence; use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::Binary; @@ -75,6 +76,12 @@ pub enum ExecuteMsg { block_hash: Binary, signature: Binary, }, + /// Slashing message. + /// + /// This message can be called by the admin only. + Slashing { + evidence: Evidence, + }, /// Enable or disable finality gadget. /// /// This message can be called by the admin only. @@ -91,3 +98,8 @@ pub enum ExecuteMsg { admin: String, }, } + +#[cw_serde] +pub struct FinalitySignatureResponse { + pub signature: Vec, +} diff --git a/contracts/op-finality-gadget/src/state/finality.rs b/contracts/op-finality-gadget/src/state/finality.rs index f3ebc5e7..645b0984 100644 --- a/contracts/op-finality-gadget/src/state/finality.rs +++ b/contracts/op-finality-gadget/src/state/finality.rs @@ -1,8 +1,15 @@ +use babylon_apis::finality_api::Evidence; use cw_storage_plus::Map; use std::collections::HashSet; /// Map of signatures by block height and fp pub(crate) const SIGNATURES: Map<(u64, &str), Vec> = Map::new("fp_sigs"); +/// Map of block hashes by block height and fp +pub(crate) const BLOCK_HASHES: Map<(u64, &str), Vec> = Map::new("block_hashes"); + /// Map of (block height, block hash) tuples to the list of fps that voted for this combination -pub(crate) const BLOCK_VOTES: Map<(u64, &[u8]), HashSet> = Map::new("block_hashes"); +pub(crate) const BLOCK_VOTES: Map<(u64, &[u8]), HashSet> = Map::new("block_votes"); + +/// Map of evidence by block height and fp +pub(crate) const EVIDENCES: Map<(u64, &str), Evidence> = Map::new("evidences"); diff --git a/packages/bindings-test/src/multitest.rs b/packages/bindings-test/src/multitest.rs index 64653de2..648bf82b 100644 --- a/packages/bindings-test/src/multitest.rs +++ b/packages/bindings-test/src/multitest.rs @@ -92,6 +92,10 @@ impl Module for BabylonModule { // FIXME? We don't do anything here Ok(AppResponse::default()) } + BabylonMsg::EquivocationEvidence { .. } => { + // FIXME? We don't do anything here + Ok(AppResponse::default()) + } } } diff --git a/packages/bindings/Cargo.toml b/packages/bindings/Cargo.toml index 79cbb957..8773ef05 100644 --- a/packages/bindings/Cargo.toml +++ b/packages/bindings/Cargo.toml @@ -11,3 +11,4 @@ license = "Apache-2.0" [dependencies] cosmwasm-std = { workspace = true } cosmwasm-schema = { workspace = true } +babylon-apis = { workspace = true } \ No newline at end of file diff --git a/packages/bindings/src/msg.rs b/packages/bindings/src/msg.rs index c05c963b..9cb09eb7 100644 --- a/packages/bindings/src/msg.rs +++ b/packages/bindings/src/msg.rs @@ -3,9 +3,9 @@ //! - ForkHeader: reporting a fork that has a valid quorum certificate //! - FinalizedHeader: reporting a BTC-finalised header. +use babylon_apis::finality_api::Evidence; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Coin, CosmosMsg, Empty}; - /// BabylonMsg is the message that the Babylon contract can send to the Cosmos zone. /// The Cosmos zone has to integrate https://github.com/babylonlabs-io/wasmbinding for /// handling these messages @@ -21,7 +21,13 @@ pub enum BabylonMsg { /// It can only be sent from the finality contract. /// The rewards are minted to the staking contract address, so that they /// can be distributed across the active finality provider set - MintRewards { amount: Coin, recipient: String }, + MintRewards { + amount: Coin, + recipient: String, + }, + EquivocationEvidence { + evidence: Option, + }, } pub type BabylonSudoMsg = Empty; diff --git a/packages/proto/babylon b/packages/proto/babylon index de84d881..6682eeca 160000 --- a/packages/proto/babylon +++ b/packages/proto/babylon @@ -1 +1 @@ -Subproject commit de84d8817da424189fa5110ac00c2e3dc1333949 +Subproject commit 6682eecad96e65c65edb7c257325ec52a65b81fa diff --git a/packages/proto/buf.gen.rust.yaml b/packages/proto/buf.gen.rust.yaml index 78d9715f..506a9004 100644 --- a/packages/proto/buf.gen.rust.yaml +++ b/packages/proto/buf.gen.rust.yaml @@ -23,6 +23,7 @@ types: - babylon.finality.v1.PubRandCommit - babylon.finality.v1.MsgAddFinalitySig - babylon.finality.v1.MsgCommitPubRandList + - babylon.finality.v1.MsgEquivocationEvidence plugins: - plugin: buf.build/community/neoeinstein-prost:v0.2.3 out: src/gen diff --git a/packages/proto/src/gen/babylon.finality.v1.rs b/packages/proto/src/gen/babylon.finality.v1.rs index 78451791..bdfd2418 100644 --- a/packages/proto/src/gen/babylon.finality.v1.rs +++ b/packages/proto/src/gen/babylon.finality.v1.rs @@ -105,4 +105,12 @@ pub struct MsgAddFinalitySig { #[prost(bytes="bytes", tag="7")] pub finality_sig: ::prost::bytes::Bytes, } +/// MsgEquivocationEvidence is the message for handling evidence of equivocation +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct MsgEquivocationEvidence { + /// evidence is the evidence of equivocation + #[prost(message, optional, tag="1")] + pub evidence: ::core::option::Option, +} // @@protoc_insertion_point(module) From 84130eb706e1fb60e16cecf9ce8ade94b66ffa62 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Mon, 16 Dec 2024 08:44:20 +0100 Subject: [PATCH 2/3] F/rewards distribution 2 (#97) Follow-up / Alternative to #95, in which the rewards are distributed over the (latest) active finality provider set --- contracts/btc-finality/src/contract.rs | 11 ++-- contracts/btc-finality/src/finality.rs | 55 ++++++++++++++++---- contracts/btc-finality/src/state/finality.rs | 12 +++-- 3 files changed, 60 insertions(+), 18 deletions(-) diff --git a/contracts/btc-finality/src/contract.rs b/contracts/btc-finality/src/contract.rs index 045e7913..9e86f060 100644 --- a/contracts/btc-finality/src/contract.rs +++ b/contracts/btc-finality/src/contract.rs @@ -13,7 +13,8 @@ use btc_staking::msg::ActivatedHeightResponse; use crate::error::ContractError; use crate::finality::{ - compute_active_finality_providers, handle_finality_signature, handle_public_randomness_commit, + compute_active_finality_providers, distribute_rewards, handle_finality_signature, + handle_public_randomness_commit, }; use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; use crate::state::config::{Config, ADMIN, CONFIG, PARAMS}; @@ -222,10 +223,14 @@ fn handle_update_staking( } fn handle_begin_block(deps: &mut DepsMut, env: Env) -> Result, ContractError> { + // Distribute rewards + distribute_rewards(deps, &env)?; + // Compute active finality provider set let max_active_fps = PARAMS.load(deps.storage)?.max_active_finality_providers as usize; - compute_active_finality_providers(deps, env, max_active_fps)?; + compute_active_finality_providers(deps, env.block.height, max_active_fps)?; + // TODO: Add events Ok(Response::new()) } @@ -245,7 +250,7 @@ fn handle_end_block( let ev = finality::index_block(deps, env.block.height, &hex::decode(app_hash_hex)?)?; res = res.add_event(ev); // Tally all non-finalised blocks - let (msg, events) = finality::tally_blocks(deps, activated_height, env.block.height)?; + let (msg, events) = finality::tally_blocks(deps, &env, activated_height)?; if let Some(msg) = msg { res = res.add_message(msg); } diff --git a/contracts/btc-finality/src/finality.rs b/contracts/btc-finality/src/finality.rs index 13f6cce6..2cf6ecf0 100644 --- a/contracts/btc-finality/src/finality.rs +++ b/contracts/btc-finality/src/finality.rs @@ -1,7 +1,9 @@ use crate::contract::encode_smart_query; use crate::error::ContractError; use crate::state::config::{Config, CONFIG, PARAMS}; -use crate::state::finality::{BLOCKS, EVIDENCES, FP_SET, NEXT_HEIGHT, SIGNATURES, TOTAL_POWER}; +use crate::state::finality::{ + BLOCKS, EVIDENCES, FP_SET, NEXT_HEIGHT, REWARDS, SIGNATURES, TOTAL_REWARDS, +}; use crate::state::public_randomness::{ get_last_pub_rand_commit, get_pub_rand_commit_for_height, PUB_RAND_COMMITS, PUB_RAND_VALUES, }; @@ -13,7 +15,7 @@ use btc_staking::msg::{FinalityProviderInfo, FinalityProvidersByPowerResponse}; use cosmwasm_std::Order::Ascending; use cosmwasm_std::{ to_json_binary, Addr, Coin, Decimal, DepsMut, Env, Event, QuerierWrapper, Response, StdResult, - Storage, WasmMsg, + Storage, Uint128, WasmMsg, }; use k256::ecdsa::signature::Verifier; use k256::schnorr::{Signature, VerifyingKey}; @@ -420,8 +422,8 @@ pub fn index_block( /// It must be invoked only after the BTC staking protocol is activated. pub fn tally_blocks( deps: &mut DepsMut, + env: &Env, activated_height: u64, - height: u64, ) -> Result<(Option, Vec), ContractError> { // Start finalising blocks since max(activated_height, next_height) let next_height = NEXT_HEIGHT.may_load(deps.storage)?.unwrap_or(0); @@ -438,7 +440,7 @@ pub fn tally_blocks( // non-finalisable let mut events = vec![]; let mut finalized_blocks = 0; - for h in start_height..=height { + for h in start_height..=env.block.height { let mut indexed_block = BLOCKS.load(deps.storage, h)?; // Get the finality provider set of this block let fp_set = FP_SET.may_load(deps.storage, h)?; @@ -490,7 +492,7 @@ pub fn tally_blocks( // Assemble mint message let mint_msg = BabylonMsg::MintRewards { amount: rewards, - recipient: cfg.staking.into(), + recipient: env.contract.address.to_string(), }; Some(mint_msg) } else { @@ -528,8 +530,6 @@ fn finalize_block( // Set the next height to finalise as height+1 NEXT_HEIGHT.save(store, &(block.height + 1))?; - // TODO: Distribute rewards to BTC staking delegators - // Record the last finalized height metric let ev = Event::new("finalize_block") .add_attribute("module", "finality") @@ -568,7 +568,7 @@ const QUERY_LIMIT: Option = Some(30); /// power of top finality providers, and records them in the contract state pub fn compute_active_finality_providers( deps: &mut DepsMut, - env: Env, + height: u64, max_active_fps: usize, ) -> Result<(), ContractError> { let cfg = CONFIG.load(deps.storage)?; @@ -602,9 +602,7 @@ pub fn compute_active_finality_providers( // 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(deps.storage, env.block.height, &finality_providers)?; - // Save the total voting power of the top n finality providers - TOTAL_POWER.save(deps.storage, &total_power)?; + FP_SET.save(deps.storage, height, &finality_providers)?; Ok(()) } @@ -622,3 +620,38 @@ pub fn list_fps_by_power( let res: FinalityProvidersByPowerResponse = querier.query(&query)?; Ok(res.fps) } + +/// `distribute_rewards` distributes rewards to finality providers who are in the active set at `height` +pub fn distribute_rewards(deps: &mut DepsMut, env: &Env) -> Result<(), ContractError> { + // Try to use the finality provider set at the previous height + let active_fps = FP_SET.may_load(deps.storage, env.block.height - 1)?; + // Short-circuit if there are no active finality providers + let active_fps = match active_fps { + Some(active_fps) => active_fps, + None => return Ok(()), + }; + // Get the voting power of the active FPS + let total_voting_power = active_fps.iter().map(|fp| fp.power as u128).sum::(); + // Get the rewards to distribute (bank balance of the finality contract) + let cfg = CONFIG.load(deps.storage)?; + let rewards_amount = deps + .querier + .query_balance(env.contract.address.clone(), cfg.denom)? + .amount; + // Compute the rewards for each active FP + let mut total_rewards = Uint128::zero(); + for fp in active_fps { + let reward = (Decimal::from_ratio(fp.power as u128, total_voting_power) + * Decimal::from_ratio(rewards_amount, 1u128)) + .to_uint_floor(); + // Update the rewards for this FP + REWARDS.update(deps.storage, &fp.btc_pk_hex, |r| { + Ok::(r.unwrap_or_default() + reward) + })?; + // Compute the total rewards + total_rewards += reward; + } + // Update the total rewards + TOTAL_REWARDS.save(deps.storage, &total_rewards)?; + Ok(()) +} diff --git a/contracts/btc-finality/src/state/finality.rs b/contracts/btc-finality/src/state/finality.rs index 06202abf..122916af 100644 --- a/contracts/btc-finality/src/state/finality.rs +++ b/contracts/btc-finality/src/state/finality.rs @@ -1,3 +1,5 @@ +use cosmwasm_std::Uint128; + use cw_storage_plus::{Item, Map}; use babylon_apis::finality_api::{Evidence, IndexedBlock}; @@ -15,9 +17,11 @@ pub const NEXT_HEIGHT: Item = Item::new("next_height"); /// `FP_SET` is the calculated list of the active finality providers by height pub const FP_SET: Map> = Map::new("fp_set"); -/// `TOTAL_POWER` is the total power of all finality providers -// FIXME: Store by height? Remove? Not currently being used in the contract -pub const TOTAL_POWER: Item = Item::new("total_power"); - /// Map of double signing evidence by FP and block height pub const EVIDENCES: Map<(&str, u64), Evidence> = Map::new("evidences"); + +/// Map of pending finality provider rewards +pub const REWARDS: Map<&str, Uint128> = Map::new("rewards"); + +/// Total pending rewards +pub const TOTAL_REWARDS: Item = Item::new("total_rewards"); From 3e9f03d6791498f4ebc58e970f20acd97e7306e2 Mon Sep 17 00:00:00 2001 From: parketh Date: Thu, 19 Dec 2024 07:01:34 +0000 Subject: [PATCH 3/3] feat: op consumer chain slashing (2/2) (#98) ## Summary This is a follow-on PR from https://github.com/babylonlabs-io/babylon-contract/pull/92 (implement slashing for op stack consumer chains) It updates the `EquivocationEvidence` msg to match the latest Babylon update ## Test plan ```bash # build the contract cargo build cargo optimize # test it cargo test cargo integration ``` --- contracts/op-finality-gadget/src/contract.rs | 3 +- .../op-finality-gadget/src/exec/finality.rs | 31 +++++++++++++---- contracts/op-finality-gadget/src/msg.rs | 3 +- packages/bindings/src/msg.rs | 33 +++++++++++++++---- 4 files changed, 55 insertions(+), 15 deletions(-) diff --git a/contracts/op-finality-gadget/src/contract.rs b/contracts/op-finality-gadget/src/contract.rs index 714604d7..f0a1d085 100644 --- a/contracts/op-finality-gadget/src/contract.rs +++ b/contracts/op-finality-gadget/src/contract.rs @@ -82,6 +82,7 @@ pub fn execute( } => handle_finality_signature( deps, env, + info, &fp_pubkey_hex, height, &pub_rand, @@ -89,7 +90,7 @@ pub fn execute( &block_hash, &signature, ), - ExecuteMsg::Slashing { evidence } => handle_slashing(&evidence), + ExecuteMsg::Slashing { sender, evidence } => handle_slashing(&sender, &evidence), ExecuteMsg::SetEnabled { enabled } => set_enabled(deps, info, enabled), ExecuteMsg::UpdateAdmin { admin } => ADMIN .execute_update_admin(deps, info, Some(api.addr_validate(&admin)?)) diff --git a/contracts/op-finality-gadget/src/exec/finality.rs b/contracts/op-finality-gadget/src/exec/finality.rs index 3a255014..d0e5f07c 100644 --- a/contracts/op-finality-gadget/src/exec/finality.rs +++ b/contracts/op-finality-gadget/src/exec/finality.rs @@ -13,7 +13,9 @@ use babylon_bindings::BabylonMsg; use babylon_apis::finality_api::{Evidence, PubRandCommit}; use babylon_merkle::Proof; -use cosmwasm_std::{to_json_binary, Deps, DepsMut, Env, Event, Response, WasmMsg}; +use cosmwasm_std::{ + to_json_binary, Addr, Deps, DepsMut, Env, Event, MessageInfo, Response, WasmMsg, +}; use k256::ecdsa::signature::Verifier; use k256::schnorr::{Signature, VerifyingKey}; use k256::sha2::{Digest, Sha256}; @@ -114,6 +116,7 @@ pub(crate) fn verify_commitment_signature( pub fn handle_finality_signature( deps: DepsMut, env: Env, + info: MessageInfo, fp_btc_pk_hex: &str, height: u64, pub_rand: &[u8], @@ -211,7 +214,7 @@ pub fn handle_finality_signature( // slash this finality provider, including setting its voting power to // zero, extracting its BTC SK, and emit an event - let (msg, ev) = slash_finality_provider(&env, fp_btc_pk_hex, &evidence)?; + let (msg, ev) = slash_finality_provider(&env, &info, fp_btc_pk_hex, &evidence)?; res = res.add_message(msg); res = res.add_event(ev); } @@ -311,6 +314,7 @@ fn check_fp_exist(deps: Deps, fp_pubkey_hex: &str) -> Result<(), ContractError> /// its voting power to zero, extracting its BTC SK, and emitting an event fn slash_finality_provider( env: &Env, + info: &MessageInfo, fp_btc_pk_hex: &str, evidence: &Evidence, ) -> Result<(WasmMsg, Event), ContractError> { @@ -328,6 +332,7 @@ fn slash_finality_provider( // Emit slashing event. // Raises slashing event to babylon over IBC. let msg = ExecuteMsg::Slashing { + sender: info.sender.clone(), evidence: evidence.clone(), }; let wasm_msg: WasmMsg = WasmMsg::Execute { @@ -357,12 +362,22 @@ fn slash_finality_provider( Ok((wasm_msg, ev)) } -pub(crate) fn handle_slashing(evidence: &Evidence) -> Result, ContractError> { +pub(crate) fn handle_slashing( + sender: &Addr, + evidence: &Evidence, +) -> Result, ContractError> { let mut res = Response::new(); // Send msg to Babylon let msg = BabylonMsg::EquivocationEvidence { - evidence: Some(evidence.clone()), + signer: sender.to_string(), + fp_btc_pk: evidence.fp_btc_pk.clone(), + block_height: evidence.block_height, + pub_rand: evidence.pub_rand.clone(), + canonical_app_hash: evidence.canonical_app_hash.clone(), + fork_app_hash: evidence.fork_app_hash.clone(), + canonical_finality_sig: evidence.canonical_finality_sig.clone(), + fork_finality_sig: evidence.fork_finality_sig.clone(), }; // Convert to CosmosMsg @@ -376,8 +391,9 @@ pub(crate) fn handle_slashing(evidence: &Evidence) -> Result(&msg).unwrap(); match msg_evidence { ExecuteMsg::Slashing { + sender: _, evidence: msg_evidence, } => { assert_eq!(evidence, msg_evidence); diff --git a/contracts/op-finality-gadget/src/msg.rs b/contracts/op-finality-gadget/src/msg.rs index 42436878..7b82a16d 100644 --- a/contracts/op-finality-gadget/src/msg.rs +++ b/contracts/op-finality-gadget/src/msg.rs @@ -6,7 +6,7 @@ use { use babylon_apis::finality_api::Evidence; use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::Binary; +use cosmwasm_std::{Addr, Binary}; use babylon_merkle::Proof; @@ -80,6 +80,7 @@ pub enum ExecuteMsg { /// /// This message can be called by the admin only. Slashing { + sender: Addr, evidence: Evidence, }, /// Enable or disable finality gadget. diff --git a/packages/bindings/src/msg.rs b/packages/bindings/src/msg.rs index 9cb09eb7..3b3c8d39 100644 --- a/packages/bindings/src/msg.rs +++ b/packages/bindings/src/msg.rs @@ -3,9 +3,9 @@ //! - ForkHeader: reporting a fork that has a valid quorum certificate //! - FinalizedHeader: reporting a BTC-finalised header. -use babylon_apis::finality_api::Evidence; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Coin, CosmosMsg, Empty}; + /// BabylonMsg is the message that the Babylon contract can send to the Cosmos zone. /// The Cosmos zone has to integrate https://github.com/babylonlabs-io/wasmbinding for /// handling these messages @@ -21,12 +21,33 @@ pub enum BabylonMsg { /// It can only be sent from the finality contract. /// The rewards are minted to the staking contract address, so that they /// can be distributed across the active finality provider set - MintRewards { - amount: Coin, - recipient: String, - }, + MintRewards { amount: Coin, recipient: String }, + /// EquivocationEvidence is the message that the Babylon contract sends to Babylon + /// to notify it of consumer chain slashing. EquivocationEvidence { - evidence: Option, + /// `signer` is the address submitting the evidence + signer: String, + /// `fp_btc_pk` is the BTC PK of the finality provider that casts this vote + fp_btc_pk: Vec, + /// `block_height` is the height of the conflicting blocks + block_height: u64, + /// `pub_rand is` the public randomness the finality provider has committed to. + /// Deserializes to `SchnorrPubRand` + pub_rand: Vec, + /// `canonical_app_hash` is the AppHash of the canonical block + canonical_app_hash: Vec, + /// `fork_app_hash` is the AppHash of the fork block + fork_app_hash: Vec, + /// `canonical_finality_sig` is the finality signature to the canonical block, + /// where finality signature is an EOTS signature, i.e., + /// the `s` in a Schnorr signature `(r, s)`. + /// `r` is the public randomness already committed by the finality provider. + /// Deserializes to `SchnorrEOTSSig` + canonical_finality_sig: Vec, + /// `fork_finality_sig` is the finality signature to the fork block, + /// where finality signature is an EOTS signature. + /// Deserializes to `SchnorrEOTSSig` + fork_finality_sig: Vec, }, }