diff --git a/Cargo.lock b/Cargo.lock index 7d7afa4c..28fea8c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -238,6 +238,7 @@ name = "babylon-bindings" version = "0.4.0" dependencies = [ "anybuf", + "babylon-apis", "cosmwasm-schema", "cosmwasm-std", ] @@ -2110,17 +2111,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/btc-finality/src/contract.rs b/contracts/btc-finality/src/contract.rs index b46d4e9e..f1802577 100644 --- a/contracts/btc-finality/src/contract.rs +++ b/contracts/btc-finality/src/contract.rs @@ -16,7 +16,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}; @@ -228,10 +229,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()) } @@ -251,7 +256,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 49207920..115250c6 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, }; @@ -14,7 +16,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}; @@ -425,8 +427,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); @@ -443,7 +445,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)?; @@ -535,8 +537,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") @@ -575,7 +575,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 params = get_babylon_sdk_params(&deps.querier)?; @@ -621,9 +621,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(()) } @@ -641,3 +639,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"); 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..f0a1d085 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 { @@ -79,6 +82,7 @@ pub fn execute( } => handle_finality_signature( deps, env, + info, &fp_pubkey_hex, height, &pub_rand, @@ -86,6 +90,7 @@ pub fn execute( &block_hash, &signature, ), + 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)?)) @@ -145,7 +150,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..d0e5f07c 100644 --- a/contracts/op-finality-gadget/src/exec/finality.rs +++ b/contracts/op-finality-gadget/src/exec/finality.rs @@ -1,17 +1,21 @@ 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, Addr, Deps, DepsMut, Env, Event, MessageInfo, Response, WasmMsg, +}; use k256::ecdsa::signature::Verifier; use k256::schnorr::{Signature, VerifyingKey}; use k256::sha2::{Digest, Sha256}; @@ -24,7 +28,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 +115,15 @@ pub(crate) fn verify_commitment_signature( #[allow(clippy::too_many_arguments)] pub fn handle_finality_signature( deps: DepsMut, - _env: Env, + env: Env, + info: MessageInfo, 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 +183,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, &info, 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 +234,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 +310,97 @@ 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, + info: &MessageInfo, + 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 { + sender: info.sender.clone(), + 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( + sender: &Addr, + evidence: &Evidence, +) -> Result, ContractError> { + let mut res = Response::new(); + // Send msg to Babylon + + let msg = BabylonMsg::EquivocationEvidence { + 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 + res = res + .add_message(msg) + .add_attribute("action", "equivocation_evidence"); + + Ok(res) +} + #[cfg(test)] pub(crate) mod tests { use super::*; + use cosmwasm_std::testing::mock_env; + use cosmwasm_std::Addr; + use cosmwasm_std::{from_json, testing::message_info}; + 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 +461,87 @@ 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 + let info = message_info(&Addr::unchecked("test"), &[]); + // Test slash_finality_provider + let (wasm_msg, event) = slash_finality_provider(&env, &info, &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 { + sender: _, + 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..7b82a16d 100644 --- a/contracts/op-finality-gadget/src/msg.rs +++ b/contracts/op-finality-gadget/src/msg.rs @@ -4,8 +4,9 @@ use { cw_controllers::AdminResponse, std::collections::HashSet, }; +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; @@ -75,6 +76,13 @@ pub enum ExecuteMsg { block_hash: Binary, signature: Binary, }, + /// Slashing message. + /// + /// This message can be called by the admin only. + Slashing { + sender: Addr, + evidence: Evidence, + }, /// Enable or disable finality gadget. /// /// This message can be called by the admin only. @@ -91,3 +99,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 ffe285ea..5aea99e1 100644 --- a/packages/bindings/Cargo.toml +++ b/packages/bindings/Cargo.toml @@ -12,3 +12,4 @@ license = "Apache-2.0" anybuf = { workspace = true } 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..3b3c8d39 100644 --- a/packages/bindings/src/msg.rs +++ b/packages/bindings/src/msg.rs @@ -22,6 +22,33 @@ pub enum BabylonMsg { /// 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 }, + /// EquivocationEvidence is the message that the Babylon contract sends to Babylon + /// to notify it of consumer chain slashing. + EquivocationEvidence { + /// `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, + }, } 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)