diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ee4628d5..80510de9 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -26,6 +26,7 @@ jobs: path: |- artifacts/babylon_contract.wasm artifacts/btc_staking.wasm + artifacts/btc_finality.wasm artifacts/checksums.txt - name: Show data run: |- @@ -35,6 +36,7 @@ jobs: run: |- zip artifacts/babylon_contract.wasm.zip artifacts/babylon_contract.wasm zip artifacts/btc_staking.wasm.zip artifacts/btc_staking.wasm + zip artifacts/btc_finality.wasm.zip artifacts/btc_finality.wasm zip artifacts/op_finality_gadget.wasm.zip artifacts/op_finality_gadget.wasm - name: Create a Release id: create-release @@ -68,6 +70,16 @@ jobs: asset_path: ./artifacts/btc_staking.wasm.zip asset_name: btc_staking.wasm.zip asset_content_type: application/zip + - name: Upload btc_finality + id: upload-btc_finality + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ github.token }} + with: + upload_url: ${{ steps.create-release.outputs.upload_url }} + asset_path: ./artifacts/btc_finality.wasm.zip + asset_name: btc_finality.wasm.zip + asset_content_type: application/zip - name: Upload op_finality_gadget id: upload-op_finality_gadget uses: actions/upload-release-asset@v1 @@ -91,7 +103,8 @@ jobs: - name: Build and run schema generator run: |- (cd ./contracts/babylon && cargo run --bin schema) - (cd ./contracts/btc-staking && cargo run --bin btcstaking-schema) + (cd ./contracts/btc-staking && cargo run --bin btc-staking-schema) + (cd ./contracts/btc-finality && cargo run --bin btc-finality-schema) - name: Consolidate schemas run: |- mkdir -p ./schemas diff --git a/.github/workflows/local-tests.yml b/.github/workflows/local-tests.yml index 7e159659..12342b6a 100644 --- a/.github/workflows/local-tests.yml +++ b/.github/workflows/local-tests.yml @@ -13,5 +13,13 @@ jobs: image: rust:1.78.0 steps: - uses: actions/checkout@v4.1.0 - - name: Build contracts, check formats, and run unit tests (with full validation) - run: cargo test --lib --features full-validation + - name: Build contracts, check formats, and run unit tests + run: cargo test --lib + local-build-test-full-validation: + runs-on: ubuntu-latest + container: + image: rust:1.78.0 + steps: + - uses: actions/checkout@v4.1.0 + - name: Build contracts, check formats, and run unit tests (full validation) + run: cargo test --package btc-staking --lib --features full-validation diff --git a/.github/workflows/wasm-tests.yml b/.github/workflows/wasm-tests.yml index 5f8a7a56..b14644e1 100644 --- a/.github/workflows/wasm-tests.yml +++ b/.github/workflows/wasm-tests.yml @@ -25,3 +25,4 @@ jobs: path: |- artifacts/babylon_contract.wasm artifacts/btc_staking.wasm + artifacts/btc_finality.wasm diff --git a/Cargo.lock b/Cargo.lock index b384c651..8c15d6e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -160,6 +160,7 @@ dependencies = [ "babylon-bitcoin", "babylon-proto", "blst", + "btc-finality", "btc-staking", "cosmos-sdk-proto", "cosmwasm-schema", @@ -169,6 +170,7 @@ dependencies = [ "cw-multi-test", "cw-storage-plus", "cw-utils", + "cw2", "derivative", "hex", "ics23", @@ -377,6 +379,41 @@ dependencies = [ "syn_derive", ] +[[package]] +name = "btc-finality" +version = "0.9.0" +dependencies = [ + "anyhow", + "assert_matches", + "babylon-apis", + "babylon-bindings", + "babylon-bindings-test", + "babylon-bitcoin", + "babylon-btcstaking", + "babylon-contract", + "babylon-merkle", + "babylon-proto", + "bitcoin", + "btc-staking", + "cosmwasm-schema", + "cosmwasm-std", + "cosmwasm-vm", + "cw-controllers", + "cw-multi-test", + "cw-storage-plus", + "cw-utils", + "cw2", + "derivative", + "eots", + "hex", + "k256", + "pbjson-types", + "prost 0.11.9", + "tendermint-proto", + "test-utils", + "thiserror", +] + [[package]] name = "btc-staking" version = "0.9.0" diff --git a/contracts/babylon/Cargo.toml b/contracts/babylon/Cargo.toml index 12588565..7564e5d9 100644 --- a/contracts/babylon/Cargo.toml +++ b/contracts/babylon/Cargo.toml @@ -37,6 +37,7 @@ babylon-bitcoin = { path = "../../packages/bitcoin" } blst = { workspace = true } cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } +cw2 = { workspace = true } cw-storage-plus = { workspace = true } cw-utils = { workspace = true } hex = { workspace = true } @@ -49,6 +50,7 @@ ics23 = { workspace = true } [dev-dependencies] babylon-bindings-test = { path = "../../packages/bindings-test" } btc-staking = { path = "../btc-staking", features = [ "library" ] } +btc-finality = { path = "../btc-finality", features = [ "library" ] } test-utils = { path = "../../packages/test-utils" } cosmwasm-vm = { workspace = true } diff --git a/contracts/babylon/benches/main.rs b/contracts/babylon/benches/main.rs index 32675f05..644b2f36 100644 --- a/contracts/babylon/benches/main.rs +++ b/contracts/babylon/benches/main.rs @@ -51,6 +51,8 @@ pub fn setup_instance() -> Instance { notify_cosmos_zone: false, btc_staking_code_id: None, btc_staking_msg: None, + btc_finality_code_id: None, + btc_finality_msg: None, admin: None, }; let info = mock_info(CREATOR, &[]); diff --git a/contracts/babylon/schema/babylon-contract.json b/contracts/babylon/schema/babylon-contract.json index 56b3ad6c..b0914132 100644 --- a/contracts/babylon/schema/babylon-contract.json +++ b/contracts/babylon/schema/babylon-contract.json @@ -15,7 +15,7 @@ ], "properties": { "admin": { - "description": "If set, this will be the Wasm migration / upgrade admin of the BTC staking contract", + "description": "If set, this will be the Wasm migration / upgrade admin of the BTC staking contract and the BTC finality contract", "type": [ "string", "null" @@ -30,6 +30,26 @@ "format": "uint64", "minimum": 0.0 }, + "btc_finality_code_id": { + "description": "If set, this will instantiate a BTC finality contract", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "btc_finality_msg": { + "description": "If set, this will define the instantiation message for the BTC finality contract. This message is opaque to the Babylon contract, and depends on the specific finality contract being instantiated", + "anyOf": [ + { + "$ref": "#/definitions/Binary" + }, + { + "type": "null" + } + ] + }, "btc_staking_code_id": { "description": "If set, this will instantiate a BTC staking contract for BTC re-staking", "type": [ @@ -40,7 +60,7 @@ "minimum": 0.0 }, "btc_staking_msg": { - "description": "If set, this will define the instantiate message for the BTC staking contract. This message is opaque to the Babylon contract, and depends on the specific staking contract being instantiated", + "description": "If set, this will define the instantiation message for the BTC staking contract. This message is opaque to the Babylon contract, and depends on the specific staking contract being instantiated", "anyOf": [ { "$ref": "#/definitions/Binary" @@ -73,6 +93,7 @@ "$ref": "#/definitions/Network" }, "notify_cosmos_zone": { + "description": "notify_cosmos_zone indicates whether to send Cosmos zone messages notifying BTC-finalised headers. NOTE: If set to true, then the Cosmos zone needs to integrate the corresponding message handler as well", "type": "boolean" } }, @@ -1281,6 +1302,17 @@ "format": "uint64", "minimum": 0.0 }, + "btc_finality": { + "description": "If set, this stores a BTC finality contract used for BTC finality on the Consumer", + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + }, "btc_staking": { "description": "If set, this stores a BTC staking contract used for BTC re-staking", "anyOf": [ @@ -1315,6 +1347,7 @@ "$ref": "#/definitions/Network" }, "notify_cosmos_zone": { + "description": "notify_cosmos_zone indicates whether to send Cosmos zone messages notifying BTC-finalised headers. NOTE: if set to true, then the Cosmos zone needs to integrate the corresponding message handler as well", "type": "boolean" } }, diff --git a/contracts/babylon/schema/raw/instantiate.json b/contracts/babylon/schema/raw/instantiate.json index 108bee01..89440deb 100644 --- a/contracts/babylon/schema/raw/instantiate.json +++ b/contracts/babylon/schema/raw/instantiate.json @@ -11,7 +11,7 @@ ], "properties": { "admin": { - "description": "If set, this will be the Wasm migration / upgrade admin of the BTC staking contract", + "description": "If set, this will be the Wasm migration / upgrade admin of the BTC staking contract and the BTC finality contract", "type": [ "string", "null" @@ -26,6 +26,26 @@ "format": "uint64", "minimum": 0.0 }, + "btc_finality_code_id": { + "description": "If set, this will instantiate a BTC finality contract", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "btc_finality_msg": { + "description": "If set, this will define the instantiation message for the BTC finality contract. This message is opaque to the Babylon contract, and depends on the specific finality contract being instantiated", + "anyOf": [ + { + "$ref": "#/definitions/Binary" + }, + { + "type": "null" + } + ] + }, "btc_staking_code_id": { "description": "If set, this will instantiate a BTC staking contract for BTC re-staking", "type": [ @@ -36,7 +56,7 @@ "minimum": 0.0 }, "btc_staking_msg": { - "description": "If set, this will define the instantiate message for the BTC staking contract. This message is opaque to the Babylon contract, and depends on the specific staking contract being instantiated", + "description": "If set, this will define the instantiation message for the BTC staking contract. This message is opaque to the Babylon contract, and depends on the specific staking contract being instantiated", "anyOf": [ { "$ref": "#/definitions/Binary" @@ -69,6 +89,7 @@ "$ref": "#/definitions/Network" }, "notify_cosmos_zone": { + "description": "notify_cosmos_zone indicates whether to send Cosmos zone messages notifying BTC-finalised headers. NOTE: If set to true, then the Cosmos zone needs to integrate the corresponding message handler as well", "type": "boolean" } }, diff --git a/contracts/babylon/schema/raw/response_to_config.json b/contracts/babylon/schema/raw/response_to_config.json index e5c34497..f097e13b 100644 --- a/contracts/babylon/schema/raw/response_to_config.json +++ b/contracts/babylon/schema/raw/response_to_config.json @@ -23,6 +23,17 @@ "format": "uint64", "minimum": 0.0 }, + "btc_finality": { + "description": "If set, this stores a BTC finality contract used for BTC finality on the Consumer", + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + }, "btc_staking": { "description": "If set, this stores a BTC staking contract used for BTC re-staking", "anyOf": [ @@ -57,6 +68,7 @@ "$ref": "#/definitions/Network" }, "notify_cosmos_zone": { + "description": "notify_cosmos_zone indicates whether to send Cosmos zone messages notifying BTC-finalised headers. NOTE: if set to true, then the Cosmos zone needs to integrate the corresponding message handler as well", "type": "boolean" } }, diff --git a/contracts/babylon/src/contract.rs b/contracts/babylon/src/contract.rs index 2f6b3bf3..c5bc8830 100644 --- a/contracts/babylon/src/contract.rs +++ b/contracts/babylon/src/contract.rs @@ -1,18 +1,25 @@ -use crate::error::ContractError; use cosmwasm_std::{ to_json_binary, Addr, Binary, Deps, DepsMut, Empty, Env, MessageInfo, QueryResponse, Reply, Response, SubMsg, SubMsgResponse, WasmMsg, }; +use cw2::set_contract_version; use cw_utils::ParseReplyError; +use babylon_apis::{btc_staking_api, finality_api}; +use babylon_bindings::BabylonMsg; + +use crate::error::ContractError; use crate::ibc::{ibc_packet, IBC_CHANNEL}; use crate::msg::contract::{ContractMsg, ExecuteMsg, InstantiateMsg, QueryMsg}; use crate::queries; use crate::state::btc_light_client; use crate::state::config::{Config, CONFIG}; -use babylon_bindings::BabylonMsg; -const REPLY_ID_INSTANTIATE: u64 = 1; +pub const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); +pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +const REPLY_ID_INSTANTIATE_STAKING: u64 = 2; +const REPLY_ID_INSTANTIATE_FINALITY: u64 = 3; /// When we instantiate the Babylon contract, it will optionally instantiate a BTC staking /// contract – if its code id is provided – to work with it for BTC re-staking support, @@ -34,6 +41,7 @@ pub fn instantiate( checkpoint_finalization_timeout: msg.checkpoint_finalization_timeout, notify_cosmos_zone: msg.notify_cosmos_zone, btc_staking: None, // Will be set in `reply` if `btc_staking_code_id` is provided + btc_finality: None, // Will be set in `reply` if `btc_finality_code_id` is provided consumer_name: None, consumer_description: None, }; @@ -47,13 +55,38 @@ pub fn instantiate( // Instantiate BTC staking contract let init_msg = WasmMsg::Instantiate { - admin: msg.admin, + admin: msg.admin.clone(), code_id: btc_staking_code_id, msg: msg.btc_staking_msg.unwrap_or(Binary::from(b"{}")), funds: vec![], label: "BTC Staking".into(), }; - let init_msg = SubMsg::reply_on_success(init_msg, REPLY_ID_INSTANTIATE); + let init_msg = SubMsg::reply_on_success(init_msg, REPLY_ID_INSTANTIATE_STAKING); + + // Test code sets a channel, so that we can better approximate IBC in test code + #[cfg(any(test, feature = "library"))] + { + let channel = cosmwasm_std::testing::mock_ibc_channel( + "channel-123", + cosmwasm_std::IbcOrder::Ordered, + "babylon", + ); + IBC_CHANNEL.save(deps.storage, &channel)?; + } + + res = res.add_submessage(init_msg); + } + + if let Some(btc_finality_code_id) = msg.btc_finality_code_id { + // Instantiate BTC finality contract + let init_msg = WasmMsg::Instantiate { + admin: msg.admin, + code_id: btc_finality_code_id, + msg: msg.btc_finality_msg.unwrap_or(Binary::from(b"{}")), + funds: vec![], + label: "BTC Finality".into(), + }; + let init_msg = SubMsg::reply_on_success(init_msg, REPLY_ID_INSTANTIATE_FINALITY); res = res.add_submessage(init_msg); } @@ -61,6 +94,7 @@ pub fn instantiate( // Save the config after potentially updating it CONFIG.save(deps.storage, &cfg)?; + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; Ok(res) } @@ -70,46 +104,69 @@ pub fn reply( reply: Reply, ) -> Result, ContractError> { match reply.id { - REPLY_ID_INSTANTIATE => reply_init_callback(deps, reply.result.unwrap()), + REPLY_ID_INSTANTIATE_STAKING => reply_init_callback_staking(deps, reply.result.unwrap()), + REPLY_ID_INSTANTIATE_FINALITY => reply_init_finality_callback(deps, reply.result.unwrap()), _ => Err(ContractError::InvalidReplyId(reply.id)), } } -/// Store virtual BTC staking address -fn reply_init_callback( - deps: DepsMut, - reply: SubMsgResponse, -) -> Result, ContractError> { - // Try to get contract address from events in reply +/// Tries to get contract address from events in reply +fn reply_init_get_contract_address(reply: SubMsgResponse) -> Result { for event in reply.events { if event.ty == "instantiate" { for attr in event.attributes { if attr.key == "_contract_address" { - let btc_staking = Addr::unchecked(attr.value); - CONFIG.update(deps.storage, |mut cfg| { - cfg.btc_staking = Some(btc_staking.clone()); - Ok::<_, ContractError>(cfg) - })?; - return Ok(Response::new()); + return Ok(Addr::unchecked(attr.value)); } } } } - // Fall back to deprecated way of getting contract address from data - // TODO: Remove this if the method above works - // TODO: Use the new `msg_responses` field if / when available - // let init_data = parse_instantiate_response_data(&reply.data.unwrap())?; - // let btc_staking = Addr::unchecked(init_data.contract_address); - // CONFIG.update(deps.storage, |mut cfg| { - // cfg.btc_staking = Some(btc_staking.clone()); - // Ok::<_, ContractError>(cfg) - // })?; - // Ok(Response::new()) Err(ContractError::ParseReply(ParseReplyError::ParseFailure( "Cannot parse contract address".to_string(), ))) } +/// Store BTC staking address +fn reply_init_callback_staking( + deps: DepsMut, + reply: SubMsgResponse, +) -> Result, ContractError> { + // Try to get contract address from events in reply + let addr = reply_init_get_contract_address(reply)?; + CONFIG.update(deps.storage, |mut cfg| { + cfg.btc_staking = Some(addr); + Ok::<_, ContractError>(cfg) + })?; + Ok(Response::new()) +} + +/// Store BTC finality address +fn reply_init_finality_callback( + deps: DepsMut, + reply: SubMsgResponse, +) -> Result, ContractError> { + // Try to get contract address from events in reply + let finality_addr = reply_init_get_contract_address(reply)?; + CONFIG.update(deps.storage, |mut cfg| { + cfg.btc_finality = Some(finality_addr.clone()); + Ok::<_, ContractError>(cfg) + })?; + // Set the BTC staking contract address to the BTC finality contract + let cfg = CONFIG.load(deps.storage)?; + let msg = finality_api::ExecuteMsg::UpdateStaking { + staking: cfg + .btc_staking + .ok_or(ContractError::BtcStakingNotSet {})? + .to_string(), + }; + let wasm_msg = WasmMsg::Execute { + contract_addr: finality_addr.to_string(), + msg: to_json_binary(&msg)?, + funds: vec![], + }; + Ok(Response::new().add_message(wasm_msg)) +} + pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result { match msg { QueryMsg::Config {} => Ok(to_json_binary(&queries::config(deps)?)?), @@ -171,20 +228,46 @@ pub fn execute( Ok(Response::new()) } ExecuteMsg::Slashing { evidence } => { - // This is an internal routing message from the `btc-staking` contract + // This is an internal routing message from the `btc_finality` contract + let cfg = CONFIG.load(deps.storage)?; // Check sender - let btc_staking = CONFIG - .load(deps.storage)? - .btc_staking - .ok_or(ContractError::BtcStakingNotSet {})?; - if info.sender != btc_staking { + let btc_finality = cfg + .btc_finality + .ok_or(ContractError::BtcFinalityNotSet {})?; + if info.sender != btc_finality { return Err(ContractError::Unauthorized {}); } + // Send to the staking contract for processing + let mut res = Response::new(); + let btc_staking = cfg.btc_staking.ok_or(ContractError::BtcStakingNotSet {})?; + // Slashes this finality provider, i.e., sets its slashing height to the block height + // and its power to zero + let msg = btc_staking_api::ExecuteMsg::Slash { + fp_btc_pk_hex: hex::encode(evidence.fp_btc_pk.clone()), + }; + let wasm_msg = WasmMsg::Execute { + contract_addr: btc_staking.to_string(), + msg: to_json_binary(&msg)?, + funds: vec![], + }; + res = res.add_message(wasm_msg); + // Send over IBC to the Provider (Babylon) let channel = IBC_CHANNEL.load(deps.storage)?; - let msg = ibc_packet::slashing_msg(&env, &channel, &evidence)?; + let ibc_msg = ibc_packet::slashing_msg(&env, &channel, &evidence)?; + // Send packet only if we are IBC enabled + // TODO: send in test code when multi-test can handle it + #[cfg(not(any(test, feature = "library")))] + { + res = res.add_message(ibc_msg); + } + #[cfg(any(test, feature = "library"))] + { + let _ = ibc_msg; + } + // TODO: Add events - Ok(Response::new().add_message(msg)) + Ok(res) } } } @@ -217,6 +300,8 @@ mod tests { notify_cosmos_zone: false, btc_staking_code_id: None, btc_staking_msg: None, + btc_finality_code_id: None, + btc_finality_msg: None, admin: None, consumer_name: None, consumer_description: None, diff --git a/contracts/babylon/src/error.rs b/contracts/babylon/src/error.rs index 7b9e9fee..19a3f84f 100644 --- a/contracts/babylon/src/error.rs +++ b/contracts/babylon/src/error.rs @@ -34,6 +34,8 @@ pub enum ContractError { Unauthorized {}, #[error("The BTC staking contract is not set")] BtcStakingNotSet {}, + #[error("The BTC finality contract is not set")] + BtcFinalityNotSet {}, #[error("Invalid configuration: {msg}")] InvalidConfig { msg: String }, } diff --git a/contracts/babylon/src/ibc.rs b/contracts/babylon/src/ibc.rs index deb8fbfe..c2951632 100644 --- a/contracts/babylon/src/ibc.rs +++ b/contracts/babylon/src/ibc.rs @@ -434,6 +434,8 @@ mod tests { notify_cosmos_zone: false, btc_staking_code_id: None, btc_staking_msg: None, + btc_finality_code_id: None, + btc_finality_msg: None, admin: None, consumer_name: None, consumer_description: None, diff --git a/contracts/babylon/src/msg/contract.rs b/contracts/babylon/src/msg/contract.rs index 24d30b95..7093e56a 100644 --- a/contracts/babylon/src/msg/contract.rs +++ b/contracts/babylon/src/msg/contract.rs @@ -22,16 +22,25 @@ pub struct InstantiateMsg { pub babylon_tag: String, pub btc_confirmation_depth: u64, pub checkpoint_finalization_timeout: u64, - // notify_cosmos_zone indicates whether to send Cosmos zone messages notifying BTC-finalised headers - // NOTE: if set true, then the Cosmos zone needs to integrate the corresponding message handler as well + /// notify_cosmos_zone indicates whether to send Cosmos zone messages notifying BTC-finalised + /// headers. + /// NOTE: If set to true, then the Cosmos zone needs to integrate the corresponding message handler + /// as well pub notify_cosmos_zone: bool, /// If set, this will instantiate a BTC staking contract for BTC re-staking pub btc_staking_code_id: Option, - /// If set, this will define the instantiate message for the BTC staking contract. + /// If set, this will define the instantiation message for the BTC staking contract. /// This message is opaque to the Babylon contract, and depends on the specific staking contract /// being instantiated pub btc_staking_msg: Option, - /// If set, this will be the Wasm migration / upgrade admin of the BTC staking contract + /// If set, this will instantiate a BTC finality contract + pub btc_finality_code_id: Option, + /// If set, this will define the instantiation message for the BTC finality contract. + /// This message is opaque to the Babylon contract, and depends on the specific finality contract + /// being instantiated + pub btc_finality_msg: Option, + /// If set, this will be the Wasm migration / upgrade admin of the BTC staking contract and the + /// BTC finality contract pub admin: Option, /// Name of the consumer pub consumer_name: Option, diff --git a/contracts/babylon/src/multitest.rs b/contracts/babylon/src/multitest.rs index 51bdc8cd..44fc23ee 100644 --- a/contracts/babylon/src/multitest.rs +++ b/contracts/babylon/src/multitest.rs @@ -5,9 +5,9 @@ use suite::SuiteBuilder; // Some multi-test default settings // TODO: Replace these with their address generators -const CONTRACT0_ADDR: &str = "cosmwasm1uzyszmsnca8euusre35wuqj4el3hyj8jty84kwln7du5stwwxyns2z5hxp"; +const CONTRACT0_ADDR: &str = "cosmwasm19mfs8tl4s396u7vqw9rrnsmrrtca5r66p7v8jvwdxvjn3shcmllqupdgxu"; const CONTRACT1_ADDR: &str = "cosmwasm14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s8jef58"; -const TOKEN: &str = "TOKEN"; +const CONTRACT2_ADDR: &str = "cosmwasm1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrqt8utkp"; #[test] fn initialization() { @@ -24,11 +24,15 @@ fn initialization() { assert_eq!(config.checkpoint_finalization_timeout, 10); assert!(!config.notify_cosmos_zone); assert_eq!(config.btc_staking, Some(Addr::unchecked(CONTRACT1_ADDR))); + assert_eq!(config.btc_finality, Some(Addr::unchecked(CONTRACT2_ADDR))); // Check that the btc-staking contract was initialized correctly let btc_staking_config = suite.get_btc_staking_config(); assert_eq!(btc_staking_config.babylon, Addr::unchecked(CONTRACT0_ADDR)); - assert_eq!(btc_staking_config.denom, TOKEN); + + // Check that the btc-finality contract was initialized correctly + let btc_finality_config = suite.get_btc_finality_config(); + assert_eq!(btc_finality_config.babylon, Addr::unchecked(CONTRACT0_ADDR)); } mod instantiation { @@ -41,13 +45,11 @@ mod instantiation { // Confirm the btc-staking contract has been instantiated and set let config = suite.get_config(); assert_eq!(config.btc_staking, Some(Addr::unchecked(CONTRACT1_ADDR))); + // Confirm the btc-finality contract has been instantiated and set + assert_eq!(config.btc_finality, Some(Addr::unchecked(CONTRACT2_ADDR))); } } -mod btc_staking {} - -mod slashing {} - mod migration { use super::*; use cosmwasm_std::Empty; diff --git a/contracts/babylon/src/multitest/suite.rs b/contracts/babylon/src/multitest/suite.rs index b99dae9e..0c9b9929 100644 --- a/contracts/babylon/src/multitest/suite.rs +++ b/contracts/babylon/src/multitest/suite.rs @@ -1,5 +1,5 @@ use crate::msg::contract::{InstantiateMsg, QueryMsg}; -use crate::multitest::CONTRACT1_ADDR; +use crate::multitest::{CONTRACT1_ADDR, CONTRACT2_ADDR}; use crate::state::config::Config; use anyhow::Result as AnyResult; use babylon_bindings::BabylonMsg; @@ -18,6 +18,15 @@ fn contract_btc_staking() -> Box> { Box::new(contract) } +fn contract_btc_finality() -> Box> { + let contract = ContractWrapper::new( + btc_finality::contract::execute, + btc_finality::contract::instantiate, + btc_finality::contract::query, + ); + Box::new(contract) +} + fn contract_babylon() -> Box> { let contract = ContractWrapper::new(crate::execute, crate::instantiate, crate::query) .with_reply(crate::reply) @@ -52,8 +61,11 @@ impl SuiteBuilder { app.init_modules(|_router, _api, _storage| -> AnyResult<()> { Ok(()) }) .unwrap(); - let btc_staking_code_id = app.store_code(contract_btc_staking()); - let contract_code_id = app.store_code(contract_babylon()); + let btc_staking_code_id = + app.store_code_with_creator(owner.clone(), contract_btc_staking()); + let btc_finality_code_id = + app.store_code_with_creator(owner.clone(), contract_btc_finality()); + let contract_code_id = app.store_code_with_creator(owner.clone(), contract_babylon()); let contract = app .instantiate_contract( contract_code_id, @@ -66,6 +78,8 @@ impl SuiteBuilder { notify_cosmos_zone: false, btc_staking_code_id: Some(btc_staking_code_id), btc_staking_msg: None, + btc_finality_code_id: Some(btc_finality_code_id), + btc_finality_msg: None, admin: Some(owner.to_string()), consumer_name: Some("TestConsumer".to_string()), consumer_description: Some("Test Consumer Description".to_string()), @@ -119,6 +133,14 @@ impl Suite { .unwrap() } + #[track_caller] + pub fn get_btc_finality_config(&self) -> btc_finality::state::config::Config { + self.app + .wrap() + .query_wasm_smart(CONTRACT2_ADDR, &btc_finality::msg::QueryMsg::Config {}) + .unwrap() + } + pub fn migrate(&mut self, addr: &str, msg: Empty) -> AnyResult { self.app.migrate_contract( Addr::unchecked(addr), diff --git a/contracts/babylon/src/state/btc_light_client.rs b/contracts/babylon/src/state/btc_light_client.rs index db7e0e95..7526fb3c 100644 --- a/contracts/babylon/src/state/btc_light_client.rs +++ b/contracts/babylon/src/state/btc_light_client.rs @@ -357,6 +357,7 @@ pub(crate) mod tests { checkpoint_finalization_timeout: w as u64, notify_cosmos_zone: false, btc_staking: None, + btc_finality: None, consumer_name: None, consumer_description: None, }; diff --git a/contracts/babylon/src/state/config.rs b/contracts/babylon/src/state/config.rs index c0c2c726..87cb70c9 100644 --- a/contracts/babylon/src/state/config.rs +++ b/contracts/babylon/src/state/config.rs @@ -11,12 +11,15 @@ pub struct Config { pub babylon_tag: Vec, pub btc_confirmation_depth: u64, pub checkpoint_finalization_timeout: u64, - // notify_cosmos_zone indicates whether to send Cosmos zone messages notifying BTC-finalised headers - // NOTE: if set true, then the Cosmos zone needs to integrate the corresponding message handler as well + /// notify_cosmos_zone indicates whether to send Cosmos zone messages notifying BTC-finalised headers. + /// NOTE: if set to true, then the Cosmos zone needs to integrate the corresponding message + /// handler as well pub notify_cosmos_zone: bool, /// If set, this stores a BTC staking contract used for BTC re-staking pub btc_staking: Option, - /// Consumer name + /// If set, this stores a BTC finality contract used for BTC finality on the Consumer + pub btc_finality: Option, + /// Consumer name pub consumer_name: Option, /// Consumer description pub consumer_description: Option, diff --git a/contracts/babylon/tests/integration.rs b/contracts/babylon/tests/integration.rs index 56c71f07..dbe78dde 100644 --- a/contracts/babylon/tests/integration.rs +++ b/contracts/babylon/tests/integration.rs @@ -49,6 +49,8 @@ fn setup() -> Instance { notify_cosmos_zone: false, btc_staking_code_id: None, btc_staking_msg: None, + btc_finality_code_id: None, + btc_finality_msg: None, admin: None, }; let info = message_info(&Addr::unchecked(CREATOR), &[]); @@ -101,6 +103,8 @@ fn instantiate_works() { notify_cosmos_zone: false, btc_staking_code_id: None, btc_staking_msg: None, + btc_finality_code_id: None, + btc_finality_msg: None, admin: None, }; let info = message_info(&Addr::unchecked(CREATOR), &[]); diff --git a/contracts/btc-finality/.cargo/config.toml b/contracts/btc-finality/.cargo/config.toml new file mode 100644 index 00000000..3d69bbce --- /dev/null +++ b/contracts/btc-finality/.cargo/config.toml @@ -0,0 +1,4 @@ +[alias] +unit-test = "test --lib" +integration-test = "test --test integration" +schema = "run --bin btc-finality-schema" diff --git a/contracts/btc-finality/Cargo.toml b/contracts/btc-finality/Cargo.toml new file mode 100644 index 00000000..d213ed0b --- /dev/null +++ b/contracts/btc-finality/Cargo.toml @@ -0,0 +1,68 @@ +[package] +name = "btc-finality" +edition.workspace = true +version.workspace = true +license.workspace = true +repository.workspace = true +authors = ["Babylon Labs Ltd. "] +publish = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] +doctest = false + +[[bin]] +name = "btc-finality-schema" +path = "src/bin/schema.rs" +test = false + +[features] +# Add feature "cranelift" to default if you need 32 bit or ARM support +default = [] +# Use cranelift backend instead of singlepass. This is required for development on 32 bit or ARM machines. +cranelift = ["cosmwasm-vm/cranelift"] +# for quicker tests, cargo test --lib +library = [] + +[dependencies] +babylon-apis = { path = "../../packages/apis" } +babylon-bindings = { path = "../../packages/bindings" } +babylon-contract = { path = "../babylon", features = [ "library" ] } +babylon-merkle = { path = "../../packages/merkle" } +babylon-proto = { path = "../../packages/proto" } +babylon-btcstaking = { path = "../../packages/btcstaking" } +babylon-bitcoin = { path = "../../packages/bitcoin" } +btc-staking = { path = "../btc-staking", features = [ "library" ] } +eots = { path = "../../packages/eots" } + +bitcoin = { workspace = true } +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +cw2 = { workspace = true } +cw-storage-plus = { workspace = true } +cw-utils = { workspace = true } +derivative = { workspace = true } +hex = { workspace = true } +k256 = { workspace = true } +prost = { workspace = true } +thiserror = { workspace = true } +cw-controllers = { workspace = true } + +[dev-dependencies] +babylon-bindings-test = { path = "../../packages/bindings-test" } +babylon-proto = { path = "../../packages/proto" } +babylon-contract = { path = "../babylon", features = [ "library" ] } +btc-staking = { path = "../btc-staking", features = [ "library" ] } +test-utils = { path = "../../packages/test-utils" } + +cosmwasm-vm = { workspace = true } +cw-multi-test = { workspace = true } + +anyhow = { workspace = true } +assert_matches = { workspace = true } +derivative = { workspace = true } +pbjson-types = { workspace = true } +prost = { workspace = true } +tendermint-proto = { workspace = true } diff --git a/contracts/btc-finality/schema/btc-finality.json b/contracts/btc-finality/schema/btc-finality.json new file mode 100644 index 00000000..d9c8c88c --- /dev/null +++ b/contracts/btc-finality/schema/btc-finality.json @@ -0,0 +1,1809 @@ +{ + "contract_name": "btc-finality", + "contract_version": "0.9.0", + "idl_version": "1.0.0", + "instantiate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "properties": { + "admin": { + "type": [ + "string", + "null" + ] + }, + "params": { + "anyOf": [ + { + "$ref": "#/definitions/Params" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Network": { + "type": "string", + "enum": [ + "mainnet", + "testnet", + "signet", + "regtest" + ] + }, + "Params": { + "description": "Params define Consumer-selectable BTC staking parameters", + "type": "object", + "required": [ + "btc_network", + "covenant_pks", + "covenant_quorum", + "min_slashing_tx_fee_sat", + "slashing_address", + "slashing_rate" + ], + "properties": { + "btc_network": { + "$ref": "#/definitions/Network" + }, + "covenant_pks": { + "type": "array", + "items": { + "type": "string" + } + }, + "covenant_quorum": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "min_slashing_tx_fee_sat": { + "description": "`min_slashing_tx_fee_sat` is the minimum amount of tx fee (quantified in Satoshi) needed for the pre-signed slashing tx", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "slashing_address": { + "description": "`slashing_address` is the address that the slashed BTC goes to. The address is in string format on Bitcoin.", + "type": "string" + }, + "slashing_rate": { + "description": "`slashing_rate` determines the portion of the staked amount to be slashed, expressed as a decimal (e.g. 0.5 for 50%).", + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "execute": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "description": "btc_staking execution handlers", + "oneOf": [ + { + "description": "Change the admin", + "type": "object", + "required": [ + "update_admin" + ], + "properties": { + "update_admin": { + "type": "object", + "properties": { + "admin": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "BTC Staking operations", + "type": "object", + "required": [ + "btc_staking" + ], + "properties": { + "btc_staking": { + "type": "object", + "required": [ + "active_del", + "new_fp", + "slashed_del", + "unbonded_del" + ], + "properties": { + "active_del": { + "type": "array", + "items": { + "$ref": "#/definitions/ActiveBtcDelegation" + } + }, + "new_fp": { + "type": "array", + "items": { + "$ref": "#/definitions/NewFinalityProvider" + } + }, + "slashed_del": { + "type": "array", + "items": { + "$ref": "#/definitions/SlashedBtcDelegation" + } + }, + "unbonded_del": { + "type": "array", + "items": { + "$ref": "#/definitions/UnbondedBtcDelegation" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Slash finality provider staking power. Used by the babylon-contract only. The Babylon contract will call this message to set the finality provider's staking power to zero when the finality provider is found to be malicious by the finality contract.", + "type": "object", + "required": [ + "slash" + ], + "properties": { + "slash": { + "type": "object", + "required": [ + "fp_btc_pk_hex" + ], + "properties": { + "fp_btc_pk_hex": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ], + "definitions": { + "ActiveBtcDelegation": { + "description": "ActiveBTCDelegation is a message sent when a BTC delegation newly receives covenant signatures and thus becomes active", + "type": "object", + "required": [ + "btc_pk_hex", + "covenant_sigs", + "delegator_slashing_sig", + "end_height", + "fp_btc_pk_list", + "params_version", + "slashing_tx", + "staker_addr", + "staking_output_idx", + "staking_tx", + "start_height", + "total_sat", + "unbonding_time", + "undelegation_info" + ], + "properties": { + "btc_pk_hex": { + "description": "btc_pk_hex is the Bitcoin secp256k1 PK of the BTC delegator. The PK follows encoding in BIP-340 spec in hex format", + "type": "string" + }, + "covenant_sigs": { + "description": "covenant_sigs is a list of adaptor signatures on the slashing tx by each covenant member. It will be a part of the witness for the staking tx output.", + "type": "array", + "items": { + "$ref": "#/definitions/CovenantAdaptorSignatures" + } + }, + "delegator_slashing_sig": { + "description": "delegator_slashing_sig is the signature on the slashing tx by the delegator (i.e. SK corresponding to btc_pk) as string hex. It will be a part of the witness for the staking tx output.", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "end_height": { + "description": "end_height is the end height of the BTC delegation it is the end BTC height of the time-lock - w", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "fp_btc_pk_list": { + "description": "fp_btc_pk_list is the list of BIP-340 PKs of the finality providers that this BTC delegation delegates to", + "type": "array", + "items": { + "type": "string" + } + }, + "params_version": { + "description": "params version used to validate the delegation", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "slashing_tx": { + "description": "slashing_tx is the slashing tx", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "staker_addr": { + "description": "staker_addr is the address to receive rewards from BTC delegation", + "type": "string" + }, + "staking_output_idx": { + "description": "staking_output_idx is the index of the staking output in the staking tx", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "staking_tx": { + "description": "staking_tx is the staking tx", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "start_height": { + "description": "start_height is the start BTC height of the BTC delegation. It is the start BTC height of the time-lock", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "total_sat": { + "description": "total_sat is the total BTC stakes in this delegation, quantified in satoshi", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "unbonding_time": { + "description": "unbonding_time is used in unbonding output time-lock path and in slashing transactions change outputs", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "undelegation_info": { + "description": "undelegation_info is the undelegation info of this delegation.", + "allOf": [ + { + "$ref": "#/definitions/BtcUndelegationInfo" + } + ] + } + }, + "additionalProperties": false + }, + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + }, + "BtcUndelegationInfo": { + "description": "BTCUndelegationInfo provides all necessary info about the undelegation", + "type": "object", + "required": [ + "covenant_slashing_sigs", + "covenant_unbonding_sig_list", + "delegator_slashing_sig", + "delegator_unbonding_sig", + "slashing_tx", + "unbonding_tx" + ], + "properties": { + "covenant_slashing_sigs": { + "description": "covenant_slashing_sigs is a list of adaptor signatures on the unbonding slashing tx by each covenant member It will be a part of the witness for the staking tx output.", + "type": "array", + "items": { + "$ref": "#/definitions/CovenantAdaptorSignatures" + } + }, + "covenant_unbonding_sig_list": { + "description": "covenant_unbonding_sig_list is the list of signatures on the unbonding tx by covenant members", + "type": "array", + "items": { + "$ref": "#/definitions/SignatureInfo" + } + }, + "delegator_slashing_sig": { + "description": "delegator_slashing_sig is the signature on the slashing tx by the delegator (i.e. SK corresponding to btc_pk). It will be a part of the witness for the unbonding tx output.", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "delegator_unbonding_sig": { + "description": "delegator_unbonding_sig is the signature on the unbonding tx by the delegator (i.e. SK corresponding to btc_pk). It effectively proves that the delegator wants to unbond and thus Babylon will consider this BTC delegation unbonded. Delegator's BTC on Bitcoin will be unbonded after time-lock.", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "slashing_tx": { + "description": "slashing_tx is the unbonding slashing tx", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "unbonding_tx": { + "description": "unbonding_tx is the transaction which will transfer the funds from staking output to unbonding output. Unbonding output will usually have lower timelock than staking output.", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + } + }, + "additionalProperties": false + }, + "CovenantAdaptorSignatures": { + "description": "CovenantAdaptorSignatures is a list adaptor signatures signed by the covenant with different finality provider's public keys as encryption keys", + "type": "object", + "required": [ + "adaptor_sigs", + "cov_pk" + ], + "properties": { + "adaptor_sigs": { + "description": "adaptor_sigs is a list of adaptor signatures, each encrypted by a restaked BTC finality provider's public key", + "type": "array", + "items": { + "$ref": "#/definitions/Binary" + } + }, + "cov_pk": { + "description": "cov_pk is the public key of the covenant emulator, used as the public key of the adaptor signature", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + } + }, + "additionalProperties": false + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "FinalityProviderDescription": { + "type": "object", + "required": [ + "details", + "identity", + "moniker", + "security_contact", + "website" + ], + "properties": { + "details": { + "description": "details is the details of the finality provider", + "type": "string" + }, + "identity": { + "description": "identity is the identity of the finality provider", + "type": "string" + }, + "moniker": { + "description": "moniker is the name of the finality provider", + "type": "string" + }, + "security_contact": { + "description": "security_contact is the security contact of the finality provider", + "type": "string" + }, + "website": { + "description": "website is the website of the finality provider", + "type": "string" + } + }, + "additionalProperties": false + }, + "NewFinalityProvider": { + "type": "object", + "required": [ + "addr", + "btc_pk_hex", + "commission", + "consumer_id" + ], + "properties": { + "addr": { + "description": "addr is the bech32 address identifier of the finality provider", + "type": "string" + }, + "btc_pk_hex": { + "description": "btc_pk_hex is the Bitcoin secp256k1 PK of this finality provider the PK follows encoding in BIP-340 spec in hex format", + "type": "string" + }, + "commission": { + "description": "commission defines the commission rate of the finality provider.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "consumer_id": { + "description": "consumer_id is the ID of the consumer that the finality provider is operating on.", + "type": "string" + }, + "description": { + "description": "description defines the description terms for the finality provider", + "anyOf": [ + { + "$ref": "#/definitions/FinalityProviderDescription" + }, + { + "type": "null" + } + ] + }, + "pop": { + "description": "pop is the proof of possession of the babylon_pk and btc_pk", + "anyOf": [ + { + "$ref": "#/definitions/ProofOfPossessionBtc" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "ProofOfPossessionBtc": { + "description": "ProofOfPossessionBtc is the proof of possession that a Babylon secp256k1 secret key and a Bitcoin secp256k1 secret key are held by the same person", + "type": "object", + "required": [ + "btc_sig", + "btc_sig_type" + ], + "properties": { + "btc_sig": { + "description": "btc_sig is the signature generated via sign(sk_btc, babylon_sig) the signature follows encoding in either BIP-340 spec or BIP-322 spec", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "btc_sig_type": { + "description": "btc_sig_type indicates the type of btc_sig in the pop", + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "SignatureInfo": { + "description": "SignatureInfo is a BIP-340 signature together with its signer's BIP-340 PK", + "type": "object", + "required": [ + "pk", + "sig" + ], + "properties": { + "pk": { + "$ref": "#/definitions/Binary" + }, + "sig": { + "$ref": "#/definitions/Binary" + } + }, + "additionalProperties": false + }, + "SlashedBtcDelegation": { + "description": "SlashedBTCDelegation is a packet sent from Babylon to the Consumer chain about a slashed BTC delegation re-staked to >=1 of the Consumer chain's finality providers", + "type": "object", + "required": [ + "recovered_fp_btc_sk", + "staking_tx_hash" + ], + "properties": { + "recovered_fp_btc_sk": { + "description": "recovered_fp_btc_sk is the extracted BTC SK of the finality provider on this Consumer chain", + "type": "string" + }, + "staking_tx_hash": { + "description": "staking tx hash of the BTC delegation. It uniquely identifies a BTC delegation", + "type": "string" + } + }, + "additionalProperties": false + }, + "UnbondedBtcDelegation": { + "description": "UnbondedBTCDelegation is sent from Babylon to the Consumer chain upon an early unbonded BTC delegation", + "type": "object", + "required": [ + "staking_tx_hash", + "unbonding_tx_sig" + ], + "properties": { + "staking_tx_hash": { + "description": "staking tx hash of the BTC delegation. It uniquely identifies a BTC delegation", + "type": "string" + }, + "unbonding_tx_sig": { + "description": "unbonding_tx_sig is the signature on the unbonding tx signed by the BTC delegator It proves that the BTC delegator wants to unbond", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + } + }, + "additionalProperties": false + } + } + }, + "query": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "description": "`Config` returns the current configuration of the btc-staking contract", + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "`Params` returns the current Consumer-specific parameters of the btc-staking contract", + "type": "object", + "required": [ + "params" + ], + "properties": { + "params": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "`Admin` returns the current admin of the contract", + "type": "object", + "required": [ + "admin" + ], + "properties": { + "admin": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "`FinalityProvider` returns the finality provider by its BTC public key, in hex format", + "type": "object", + "required": [ + "finality_provider" + ], + "properties": { + "finality_provider": { + "type": "object", + "required": [ + "btc_pk_hex" + ], + "properties": { + "btc_pk_hex": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "`FinalityProviders` returns the list of registered finality providers\n\n`start_after` is the BTC public key of the FP to start after, or `None` to start from the beginning", + "type": "object", + "required": [ + "finality_providers" + ], + "properties": { + "finality_providers": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "`Delegation` returns delegation information by its staking tx hash, in hex format", + "type": "object", + "required": [ + "delegation" + ], + "properties": { + "delegation": { + "type": "object", + "required": [ + "staking_tx_hash_hex" + ], + "properties": { + "staking_tx_hash_hex": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "`Delegations` return the list of delegations\n\n`start_after` is the staking tx hash (in hex format) of the delegation to start after, or `None` to start from the beginning. `limit` is the maximum number of delegations to return. `active` is an optional filter to return only active delegations", + "type": "object", + "required": [ + "delegations" + ], + "properties": { + "delegations": { + "type": "object", + "properties": { + "active": { + "type": [ + "boolean", + "null" + ] + }, + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "`DelegationsByFP` returns the list of staking tx hashes (in hex format) corresponding to delegations, for a given finality provider.\n\n`btc_pk_hex` is the BTC public key of the finality provider, in hex format. The hashes are returned in hex format", + "type": "object", + "required": [ + "delegations_by_f_p" + ], + "properties": { + "delegations_by_f_p": { + "type": "object", + "required": [ + "btc_pk_hex" + ], + "properties": { + "btc_pk_hex": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "`FinalityProviderInfo` returns the finality provider information by its BTC public key, in hex format The information includes the aggregated power of the finality provider.\n\n`height` is the optional block height at which the power is being aggregated. If `height` is not provided, the latest aggregated power is returned", + "type": "object", + "required": [ + "finality_provider_info" + ], + "properties": { + "finality_provider_info": { + "type": "object", + "required": [ + "btc_pk_hex" + ], + "properties": { + "btc_pk_hex": { + "type": "string" + }, + "height": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "`FinalityProvidersByPower` returns the list of finality provider infos sorted by their aggregated power, in descending order.\n\n`start_after` is the BTC public key of the FP to start after, or `None` to start from the top", + "type": "object", + "required": [ + "finality_providers_by_power" + ], + "properties": { + "finality_providers_by_power": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "anyOf": [ + { + "$ref": "#/definitions/FinalityProviderInfo" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "`ActivatedHeight` returns the height at which the contract gets its first delegation, if any", + "type": "object", + "required": [ + "activated_height" + ], + "properties": { + "activated_height": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ], + "definitions": { + "FinalityProviderInfo": { + "type": "object", + "required": [ + "btc_pk_hex", + "power" + ], + "properties": { + "btc_pk_hex": { + "description": "`btc_pk_hex` is the Bitcoin secp256k1 PK of this finality provider. The PK follows encoding in BIP-340 spec in hex format", + "type": "string" + }, + "power": { + "description": "`power` is the aggregated power of this finality provider. The power is calculated based on the amount of BTC delegated to this finality provider", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } + }, + "migrate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", + "type": "object" + }, + "sudo": null, + "responses": { + "activated_height": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ActivatedHeightResponse", + "type": "object", + "required": [ + "height" + ], + "properties": { + "height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "admin": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AdminResponse", + "description": "Returned from Admin.query_admin()", + "type": "object", + "properties": { + "admin": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, + "config": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Config", + "description": "Config are Babylon-selectable BTC staking configuration", + "type": "object", + "required": [ + "babylon" + ], + "properties": { + "babylon": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + } + } + }, + "delegation": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ActiveBtcDelegation", + "description": "ActiveBTCDelegation is a message sent when a BTC delegation newly receives covenant signatures and thus becomes active", + "type": "object", + "required": [ + "btc_pk_hex", + "covenant_sigs", + "delegator_slashing_sig", + "end_height", + "fp_btc_pk_list", + "params_version", + "slashing_tx", + "staker_addr", + "staking_output_idx", + "staking_tx", + "start_height", + "total_sat", + "unbonding_time", + "undelegation_info" + ], + "properties": { + "btc_pk_hex": { + "description": "btc_pk_hex is the Bitcoin secp256k1 PK of the BTC delegator. The PK follows encoding in BIP-340 spec in hex format", + "type": "string" + }, + "covenant_sigs": { + "description": "covenant_sigs is a list of adaptor signatures on the slashing tx by each covenant member. It will be a part of the witness for the staking tx output.", + "type": "array", + "items": { + "$ref": "#/definitions/CovenantAdaptorSignatures" + } + }, + "delegator_slashing_sig": { + "description": "delegator_slashing_sig is the signature on the slashing tx by the delegator (i.e. SK corresponding to btc_pk) as string hex. It will be a part of the witness for the staking tx output.", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "end_height": { + "description": "end_height is the end height of the BTC delegation it is the end BTC height of the time-lock - w", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "fp_btc_pk_list": { + "description": "fp_btc_pk_list is the list of BIP-340 PKs of the finality providers that this BTC delegation delegates to", + "type": "array", + "items": { + "type": "string" + } + }, + "params_version": { + "description": "params version used to validate the delegation", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "slashing_tx": { + "description": "slashing_tx is the slashing tx", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "staker_addr": { + "description": "staker_addr is the address to receive rewards from BTC delegation", + "type": "string" + }, + "staking_output_idx": { + "description": "staking_output_idx is the index of the staking output in the staking tx", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "staking_tx": { + "description": "staking_tx is the staking tx", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "start_height": { + "description": "start_height is the start BTC height of the BTC delegation. It is the start BTC height of the time-lock", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "total_sat": { + "description": "total_sat is the total BTC stakes in this delegation, quantified in satoshi", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "unbonding_time": { + "description": "unbonding_time is used in unbonding output time-lock path and in slashing transactions change outputs", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "undelegation_info": { + "description": "undelegation_info is the undelegation info of this delegation.", + "allOf": [ + { + "$ref": "#/definitions/BtcUndelegationInfo" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + }, + "BtcUndelegationInfo": { + "description": "BTCUndelegationInfo provides all necessary info about the undelegation", + "type": "object", + "required": [ + "covenant_slashing_sigs", + "covenant_unbonding_sig_list", + "delegator_slashing_sig", + "delegator_unbonding_sig", + "slashing_tx", + "unbonding_tx" + ], + "properties": { + "covenant_slashing_sigs": { + "description": "covenant_slashing_sigs is a list of adaptor signatures on the unbonding slashing tx by each covenant member It will be a part of the witness for the staking tx output.", + "type": "array", + "items": { + "$ref": "#/definitions/CovenantAdaptorSignatures" + } + }, + "covenant_unbonding_sig_list": { + "description": "covenant_unbonding_sig_list is the list of signatures on the unbonding tx by covenant members", + "type": "array", + "items": { + "$ref": "#/definitions/SignatureInfo" + } + }, + "delegator_slashing_sig": { + "description": "delegator_slashing_sig is the signature on the slashing tx by the delegator (i.e. SK corresponding to btc_pk). It will be a part of the witness for the unbonding tx output.", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "delegator_unbonding_sig": { + "description": "delegator_unbonding_sig is the signature on the unbonding tx by the delegator (i.e. SK corresponding to btc_pk). It effectively proves that the delegator wants to unbond and thus Babylon will consider this BTC delegation unbonded. Delegator's BTC on Bitcoin will be unbonded after time-lock.", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "slashing_tx": { + "description": "slashing_tx is the unbonding slashing tx", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "unbonding_tx": { + "description": "unbonding_tx is the transaction which will transfer the funds from staking output to unbonding output. Unbonding output will usually have lower timelock than staking output.", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + } + }, + "additionalProperties": false + }, + "CovenantAdaptorSignatures": { + "description": "CovenantAdaptorSignatures is a list adaptor signatures signed by the covenant with different finality provider's public keys as encryption keys", + "type": "object", + "required": [ + "adaptor_sigs", + "cov_pk" + ], + "properties": { + "adaptor_sigs": { + "description": "adaptor_sigs is a list of adaptor signatures, each encrypted by a restaked BTC finality provider's public key", + "type": "array", + "items": { + "$ref": "#/definitions/Binary" + } + }, + "cov_pk": { + "description": "cov_pk is the public key of the covenant emulator, used as the public key of the adaptor signature", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + } + }, + "additionalProperties": false + }, + "SignatureInfo": { + "description": "SignatureInfo is a BIP-340 signature together with its signer's BIP-340 PK", + "type": "object", + "required": [ + "pk", + "sig" + ], + "properties": { + "pk": { + "$ref": "#/definitions/Binary" + }, + "sig": { + "$ref": "#/definitions/Binary" + } + }, + "additionalProperties": false + } + } + }, + "delegations": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "BtcDelegationsResponse", + "type": "object", + "required": [ + "delegations" + ], + "properties": { + "delegations": { + "type": "array", + "items": { + "$ref": "#/definitions/BtcDelegation" + } + } + }, + "additionalProperties": false, + "definitions": { + "BtcDelegation": { + "type": "object", + "required": [ + "btc_pk_hex", + "covenant_sigs", + "delegator_slashing_sig", + "end_height", + "fp_btc_pk_list", + "params_version", + "slashed", + "slashing_tx", + "staker_addr", + "staking_output_idx", + "staking_tx", + "start_height", + "total_sat", + "unbonding_time", + "undelegation_info" + ], + "properties": { + "btc_pk_hex": { + "description": "btc_pk_hex is the Bitcoin secp256k1 PK of the BTC delegator. The PK follows encoding in BIP-340 spec in hex format", + "type": "string" + }, + "covenant_sigs": { + "description": "covenant_sigs is a list of adaptor signatures on the slashing tx by each covenant member. It will be a part of the witness for the staking tx output.", + "type": "array", + "items": { + "$ref": "#/definitions/CovenantAdaptorSignatures" + } + }, + "delegator_slashing_sig": { + "description": "delegator_slashing_sig is the signature on the slashing tx by the delegator (i.e. SK corresponding to btc_pk) as string hex. It will be a part of the witness for the staking tx output.", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + } + }, + "end_height": { + "description": "end_height is the end height of the BTC delegation it is the end BTC height of the time-lock - w", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "fp_btc_pk_list": { + "description": "fp_btc_pk_list is the list of BIP-340 PKs of the finality providers that this BTC delegation delegates to", + "type": "array", + "items": { + "type": "string" + } + }, + "params_version": { + "description": "params version used to validate the delegation", + "type": "integer", + "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", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + } + }, + "staker_addr": { + "description": "staker_addr is the address to receive rewards from BTC delegation", + "type": "string" + }, + "staking_output_idx": { + "description": "staking_output_idx is the index of the staking output in the staking tx", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "staking_tx": { + "description": "staking_tx is the staking tx", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + } + }, + "start_height": { + "description": "start_height is the start BTC height of the BTC delegation. It is the start BTC height of the time-lock", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "total_sat": { + "description": "total_sat is the total BTC stakes in this delegation, quantified in satoshi", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "unbonding_time": { + "description": "unbonding_time is used in unbonding output time-lock path and in slashing transactions change outputs", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "undelegation_info": { + "description": "undelegation_info is the undelegation info of this delegation.", + "allOf": [ + { + "$ref": "#/definitions/BtcUndelegationInfo" + } + ] + } + }, + "additionalProperties": false + }, + "BtcUndelegationInfo": { + "type": "object", + "required": [ + "covenant_slashing_sigs", + "covenant_unbonding_sig_list", + "delegator_slashing_sig", + "delegator_unbonding_sig", + "slashing_tx", + "unbonding_tx" + ], + "properties": { + "covenant_slashing_sigs": { + "description": "covenant_slashing_sigs is a list of adaptor signatures on the unbonding slashing tx by each covenant member It will be a part of the witness for the staking tx output.", + "type": "array", + "items": { + "$ref": "#/definitions/CovenantAdaptorSignatures" + } + }, + "covenant_unbonding_sig_list": { + "description": "covenant_unbonding_sig_list is the list of signatures on the unbonding tx by covenant members", + "type": "array", + "items": { + "$ref": "#/definitions/SignatureInfo" + } + }, + "delegator_slashing_sig": { + "description": "delegator_slashing_sig is the signature on the slashing tx by the delegator (i.e. SK corresponding to btc_pk). It will be a part of the witness for the unbonding tx output.", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + } + }, + "delegator_unbonding_sig": { + "description": "delegator_unbonding_sig is the signature on the unbonding tx by the delegator (i.e. SK corresponding to btc_pk). It effectively proves that the delegator wants to unbond and thus Babylon will consider this BTC delegation unbonded. Delegator's BTC on Bitcoin will be unbonded after time-lock.", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + } + }, + "slashing_tx": { + "description": "slashing_tx is the unbonding slashing tx", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + } + }, + "unbonding_tx": { + "description": "unbonding_tx is the transaction which will transfer the funds from staking output to unbonding output. Unbonding output will usually have lower timelock than staking output.", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + } + } + }, + "additionalProperties": false + }, + "CovenantAdaptorSignatures": { + "type": "object", + "required": [ + "adaptor_sigs", + "cov_pk" + ], + "properties": { + "adaptor_sigs": { + "description": "adaptor_sigs is a list of adaptor signatures, each encrypted by a restaked BTC finality provider's public key", + "type": "array", + "items": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + } + } + }, + "cov_pk": { + "description": "cov_pk is the public key of the covenant emulator, used as the public key of the adaptor signature", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + } + } + }, + "additionalProperties": false + }, + "SignatureInfo": { + "type": "object", + "required": [ + "pk", + "sig" + ], + "properties": { + "pk": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + } + }, + "sig": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + } + } + }, + "additionalProperties": false + } + } + }, + "delegations_by_f_p": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "DelegationsByFPResponse", + "type": "object", + "required": [ + "hashes" + ], + "properties": { + "hashes": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "finality_provider": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "FinalityProvider", + "type": "object", + "required": [ + "addr", + "btc_pk_hex", + "commission", + "consumer_id", + "slashed_btc_height", + "slashed_height" + ], + "properties": { + "addr": { + "description": "addr is the bech32 address identifier of the finality provider", + "type": "string" + }, + "btc_pk_hex": { + "description": "btc_pk_hex is the Bitcoin secp256k1 PK of this finality provider the PK follows encoding in BIP-340 spec in hex format", + "type": "string" + }, + "commission": { + "description": "commission defines the commission rate of the finality provider.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "consumer_id": { + "description": "consumer_id is the ID of the consumer that the finality provider is operating on.", + "type": "string" + }, + "description": { + "description": "description defines the description terms for the finality provider", + "anyOf": [ + { + "$ref": "#/definitions/FinalityProviderDescription" + }, + { + "type": "null" + } + ] + }, + "pop": { + "description": "pop is the proof of possession of the babylon_pk and btc_pk", + "anyOf": [ + { + "$ref": "#/definitions/ProofOfPossessionBtc" + }, + { + "type": "null" + } + ] + }, + "slashed_btc_height": { + "description": "slashed_btc_height is the BTC height on which the finality provider is slashed", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "slashed_height": { + "description": "slashed_height is the height on which the finality provider is slashed", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "FinalityProviderDescription": { + "type": "object", + "required": [ + "details", + "identity", + "moniker", + "security_contact", + "website" + ], + "properties": { + "details": { + "description": "details is the details of the finality provider", + "type": "string" + }, + "identity": { + "description": "identity is the identity of the finality provider", + "type": "string" + }, + "moniker": { + "description": "moniker is the name of the finality provider", + "type": "string" + }, + "security_contact": { + "description": "security_contact is the security contact of the finality provider", + "type": "string" + }, + "website": { + "description": "website is the website of the finality provider", + "type": "string" + } + }, + "additionalProperties": false + }, + "ProofOfPossessionBtc": { + "description": "ProofOfPossessionBtc is the proof of possession that a Babylon secp256k1 secret key and a Bitcoin secp256k1 secret key are held by the same person", + "type": "object", + "required": [ + "btc_sig", + "btc_sig_type" + ], + "properties": { + "btc_sig": { + "description": "btc_sig is the signature generated via sign(sk_btc, babylon_sig) the signature follows encoding in either BIP-340 spec or BIP-322 spec", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "btc_sig_type": { + "description": "btc_sig_type indicates the type of btc_sig in the pop", + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + } + } + }, + "finality_provider_info": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "FinalityProviderInfo", + "type": "object", + "required": [ + "btc_pk_hex", + "power" + ], + "properties": { + "btc_pk_hex": { + "description": "`btc_pk_hex` is the Bitcoin secp256k1 PK of this finality provider. The PK follows encoding in BIP-340 spec in hex format", + "type": "string" + }, + "power": { + "description": "`power` is the aggregated power of this finality provider. The power is calculated based on the amount of BTC delegated to this finality provider", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "finality_providers": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "FinalityProvidersResponse", + "type": "object", + "required": [ + "fps" + ], + "properties": { + "fps": { + "type": "array", + "items": { + "$ref": "#/definitions/FinalityProvider" + } + } + }, + "additionalProperties": false, + "definitions": { + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "FinalityProvider": { + "type": "object", + "required": [ + "addr", + "btc_pk_hex", + "commission", + "consumer_id", + "slashed_btc_height", + "slashed_height" + ], + "properties": { + "addr": { + "description": "addr is the bech32 address identifier of the finality provider", + "type": "string" + }, + "btc_pk_hex": { + "description": "btc_pk_hex is the Bitcoin secp256k1 PK of this finality provider the PK follows encoding in BIP-340 spec in hex format", + "type": "string" + }, + "commission": { + "description": "commission defines the commission rate of the finality provider.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "consumer_id": { + "description": "consumer_id is the ID of the consumer that the finality provider is operating on.", + "type": "string" + }, + "description": { + "description": "description defines the description terms for the finality provider", + "anyOf": [ + { + "$ref": "#/definitions/FinalityProviderDescription" + }, + { + "type": "null" + } + ] + }, + "pop": { + "description": "pop is the proof of possession of the babylon_pk and btc_pk", + "anyOf": [ + { + "$ref": "#/definitions/ProofOfPossessionBtc" + }, + { + "type": "null" + } + ] + }, + "slashed_btc_height": { + "description": "slashed_btc_height is the BTC height on which the finality provider is slashed", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "slashed_height": { + "description": "slashed_height is the height on which the finality provider is slashed", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "FinalityProviderDescription": { + "type": "object", + "required": [ + "details", + "identity", + "moniker", + "security_contact", + "website" + ], + "properties": { + "details": { + "description": "details is the details of the finality provider", + "type": "string" + }, + "identity": { + "description": "identity is the identity of the finality provider", + "type": "string" + }, + "moniker": { + "description": "moniker is the name of the finality provider", + "type": "string" + }, + "security_contact": { + "description": "security_contact is the security contact of the finality provider", + "type": "string" + }, + "website": { + "description": "website is the website of the finality provider", + "type": "string" + } + }, + "additionalProperties": false + }, + "ProofOfPossessionBtc": { + "description": "ProofOfPossessionBtc is the proof of possession that a Babylon secp256k1 secret key and a Bitcoin secp256k1 secret key are held by the same person", + "type": "object", + "required": [ + "btc_sig", + "btc_sig_type" + ], + "properties": { + "btc_sig": { + "description": "btc_sig is the signature generated via sign(sk_btc, babylon_sig) the signature follows encoding in either BIP-340 spec or BIP-322 spec", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "btc_sig_type": { + "description": "btc_sig_type indicates the type of btc_sig in the pop", + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + } + } + }, + "finality_providers_by_power": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "FinalityProvidersByPowerResponse", + "type": "object", + "required": [ + "fps" + ], + "properties": { + "fps": { + "type": "array", + "items": { + "$ref": "#/definitions/FinalityProviderInfo" + } + } + }, + "additionalProperties": false, + "definitions": { + "FinalityProviderInfo": { + "type": "object", + "required": [ + "btc_pk_hex", + "power" + ], + "properties": { + "btc_pk_hex": { + "description": "`btc_pk_hex` is the Bitcoin secp256k1 PK of this finality provider. The PK follows encoding in BIP-340 spec in hex format", + "type": "string" + }, + "power": { + "description": "`power` is the aggregated power of this finality provider. The power is calculated based on the amount of BTC delegated to this finality provider", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } + }, + "params": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Params", + "description": "Params define Consumer-selectable BTC staking parameters", + "type": "object", + "required": [ + "btc_network", + "covenant_pks", + "covenant_quorum", + "min_slashing_tx_fee_sat", + "slashing_address", + "slashing_rate" + ], + "properties": { + "btc_network": { + "$ref": "#/definitions/Network" + }, + "covenant_pks": { + "type": "array", + "items": { + "type": "string" + } + }, + "covenant_quorum": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "min_slashing_tx_fee_sat": { + "description": "`min_slashing_tx_fee_sat` is the minimum amount of tx fee (quantified in Satoshi) needed for the pre-signed slashing tx", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "slashing_address": { + "description": "`slashing_address` is the address that the slashed BTC goes to. The address is in string format on Bitcoin.", + "type": "string" + }, + "slashing_rate": { + "description": "`slashing_rate` determines the portion of the staked amount to be slashed, expressed as a decimal (e.g. 0.5 for 50%).", + "type": "string" + } + }, + "additionalProperties": false, + "definitions": { + "Network": { + "type": "string", + "enum": [ + "mainnet", + "testnet", + "signet", + "regtest" + ] + } + } + } + } +} diff --git a/contracts/btc-finality/schema/raw/execute.json b/contracts/btc-finality/schema/raw/execute.json new file mode 100644 index 00000000..11b583e7 --- /dev/null +++ b/contracts/btc-finality/schema/raw/execute.json @@ -0,0 +1,477 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "description": "btc_staking execution handlers", + "oneOf": [ + { + "description": "Change the admin", + "type": "object", + "required": [ + "update_admin" + ], + "properties": { + "update_admin": { + "type": "object", + "properties": { + "admin": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "BTC Staking operations", + "type": "object", + "required": [ + "btc_staking" + ], + "properties": { + "btc_staking": { + "type": "object", + "required": [ + "active_del", + "new_fp", + "slashed_del", + "unbonded_del" + ], + "properties": { + "active_del": { + "type": "array", + "items": { + "$ref": "#/definitions/ActiveBtcDelegation" + } + }, + "new_fp": { + "type": "array", + "items": { + "$ref": "#/definitions/NewFinalityProvider" + } + }, + "slashed_del": { + "type": "array", + "items": { + "$ref": "#/definitions/SlashedBtcDelegation" + } + }, + "unbonded_del": { + "type": "array", + "items": { + "$ref": "#/definitions/UnbondedBtcDelegation" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Slash finality provider staking power. Used by the babylon-contract only. The Babylon contract will call this message to set the finality provider's staking power to zero when the finality provider is found to be malicious by the finality contract.", + "type": "object", + "required": [ + "slash" + ], + "properties": { + "slash": { + "type": "object", + "required": [ + "fp_btc_pk_hex" + ], + "properties": { + "fp_btc_pk_hex": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ], + "definitions": { + "ActiveBtcDelegation": { + "description": "ActiveBTCDelegation is a message sent when a BTC delegation newly receives covenant signatures and thus becomes active", + "type": "object", + "required": [ + "btc_pk_hex", + "covenant_sigs", + "delegator_slashing_sig", + "end_height", + "fp_btc_pk_list", + "params_version", + "slashing_tx", + "staker_addr", + "staking_output_idx", + "staking_tx", + "start_height", + "total_sat", + "unbonding_time", + "undelegation_info" + ], + "properties": { + "btc_pk_hex": { + "description": "btc_pk_hex is the Bitcoin secp256k1 PK of the BTC delegator. The PK follows encoding in BIP-340 spec in hex format", + "type": "string" + }, + "covenant_sigs": { + "description": "covenant_sigs is a list of adaptor signatures on the slashing tx by each covenant member. It will be a part of the witness for the staking tx output.", + "type": "array", + "items": { + "$ref": "#/definitions/CovenantAdaptorSignatures" + } + }, + "delegator_slashing_sig": { + "description": "delegator_slashing_sig is the signature on the slashing tx by the delegator (i.e. SK corresponding to btc_pk) as string hex. It will be a part of the witness for the staking tx output.", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "end_height": { + "description": "end_height is the end height of the BTC delegation it is the end BTC height of the time-lock - w", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "fp_btc_pk_list": { + "description": "fp_btc_pk_list is the list of BIP-340 PKs of the finality providers that this BTC delegation delegates to", + "type": "array", + "items": { + "type": "string" + } + }, + "params_version": { + "description": "params version used to validate the delegation", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "slashing_tx": { + "description": "slashing_tx is the slashing tx", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "staker_addr": { + "description": "staker_addr is the address to receive rewards from BTC delegation", + "type": "string" + }, + "staking_output_idx": { + "description": "staking_output_idx is the index of the staking output in the staking tx", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "staking_tx": { + "description": "staking_tx is the staking tx", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "start_height": { + "description": "start_height is the start BTC height of the BTC delegation. It is the start BTC height of the time-lock", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "total_sat": { + "description": "total_sat is the total BTC stakes in this delegation, quantified in satoshi", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "unbonding_time": { + "description": "unbonding_time is used in unbonding output time-lock path and in slashing transactions change outputs", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "undelegation_info": { + "description": "undelegation_info is the undelegation info of this delegation.", + "allOf": [ + { + "$ref": "#/definitions/BtcUndelegationInfo" + } + ] + } + }, + "additionalProperties": false + }, + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + }, + "BtcUndelegationInfo": { + "description": "BTCUndelegationInfo provides all necessary info about the undelegation", + "type": "object", + "required": [ + "covenant_slashing_sigs", + "covenant_unbonding_sig_list", + "delegator_slashing_sig", + "delegator_unbonding_sig", + "slashing_tx", + "unbonding_tx" + ], + "properties": { + "covenant_slashing_sigs": { + "description": "covenant_slashing_sigs is a list of adaptor signatures on the unbonding slashing tx by each covenant member It will be a part of the witness for the staking tx output.", + "type": "array", + "items": { + "$ref": "#/definitions/CovenantAdaptorSignatures" + } + }, + "covenant_unbonding_sig_list": { + "description": "covenant_unbonding_sig_list is the list of signatures on the unbonding tx by covenant members", + "type": "array", + "items": { + "$ref": "#/definitions/SignatureInfo" + } + }, + "delegator_slashing_sig": { + "description": "delegator_slashing_sig is the signature on the slashing tx by the delegator (i.e. SK corresponding to btc_pk). It will be a part of the witness for the unbonding tx output.", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "delegator_unbonding_sig": { + "description": "delegator_unbonding_sig is the signature on the unbonding tx by the delegator (i.e. SK corresponding to btc_pk). It effectively proves that the delegator wants to unbond and thus Babylon will consider this BTC delegation unbonded. Delegator's BTC on Bitcoin will be unbonded after time-lock.", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "slashing_tx": { + "description": "slashing_tx is the unbonding slashing tx", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "unbonding_tx": { + "description": "unbonding_tx is the transaction which will transfer the funds from staking output to unbonding output. Unbonding output will usually have lower timelock than staking output.", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + } + }, + "additionalProperties": false + }, + "CovenantAdaptorSignatures": { + "description": "CovenantAdaptorSignatures is a list adaptor signatures signed by the covenant with different finality provider's public keys as encryption keys", + "type": "object", + "required": [ + "adaptor_sigs", + "cov_pk" + ], + "properties": { + "adaptor_sigs": { + "description": "adaptor_sigs is a list of adaptor signatures, each encrypted by a restaked BTC finality provider's public key", + "type": "array", + "items": { + "$ref": "#/definitions/Binary" + } + }, + "cov_pk": { + "description": "cov_pk is the public key of the covenant emulator, used as the public key of the adaptor signature", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + } + }, + "additionalProperties": false + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "FinalityProviderDescription": { + "type": "object", + "required": [ + "details", + "identity", + "moniker", + "security_contact", + "website" + ], + "properties": { + "details": { + "description": "details is the details of the finality provider", + "type": "string" + }, + "identity": { + "description": "identity is the identity of the finality provider", + "type": "string" + }, + "moniker": { + "description": "moniker is the name of the finality provider", + "type": "string" + }, + "security_contact": { + "description": "security_contact is the security contact of the finality provider", + "type": "string" + }, + "website": { + "description": "website is the website of the finality provider", + "type": "string" + } + }, + "additionalProperties": false + }, + "NewFinalityProvider": { + "type": "object", + "required": [ + "addr", + "btc_pk_hex", + "commission", + "consumer_id" + ], + "properties": { + "addr": { + "description": "addr is the bech32 address identifier of the finality provider", + "type": "string" + }, + "btc_pk_hex": { + "description": "btc_pk_hex is the Bitcoin secp256k1 PK of this finality provider the PK follows encoding in BIP-340 spec in hex format", + "type": "string" + }, + "commission": { + "description": "commission defines the commission rate of the finality provider.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "consumer_id": { + "description": "consumer_id is the ID of the consumer that the finality provider is operating on.", + "type": "string" + }, + "description": { + "description": "description defines the description terms for the finality provider", + "anyOf": [ + { + "$ref": "#/definitions/FinalityProviderDescription" + }, + { + "type": "null" + } + ] + }, + "pop": { + "description": "pop is the proof of possession of the babylon_pk and btc_pk", + "anyOf": [ + { + "$ref": "#/definitions/ProofOfPossessionBtc" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "ProofOfPossessionBtc": { + "description": "ProofOfPossessionBtc is the proof of possession that a Babylon secp256k1 secret key and a Bitcoin secp256k1 secret key are held by the same person", + "type": "object", + "required": [ + "btc_sig", + "btc_sig_type" + ], + "properties": { + "btc_sig": { + "description": "btc_sig is the signature generated via sign(sk_btc, babylon_sig) the signature follows encoding in either BIP-340 spec or BIP-322 spec", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "btc_sig_type": { + "description": "btc_sig_type indicates the type of btc_sig in the pop", + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "SignatureInfo": { + "description": "SignatureInfo is a BIP-340 signature together with its signer's BIP-340 PK", + "type": "object", + "required": [ + "pk", + "sig" + ], + "properties": { + "pk": { + "$ref": "#/definitions/Binary" + }, + "sig": { + "$ref": "#/definitions/Binary" + } + }, + "additionalProperties": false + }, + "SlashedBtcDelegation": { + "description": "SlashedBTCDelegation is a packet sent from Babylon to the Consumer chain about a slashed BTC delegation re-staked to >=1 of the Consumer chain's finality providers", + "type": "object", + "required": [ + "recovered_fp_btc_sk", + "staking_tx_hash" + ], + "properties": { + "recovered_fp_btc_sk": { + "description": "recovered_fp_btc_sk is the extracted BTC SK of the finality provider on this Consumer chain", + "type": "string" + }, + "staking_tx_hash": { + "description": "staking tx hash of the BTC delegation. It uniquely identifies a BTC delegation", + "type": "string" + } + }, + "additionalProperties": false + }, + "UnbondedBtcDelegation": { + "description": "UnbondedBTCDelegation is sent from Babylon to the Consumer chain upon an early unbonded BTC delegation", + "type": "object", + "required": [ + "staking_tx_hash", + "unbonding_tx_sig" + ], + "properties": { + "staking_tx_hash": { + "description": "staking tx hash of the BTC delegation. It uniquely identifies a BTC delegation", + "type": "string" + }, + "unbonding_tx_sig": { + "description": "unbonding_tx_sig is the signature on the unbonding tx signed by the BTC delegator It proves that the BTC delegator wants to unbond", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/btc-finality/schema/raw/instantiate.json b/contracts/btc-finality/schema/raw/instantiate.json new file mode 100644 index 00000000..a771d00c --- /dev/null +++ b/contracts/btc-finality/schema/raw/instantiate.json @@ -0,0 +1,78 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "properties": { + "admin": { + "type": [ + "string", + "null" + ] + }, + "params": { + "anyOf": [ + { + "$ref": "#/definitions/Params" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Network": { + "type": "string", + "enum": [ + "mainnet", + "testnet", + "signet", + "regtest" + ] + }, + "Params": { + "description": "Params define Consumer-selectable BTC staking parameters", + "type": "object", + "required": [ + "btc_network", + "covenant_pks", + "covenant_quorum", + "min_slashing_tx_fee_sat", + "slashing_address", + "slashing_rate" + ], + "properties": { + "btc_network": { + "$ref": "#/definitions/Network" + }, + "covenant_pks": { + "type": "array", + "items": { + "type": "string" + } + }, + "covenant_quorum": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "min_slashing_tx_fee_sat": { + "description": "`min_slashing_tx_fee_sat` is the minimum amount of tx fee (quantified in Satoshi) needed for the pre-signed slashing tx", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "slashing_address": { + "description": "`slashing_address` is the address that the slashed BTC goes to. The address is in string format on Bitcoin.", + "type": "string" + }, + "slashing_rate": { + "description": "`slashing_rate` determines the portion of the staked amount to be slashed, expressed as a decimal (e.g. 0.5 for 50%).", + "type": "string" + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/btc-finality/schema/raw/migrate.json b/contracts/btc-finality/schema/raw/migrate.json new file mode 100644 index 00000000..2bba9582 --- /dev/null +++ b/contracts/btc-finality/schema/raw/migrate.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", + "type": "object" +} diff --git a/contracts/btc-finality/schema/raw/query.json b/contracts/btc-finality/schema/raw/query.json new file mode 100644 index 00000000..891e0773 --- /dev/null +++ b/contracts/btc-finality/schema/raw/query.json @@ -0,0 +1,280 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "description": "`Config` returns the current configuration of the btc-staking contract", + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "`Params` returns the current Consumer-specific parameters of the btc-staking contract", + "type": "object", + "required": [ + "params" + ], + "properties": { + "params": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "`Admin` returns the current admin of the contract", + "type": "object", + "required": [ + "admin" + ], + "properties": { + "admin": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "`FinalityProvider` returns the finality provider by its BTC public key, in hex format", + "type": "object", + "required": [ + "finality_provider" + ], + "properties": { + "finality_provider": { + "type": "object", + "required": [ + "btc_pk_hex" + ], + "properties": { + "btc_pk_hex": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "`FinalityProviders` returns the list of registered finality providers\n\n`start_after` is the BTC public key of the FP to start after, or `None` to start from the beginning", + "type": "object", + "required": [ + "finality_providers" + ], + "properties": { + "finality_providers": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "`Delegation` returns delegation information by its staking tx hash, in hex format", + "type": "object", + "required": [ + "delegation" + ], + "properties": { + "delegation": { + "type": "object", + "required": [ + "staking_tx_hash_hex" + ], + "properties": { + "staking_tx_hash_hex": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "`Delegations` return the list of delegations\n\n`start_after` is the staking tx hash (in hex format) of the delegation to start after, or `None` to start from the beginning. `limit` is the maximum number of delegations to return. `active` is an optional filter to return only active delegations", + "type": "object", + "required": [ + "delegations" + ], + "properties": { + "delegations": { + "type": "object", + "properties": { + "active": { + "type": [ + "boolean", + "null" + ] + }, + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "`DelegationsByFP` returns the list of staking tx hashes (in hex format) corresponding to delegations, for a given finality provider.\n\n`btc_pk_hex` is the BTC public key of the finality provider, in hex format. The hashes are returned in hex format", + "type": "object", + "required": [ + "delegations_by_f_p" + ], + "properties": { + "delegations_by_f_p": { + "type": "object", + "required": [ + "btc_pk_hex" + ], + "properties": { + "btc_pk_hex": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "`FinalityProviderInfo` returns the finality provider information by its BTC public key, in hex format The information includes the aggregated power of the finality provider.\n\n`height` is the optional block height at which the power is being aggregated. If `height` is not provided, the latest aggregated power is returned", + "type": "object", + "required": [ + "finality_provider_info" + ], + "properties": { + "finality_provider_info": { + "type": "object", + "required": [ + "btc_pk_hex" + ], + "properties": { + "btc_pk_hex": { + "type": "string" + }, + "height": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "`FinalityProvidersByPower` returns the list of finality provider infos sorted by their aggregated power, in descending order.\n\n`start_after` is the BTC public key of the FP to start after, or `None` to start from the top", + "type": "object", + "required": [ + "finality_providers_by_power" + ], + "properties": { + "finality_providers_by_power": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "anyOf": [ + { + "$ref": "#/definitions/FinalityProviderInfo" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "`ActivatedHeight` returns the height at which the contract gets its first delegation, if any", + "type": "object", + "required": [ + "activated_height" + ], + "properties": { + "activated_height": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ], + "definitions": { + "FinalityProviderInfo": { + "type": "object", + "required": [ + "btc_pk_hex", + "power" + ], + "properties": { + "btc_pk_hex": { + "description": "`btc_pk_hex` is the Bitcoin secp256k1 PK of this finality provider. The PK follows encoding in BIP-340 spec in hex format", + "type": "string" + }, + "power": { + "description": "`power` is the aggregated power of this finality provider. The power is calculated based on the amount of BTC delegated to this finality provider", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/btc-finality/schema/raw/response_to_activated_height.json b/contracts/btc-finality/schema/raw/response_to_activated_height.json new file mode 100644 index 00000000..2854be08 --- /dev/null +++ b/contracts/btc-finality/schema/raw/response_to_activated_height.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ActivatedHeightResponse", + "type": "object", + "required": [ + "height" + ], + "properties": { + "height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false +} diff --git a/contracts/btc-finality/schema/raw/response_to_admin.json b/contracts/btc-finality/schema/raw/response_to_admin.json new file mode 100644 index 00000000..c73969ab --- /dev/null +++ b/contracts/btc-finality/schema/raw/response_to_admin.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AdminResponse", + "description": "Returned from Admin.query_admin()", + "type": "object", + "properties": { + "admin": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false +} diff --git a/contracts/btc-finality/schema/raw/response_to_config.json b/contracts/btc-finality/schema/raw/response_to_config.json new file mode 100644 index 00000000..6b405689 --- /dev/null +++ b/contracts/btc-finality/schema/raw/response_to_config.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Config", + "description": "Config are Babylon-selectable BTC staking configuration", + "type": "object", + "required": [ + "babylon" + ], + "properties": { + "babylon": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + } + } +} diff --git a/contracts/btc-finality/schema/raw/response_to_delegation.json b/contracts/btc-finality/schema/raw/response_to_delegation.json new file mode 100644 index 00000000..f20b76c1 --- /dev/null +++ b/contracts/btc-finality/schema/raw/response_to_delegation.json @@ -0,0 +1,225 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ActiveBtcDelegation", + "description": "ActiveBTCDelegation is a message sent when a BTC delegation newly receives covenant signatures and thus becomes active", + "type": "object", + "required": [ + "btc_pk_hex", + "covenant_sigs", + "delegator_slashing_sig", + "end_height", + "fp_btc_pk_list", + "params_version", + "slashing_tx", + "staker_addr", + "staking_output_idx", + "staking_tx", + "start_height", + "total_sat", + "unbonding_time", + "undelegation_info" + ], + "properties": { + "btc_pk_hex": { + "description": "btc_pk_hex is the Bitcoin secp256k1 PK of the BTC delegator. The PK follows encoding in BIP-340 spec in hex format", + "type": "string" + }, + "covenant_sigs": { + "description": "covenant_sigs is a list of adaptor signatures on the slashing tx by each covenant member. It will be a part of the witness for the staking tx output.", + "type": "array", + "items": { + "$ref": "#/definitions/CovenantAdaptorSignatures" + } + }, + "delegator_slashing_sig": { + "description": "delegator_slashing_sig is the signature on the slashing tx by the delegator (i.e. SK corresponding to btc_pk) as string hex. It will be a part of the witness for the staking tx output.", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "end_height": { + "description": "end_height is the end height of the BTC delegation it is the end BTC height of the time-lock - w", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "fp_btc_pk_list": { + "description": "fp_btc_pk_list is the list of BIP-340 PKs of the finality providers that this BTC delegation delegates to", + "type": "array", + "items": { + "type": "string" + } + }, + "params_version": { + "description": "params version used to validate the delegation", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "slashing_tx": { + "description": "slashing_tx is the slashing tx", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "staker_addr": { + "description": "staker_addr is the address to receive rewards from BTC delegation", + "type": "string" + }, + "staking_output_idx": { + "description": "staking_output_idx is the index of the staking output in the staking tx", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "staking_tx": { + "description": "staking_tx is the staking tx", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "start_height": { + "description": "start_height is the start BTC height of the BTC delegation. It is the start BTC height of the time-lock", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "total_sat": { + "description": "total_sat is the total BTC stakes in this delegation, quantified in satoshi", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "unbonding_time": { + "description": "unbonding_time is used in unbonding output time-lock path and in slashing transactions change outputs", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "undelegation_info": { + "description": "undelegation_info is the undelegation info of this delegation.", + "allOf": [ + { + "$ref": "#/definitions/BtcUndelegationInfo" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + }, + "BtcUndelegationInfo": { + "description": "BTCUndelegationInfo provides all necessary info about the undelegation", + "type": "object", + "required": [ + "covenant_slashing_sigs", + "covenant_unbonding_sig_list", + "delegator_slashing_sig", + "delegator_unbonding_sig", + "slashing_tx", + "unbonding_tx" + ], + "properties": { + "covenant_slashing_sigs": { + "description": "covenant_slashing_sigs is a list of adaptor signatures on the unbonding slashing tx by each covenant member It will be a part of the witness for the staking tx output.", + "type": "array", + "items": { + "$ref": "#/definitions/CovenantAdaptorSignatures" + } + }, + "covenant_unbonding_sig_list": { + "description": "covenant_unbonding_sig_list is the list of signatures on the unbonding tx by covenant members", + "type": "array", + "items": { + "$ref": "#/definitions/SignatureInfo" + } + }, + "delegator_slashing_sig": { + "description": "delegator_slashing_sig is the signature on the slashing tx by the delegator (i.e. SK corresponding to btc_pk). It will be a part of the witness for the unbonding tx output.", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "delegator_unbonding_sig": { + "description": "delegator_unbonding_sig is the signature on the unbonding tx by the delegator (i.e. SK corresponding to btc_pk). It effectively proves that the delegator wants to unbond and thus Babylon will consider this BTC delegation unbonded. Delegator's BTC on Bitcoin will be unbonded after time-lock.", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "slashing_tx": { + "description": "slashing_tx is the unbonding slashing tx", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "unbonding_tx": { + "description": "unbonding_tx is the transaction which will transfer the funds from staking output to unbonding output. Unbonding output will usually have lower timelock than staking output.", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + } + }, + "additionalProperties": false + }, + "CovenantAdaptorSignatures": { + "description": "CovenantAdaptorSignatures is a list adaptor signatures signed by the covenant with different finality provider's public keys as encryption keys", + "type": "object", + "required": [ + "adaptor_sigs", + "cov_pk" + ], + "properties": { + "adaptor_sigs": { + "description": "adaptor_sigs is a list of adaptor signatures, each encrypted by a restaked BTC finality provider's public key", + "type": "array", + "items": { + "$ref": "#/definitions/Binary" + } + }, + "cov_pk": { + "description": "cov_pk is the public key of the covenant emulator, used as the public key of the adaptor signature", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + } + }, + "additionalProperties": false + }, + "SignatureInfo": { + "description": "SignatureInfo is a BIP-340 signature together with its signer's BIP-340 PK", + "type": "object", + "required": [ + "pk", + "sig" + ], + "properties": { + "pk": { + "$ref": "#/definitions/Binary" + }, + "sig": { + "$ref": "#/definitions/Binary" + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/btc-finality/schema/raw/response_to_delegations.json b/contracts/btc-finality/schema/raw/response_to_delegations.json new file mode 100644 index 00000000..9cb87334 --- /dev/null +++ b/contracts/btc-finality/schema/raw/response_to_delegations.json @@ -0,0 +1,260 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "BtcDelegationsResponse", + "type": "object", + "required": [ + "delegations" + ], + "properties": { + "delegations": { + "type": "array", + "items": { + "$ref": "#/definitions/BtcDelegation" + } + } + }, + "additionalProperties": false, + "definitions": { + "BtcDelegation": { + "type": "object", + "required": [ + "btc_pk_hex", + "covenant_sigs", + "delegator_slashing_sig", + "end_height", + "fp_btc_pk_list", + "params_version", + "slashed", + "slashing_tx", + "staker_addr", + "staking_output_idx", + "staking_tx", + "start_height", + "total_sat", + "unbonding_time", + "undelegation_info" + ], + "properties": { + "btc_pk_hex": { + "description": "btc_pk_hex is the Bitcoin secp256k1 PK of the BTC delegator. The PK follows encoding in BIP-340 spec in hex format", + "type": "string" + }, + "covenant_sigs": { + "description": "covenant_sigs is a list of adaptor signatures on the slashing tx by each covenant member. It will be a part of the witness for the staking tx output.", + "type": "array", + "items": { + "$ref": "#/definitions/CovenantAdaptorSignatures" + } + }, + "delegator_slashing_sig": { + "description": "delegator_slashing_sig is the signature on the slashing tx by the delegator (i.e. SK corresponding to btc_pk) as string hex. It will be a part of the witness for the staking tx output.", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + } + }, + "end_height": { + "description": "end_height is the end height of the BTC delegation it is the end BTC height of the time-lock - w", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "fp_btc_pk_list": { + "description": "fp_btc_pk_list is the list of BIP-340 PKs of the finality providers that this BTC delegation delegates to", + "type": "array", + "items": { + "type": "string" + } + }, + "params_version": { + "description": "params version used to validate the delegation", + "type": "integer", + "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", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + } + }, + "staker_addr": { + "description": "staker_addr is the address to receive rewards from BTC delegation", + "type": "string" + }, + "staking_output_idx": { + "description": "staking_output_idx is the index of the staking output in the staking tx", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "staking_tx": { + "description": "staking_tx is the staking tx", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + } + }, + "start_height": { + "description": "start_height is the start BTC height of the BTC delegation. It is the start BTC height of the time-lock", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "total_sat": { + "description": "total_sat is the total BTC stakes in this delegation, quantified in satoshi", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "unbonding_time": { + "description": "unbonding_time is used in unbonding output time-lock path and in slashing transactions change outputs", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "undelegation_info": { + "description": "undelegation_info is the undelegation info of this delegation.", + "allOf": [ + { + "$ref": "#/definitions/BtcUndelegationInfo" + } + ] + } + }, + "additionalProperties": false + }, + "BtcUndelegationInfo": { + "type": "object", + "required": [ + "covenant_slashing_sigs", + "covenant_unbonding_sig_list", + "delegator_slashing_sig", + "delegator_unbonding_sig", + "slashing_tx", + "unbonding_tx" + ], + "properties": { + "covenant_slashing_sigs": { + "description": "covenant_slashing_sigs is a list of adaptor signatures on the unbonding slashing tx by each covenant member It will be a part of the witness for the staking tx output.", + "type": "array", + "items": { + "$ref": "#/definitions/CovenantAdaptorSignatures" + } + }, + "covenant_unbonding_sig_list": { + "description": "covenant_unbonding_sig_list is the list of signatures on the unbonding tx by covenant members", + "type": "array", + "items": { + "$ref": "#/definitions/SignatureInfo" + } + }, + "delegator_slashing_sig": { + "description": "delegator_slashing_sig is the signature on the slashing tx by the delegator (i.e. SK corresponding to btc_pk). It will be a part of the witness for the unbonding tx output.", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + } + }, + "delegator_unbonding_sig": { + "description": "delegator_unbonding_sig is the signature on the unbonding tx by the delegator (i.e. SK corresponding to btc_pk). It effectively proves that the delegator wants to unbond and thus Babylon will consider this BTC delegation unbonded. Delegator's BTC on Bitcoin will be unbonded after time-lock.", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + } + }, + "slashing_tx": { + "description": "slashing_tx is the unbonding slashing tx", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + } + }, + "unbonding_tx": { + "description": "unbonding_tx is the transaction which will transfer the funds from staking output to unbonding output. Unbonding output will usually have lower timelock than staking output.", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + } + } + }, + "additionalProperties": false + }, + "CovenantAdaptorSignatures": { + "type": "object", + "required": [ + "adaptor_sigs", + "cov_pk" + ], + "properties": { + "adaptor_sigs": { + "description": "adaptor_sigs is a list of adaptor signatures, each encrypted by a restaked BTC finality provider's public key", + "type": "array", + "items": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + } + } + }, + "cov_pk": { + "description": "cov_pk is the public key of the covenant emulator, used as the public key of the adaptor signature", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + } + } + }, + "additionalProperties": false + }, + "SignatureInfo": { + "type": "object", + "required": [ + "pk", + "sig" + ], + "properties": { + "pk": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + } + }, + "sig": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + } + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/btc-finality/schema/raw/response_to_delegations_by_f_p.json b/contracts/btc-finality/schema/raw/response_to_delegations_by_f_p.json new file mode 100644 index 00000000..c91c2ccd --- /dev/null +++ b/contracts/btc-finality/schema/raw/response_to_delegations_by_f_p.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "DelegationsByFPResponse", + "type": "object", + "required": [ + "hashes" + ], + "properties": { + "hashes": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false +} diff --git a/contracts/btc-finality/schema/raw/response_to_finality_provider.json b/contracts/btc-finality/schema/raw/response_to_finality_provider.json new file mode 100644 index 00000000..72127516 --- /dev/null +++ b/contracts/btc-finality/schema/raw/response_to_finality_provider.json @@ -0,0 +1,137 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "FinalityProvider", + "type": "object", + "required": [ + "addr", + "btc_pk_hex", + "commission", + "consumer_id", + "slashed_btc_height", + "slashed_height" + ], + "properties": { + "addr": { + "description": "addr is the bech32 address identifier of the finality provider", + "type": "string" + }, + "btc_pk_hex": { + "description": "btc_pk_hex is the Bitcoin secp256k1 PK of this finality provider the PK follows encoding in BIP-340 spec in hex format", + "type": "string" + }, + "commission": { + "description": "commission defines the commission rate of the finality provider.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "consumer_id": { + "description": "consumer_id is the ID of the consumer that the finality provider is operating on.", + "type": "string" + }, + "description": { + "description": "description defines the description terms for the finality provider", + "anyOf": [ + { + "$ref": "#/definitions/FinalityProviderDescription" + }, + { + "type": "null" + } + ] + }, + "pop": { + "description": "pop is the proof of possession of the babylon_pk and btc_pk", + "anyOf": [ + { + "$ref": "#/definitions/ProofOfPossessionBtc" + }, + { + "type": "null" + } + ] + }, + "slashed_btc_height": { + "description": "slashed_btc_height is the BTC height on which the finality provider is slashed", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "slashed_height": { + "description": "slashed_height is the height on which the finality provider is slashed", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "FinalityProviderDescription": { + "type": "object", + "required": [ + "details", + "identity", + "moniker", + "security_contact", + "website" + ], + "properties": { + "details": { + "description": "details is the details of the finality provider", + "type": "string" + }, + "identity": { + "description": "identity is the identity of the finality provider", + "type": "string" + }, + "moniker": { + "description": "moniker is the name of the finality provider", + "type": "string" + }, + "security_contact": { + "description": "security_contact is the security contact of the finality provider", + "type": "string" + }, + "website": { + "description": "website is the website of the finality provider", + "type": "string" + } + }, + "additionalProperties": false + }, + "ProofOfPossessionBtc": { + "description": "ProofOfPossessionBtc is the proof of possession that a Babylon secp256k1 secret key and a Bitcoin secp256k1 secret key are held by the same person", + "type": "object", + "required": [ + "btc_sig", + "btc_sig_type" + ], + "properties": { + "btc_sig": { + "description": "btc_sig is the signature generated via sign(sk_btc, babylon_sig) the signature follows encoding in either BIP-340 spec or BIP-322 spec", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "btc_sig_type": { + "description": "btc_sig_type indicates the type of btc_sig in the pop", + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/btc-finality/schema/raw/response_to_finality_provider_info.json b/contracts/btc-finality/schema/raw/response_to_finality_provider_info.json new file mode 100644 index 00000000..cc0d3cbc --- /dev/null +++ b/contracts/btc-finality/schema/raw/response_to_finality_provider_info.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "FinalityProviderInfo", + "type": "object", + "required": [ + "btc_pk_hex", + "power" + ], + "properties": { + "btc_pk_hex": { + "description": "`btc_pk_hex` is the Bitcoin secp256k1 PK of this finality provider. The PK follows encoding in BIP-340 spec in hex format", + "type": "string" + }, + "power": { + "description": "`power` is the aggregated power of this finality provider. The power is calculated based on the amount of BTC delegated to this finality provider", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false +} diff --git a/contracts/btc-finality/schema/raw/response_to_finality_providers.json b/contracts/btc-finality/schema/raw/response_to_finality_providers.json new file mode 100644 index 00000000..e0f3177f --- /dev/null +++ b/contracts/btc-finality/schema/raw/response_to_finality_providers.json @@ -0,0 +1,152 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "FinalityProvidersResponse", + "type": "object", + "required": [ + "fps" + ], + "properties": { + "fps": { + "type": "array", + "items": { + "$ref": "#/definitions/FinalityProvider" + } + } + }, + "additionalProperties": false, + "definitions": { + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "FinalityProvider": { + "type": "object", + "required": [ + "addr", + "btc_pk_hex", + "commission", + "consumer_id", + "slashed_btc_height", + "slashed_height" + ], + "properties": { + "addr": { + "description": "addr is the bech32 address identifier of the finality provider", + "type": "string" + }, + "btc_pk_hex": { + "description": "btc_pk_hex is the Bitcoin secp256k1 PK of this finality provider the PK follows encoding in BIP-340 spec in hex format", + "type": "string" + }, + "commission": { + "description": "commission defines the commission rate of the finality provider.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "consumer_id": { + "description": "consumer_id is the ID of the consumer that the finality provider is operating on.", + "type": "string" + }, + "description": { + "description": "description defines the description terms for the finality provider", + "anyOf": [ + { + "$ref": "#/definitions/FinalityProviderDescription" + }, + { + "type": "null" + } + ] + }, + "pop": { + "description": "pop is the proof of possession of the babylon_pk and btc_pk", + "anyOf": [ + { + "$ref": "#/definitions/ProofOfPossessionBtc" + }, + { + "type": "null" + } + ] + }, + "slashed_btc_height": { + "description": "slashed_btc_height is the BTC height on which the finality provider is slashed", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "slashed_height": { + "description": "slashed_height is the height on which the finality provider is slashed", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "FinalityProviderDescription": { + "type": "object", + "required": [ + "details", + "identity", + "moniker", + "security_contact", + "website" + ], + "properties": { + "details": { + "description": "details is the details of the finality provider", + "type": "string" + }, + "identity": { + "description": "identity is the identity of the finality provider", + "type": "string" + }, + "moniker": { + "description": "moniker is the name of the finality provider", + "type": "string" + }, + "security_contact": { + "description": "security_contact is the security contact of the finality provider", + "type": "string" + }, + "website": { + "description": "website is the website of the finality provider", + "type": "string" + } + }, + "additionalProperties": false + }, + "ProofOfPossessionBtc": { + "description": "ProofOfPossessionBtc is the proof of possession that a Babylon secp256k1 secret key and a Bitcoin secp256k1 secret key are held by the same person", + "type": "object", + "required": [ + "btc_sig", + "btc_sig_type" + ], + "properties": { + "btc_sig": { + "description": "btc_sig is the signature generated via sign(sk_btc, babylon_sig) the signature follows encoding in either BIP-340 spec or BIP-322 spec", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "btc_sig_type": { + "description": "btc_sig_type indicates the type of btc_sig in the pop", + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/btc-finality/schema/raw/response_to_finality_providers_by_power.json b/contracts/btc-finality/schema/raw/response_to_finality_providers_by_power.json new file mode 100644 index 00000000..e36afe41 --- /dev/null +++ b/contracts/btc-finality/schema/raw/response_to_finality_providers_by_power.json @@ -0,0 +1,39 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "FinalityProvidersByPowerResponse", + "type": "object", + "required": [ + "fps" + ], + "properties": { + "fps": { + "type": "array", + "items": { + "$ref": "#/definitions/FinalityProviderInfo" + } + } + }, + "additionalProperties": false, + "definitions": { + "FinalityProviderInfo": { + "type": "object", + "required": [ + "btc_pk_hex", + "power" + ], + "properties": { + "btc_pk_hex": { + "description": "`btc_pk_hex` is the Bitcoin secp256k1 PK of this finality provider. The PK follows encoding in BIP-340 spec in hex format", + "type": "string" + }, + "power": { + "description": "`power` is the aggregated power of this finality provider. The power is calculated based on the amount of BTC delegated to this finality provider", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/btc-finality/schema/raw/response_to_params.json b/contracts/btc-finality/schema/raw/response_to_params.json new file mode 100644 index 00000000..89d6b26a --- /dev/null +++ b/contracts/btc-finality/schema/raw/response_to_params.json @@ -0,0 +1,56 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Params", + "description": "Params define Consumer-selectable BTC staking parameters", + "type": "object", + "required": [ + "btc_network", + "covenant_pks", + "covenant_quorum", + "min_slashing_tx_fee_sat", + "slashing_address", + "slashing_rate" + ], + "properties": { + "btc_network": { + "$ref": "#/definitions/Network" + }, + "covenant_pks": { + "type": "array", + "items": { + "type": "string" + } + }, + "covenant_quorum": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "min_slashing_tx_fee_sat": { + "description": "`min_slashing_tx_fee_sat` is the minimum amount of tx fee (quantified in Satoshi) needed for the pre-signed slashing tx", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "slashing_address": { + "description": "`slashing_address` is the address that the slashed BTC goes to. The address is in string format on Bitcoin.", + "type": "string" + }, + "slashing_rate": { + "description": "`slashing_rate` determines the portion of the staked amount to be slashed, expressed as a decimal (e.g. 0.5 for 50%).", + "type": "string" + } + }, + "additionalProperties": false, + "definitions": { + "Network": { + "type": "string", + "enum": [ + "mainnet", + "testnet", + "signet", + "regtest" + ] + } + } +} diff --git a/contracts/btc-finality/src/bin/schema.rs b/contracts/btc-finality/src/bin/schema.rs new file mode 100644 index 00000000..b975175a --- /dev/null +++ b/contracts/btc-finality/src/bin/schema.rs @@ -0,0 +1,24 @@ +use cosmwasm_schema::write_api; +use cosmwasm_std::Empty; + +use btc_staking::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +fn main() { + // Clear & write standard API + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + migrate: Empty, + execute: ExecuteMsg, + } + + // Schemas for inter-contract communication + // let mut out_dir = current_dir().unwrap(); + // out_dir.push("schema"); + // export_schema(&schema_for!(PacketMsg), &out_dir); // TODO: find a way to export schema for IBC packet + // export_schema_with_title( + // &schema_for!(AcknowledgementMsg), + // &out_dir, + // "AcknowledgementMsgBtcTimestamp", + // ); +} diff --git a/contracts/btc-finality/src/contract.rs b/contracts/btc-finality/src/contract.rs new file mode 100644 index 00000000..5ab224b6 --- /dev/null +++ b/contracts/btc-finality/src/contract.rs @@ -0,0 +1,468 @@ +use babylon_apis::finality_api::SudoMsg; +use babylon_bindings::BabylonMsg; +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + attr, to_json_binary, Addr, CustomQuery, Deps, DepsMut, Empty, Env, MessageInfo, + QuerierWrapper, QueryRequest, QueryResponse, Reply, Response, StdResult, WasmQuery, +}; +use cw2::set_contract_version; +use cw_utils::{maybe_addr, nonpayable}; + +use btc_staking::msg::ActivatedHeightResponse; + +use crate::error::ContractError; +use crate::finality::{ + compute_active_finality_providers, handle_finality_signature, handle_public_randomness_commit, +}; +use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use crate::state::config::{Config, ADMIN, CONFIG, PARAMS}; +use crate::{finality, queries, state}; + +pub const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); +pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + mut deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> Result, ContractError> { + nonpayable(&info)?; + let config = Config { + babylon: info.sender, + staking: Addr::unchecked("UNSET"), // To be set later, through `UpdateStaking` + }; + CONFIG.save(deps.storage, &config)?; + + let api = deps.api; + ADMIN.set(deps.branch(), maybe_addr(api, msg.admin.clone())?)?; + + let params = msg.params.unwrap_or_default(); + PARAMS.save(deps.storage, ¶ms)?; + // initialize storage, so no issue when reading for the first time + + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + Ok(Response::new().add_attribute("action", "instantiate")) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply(_deps: DepsMut, _env: Env, _reply: Reply) -> StdResult { + Ok(Response::default()) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result { + match msg { + QueryMsg::Config {} => Ok(to_json_binary(&queries::config(deps)?)?), + QueryMsg::Params {} => Ok(to_json_binary(&queries::params(deps)?)?), + QueryMsg::Admin {} => to_json_binary(&ADMIN.query_admin(deps)?).map_err(Into::into), + QueryMsg::FinalitySignature { btc_pk_hex, height } => Ok(to_json_binary( + &queries::finality_signature(deps, btc_pk_hex, height)?, + )?), + QueryMsg::PubRandCommit { + btc_pk_hex, + start_after, + limit, + reverse, + } => Ok(to_json_binary( + &state::public_randomness::get_pub_rand_commit( + deps.storage, + &btc_pk_hex, + start_after, + limit, + reverse, + )?, + )?), + QueryMsg::FirstPubRandCommit { btc_pk_hex } => Ok(to_json_binary( + &state::public_randomness::get_first_pub_rand_commit(deps.storage, &btc_pk_hex)?, + )?), + QueryMsg::LastPubRandCommit { btc_pk_hex } => Ok(to_json_binary( + &state::public_randomness::get_last_pub_rand_commit(deps.storage, &btc_pk_hex)?, + )?), + QueryMsg::Block { height } => Ok(to_json_binary(&queries::block(deps, height)?)?), + QueryMsg::Blocks { + start_after, + limit, + finalised, + reverse, + } => Ok(to_json_binary(&queries::blocks( + deps, + start_after, + limit, + finalised, + reverse, + )?)?), + QueryMsg::Evidence { btc_pk_hex, height } => Ok(to_json_binary(&queries::evidence( + deps, btc_pk_hex, height, + )?)?), + } +} + +/// This is a no-op just to test how this integrates with wasmd +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(_deps: DepsMut, _env: Env, _msg: Empty) -> StdResult { + Ok(Response::default()) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result, ContractError> { + let api = deps.api; + match msg { + ExecuteMsg::UpdateAdmin { admin } => ADMIN + .execute_update_admin(deps, info, maybe_addr(api, admin)?) + .map_err(Into::into), + ExecuteMsg::UpdateStaking { staking } => handle_update_staking(deps, info, staking), + ExecuteMsg::SubmitFinalitySignature { + fp_pubkey_hex, + height, + pub_rand, + proof, + block_hash, + signature, + } => handle_finality_signature( + deps, + env, + &fp_pubkey_hex, + height, + &pub_rand, + &proof, + &block_hash, + &signature, + ), + ExecuteMsg::CommitPublicRandomness { + fp_pubkey_hex, + start_height, + num_pub_rand, + commitment, + signature, + } => handle_public_randomness_commit( + deps, + &fp_pubkey_hex, + start_height, + num_pub_rand, + &commitment, + &signature, + ), + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn sudo( + mut deps: DepsMut, + env: Env, + msg: SudoMsg, +) -> Result, ContractError> { + match msg { + SudoMsg::BeginBlock { .. } => handle_begin_block(&mut deps, env), + SudoMsg::EndBlock { + hash_hex, + app_hash_hex, + } => handle_end_block(&mut deps, env, &hash_hex, &app_hash_hex), + } +} + +fn handle_update_staking( + deps: DepsMut, + info: MessageInfo, + staking_addr: String, +) -> Result, ContractError> { + let mut cfg = CONFIG.load(deps.storage)?; + if info.sender != cfg.babylon && !ADMIN.is_admin(deps.as_ref(), &info.sender)? { + return Err(ContractError::Unauthorized {}); + } + cfg.staking = deps.api.addr_validate(&staking_addr)?; + CONFIG.save(deps.storage, &cfg)?; + + let attributes = vec![ + attr("action", "update_btc_staking"), + attr("staking", staking_addr), + attr("sender", info.sender), + ]; + Ok(Response::new().add_attributes(attributes)) +} + +fn handle_begin_block(deps: &mut DepsMut, env: Env) -> Result, ContractError> { + // 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)?; + + Ok(Response::new()) +} + +fn handle_end_block( + deps: &mut DepsMut, + env: Env, + _hash_hex: &str, + app_hash_hex: &str, +) -> Result, ContractError> { + // If the BTC staking protocol is activated i.e. there exists a height where at least one + // finality provider has voting power, start indexing and tallying blocks + let cfg = CONFIG.load(deps.storage)?; + let mut res = Response::new(); + let activated_height = get_activated_height(&cfg.staking, &deps.querier)?; + if activated_height > 0 { + // Index the current 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 events = finality::tally_blocks(deps, activated_height, env.block.height)?; + res = res.add_events(events); + } + Ok(res) +} + +pub fn get_activated_height(staking_addr: &Addr, querier: &QuerierWrapper) -> StdResult { + // TODO: Use a raw query + let query = encode_smart_query( + staking_addr, + &btc_staking::msg::QueryMsg::ActivatedHeight {}, + )?; + let res: ActivatedHeightResponse = querier.query(&query)?; + Ok(res.height) +} + +pub(crate) fn encode_smart_query( + addr: &Addr, + msg: &btc_staking::msg::QueryMsg, +) -> StdResult> { + Ok(WasmQuery::Smart { + contract_addr: addr.to_string(), + msg: to_json_binary(&msg)?, + } + .into()) +} + +#[cfg(test)] +pub(crate) mod tests { + use std::str::FromStr; + + use super::*; + + use babylon_apis::btc_staking_api::{ + ActiveBtcDelegation, BtcUndelegationInfo, CovenantAdaptorSignatures, + FinalityProviderDescription, NewFinalityProvider, ProofOfPossessionBtc, + }; + use babylon_apis::finality_api::PubRandCommit; + use babylon_proto::babylon::btcstaking::v1::{BtcDelegation, FinalityProvider}; + use cosmwasm_std::{ + from_json, + testing::{message_info, mock_dependencies, mock_env}, + Binary, Decimal, + }; + use cw_controllers::AdminResponse; + use hex::ToHex; + use test_utils::{get_btc_delegation, get_finality_provider, get_pub_rand_commit}; + + pub(crate) const CREATOR: &str = "creator"; + pub(crate) const INIT_ADMIN: &str = "initial_admin"; + const NEW_ADMIN: &str = "new_admin"; + + fn new_finality_provider(fp: FinalityProvider) -> NewFinalityProvider { + NewFinalityProvider { + addr: fp.addr, + description: fp.description.map(|desc| FinalityProviderDescription { + moniker: desc.moniker, + identity: desc.identity, + website: desc.website, + security_contact: desc.security_contact, + details: desc.details, + }), + commission: Decimal::from_str(&fp.commission).unwrap(), + btc_pk_hex: fp.btc_pk.encode_hex(), + pop: match fp.pop { + Some(pop) => Some(ProofOfPossessionBtc { + btc_sig_type: pop.btc_sig_type, + btc_sig: Binary::new(pop.btc_sig.to_vec()), + }), + None => None, + }, + consumer_id: fp.consumer_id, + } + } + + fn new_active_btc_delegation(del: BtcDelegation) -> ActiveBtcDelegation { + let btc_undelegation = del.btc_undelegation.unwrap(); + + ActiveBtcDelegation { + staker_addr: del.staker_addr, + btc_pk_hex: del.btc_pk.encode_hex(), + fp_btc_pk_list: del + .fp_btc_pk_list + .iter() + .map(|fp_btc_pk| fp_btc_pk.encode_hex()) + .collect(), + start_height: del.start_height, + end_height: del.end_height, + 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(del.delegator_sig.to_vec()), + covenant_sigs: del + .covenant_sigs + .iter() + .map(|cov_sig| CovenantAdaptorSignatures { + cov_pk: Binary::new(cov_sig.cov_pk.to_vec()), + adaptor_sigs: cov_sig + .adaptor_sigs + .iter() + .map(|adaptor_sig| Binary::new(adaptor_sig.to_vec())) + .collect(), + }) + .collect(), + staking_output_idx: del.staking_output_idx, + unbonding_time: del.unbonding_time, + 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_slashing_sig: Binary::new( + btc_undelegation.delegator_slashing_sig.to_vec(), + ), + covenant_unbonding_sig_list: vec![], + covenant_slashing_sigs: vec![], + }, + params_version: del.params_version, + } + } + + /// Build a derived active BTC delegation from the base (from testdata) BTC delegation + pub(crate) fn get_derived_btc_delegation(del_id: i32, fp_ids: &[i32]) -> ActiveBtcDelegation { + let del = get_btc_delegation(del_id, fp_ids.to_vec()); + new_active_btc_delegation(del) + } + + /// Get public randomness public key, commitment, and signature information + /// + /// Signature is a Schnorr signature over the commitment + pub(crate) fn get_public_randomness_commitment() -> (String, PubRandCommit, Vec) { + let pub_rand_commitment_msg = get_pub_rand_commit(); + ( + pub_rand_commitment_msg.fp_btc_pk.encode_hex(), + PubRandCommit { + start_height: pub_rand_commitment_msg.start_height, + num_pub_rand: pub_rand_commitment_msg.num_pub_rand, + commitment: pub_rand_commitment_msg.commitment.to_vec(), + }, + pub_rand_commitment_msg.sig.to_vec(), + ) + } + + pub(crate) fn create_new_finality_provider(id: i32) -> NewFinalityProvider { + let fp = get_finality_provider(id); + new_finality_provider(fp) + } + + #[test] + fn instantiate_without_admin() { + let mut deps = mock_dependencies(); + + // Create an InstantiateMsg with admin set to None + let msg = InstantiateMsg { + params: None, + admin: None, // No admin provided + }; + + let info = message_info(&deps.api.addr_make(CREATOR), &[]); + + // Call the instantiate function + let res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); + + // Assert that no messages were sent + assert_eq!(0, res.messages.len()); + + // Query the admin to verify it was not set + let res = ADMIN.query_admin(deps.as_ref()).unwrap(); + assert_eq!(None, res.admin); + } + + #[test] + fn instantiate_with_admin() { + let mut deps = mock_dependencies(); + let init_admin = deps.api.addr_make(INIT_ADMIN); + + // Create an InstantiateMsg with admin set to Some(INIT_ADMIN.into()) + let msg = InstantiateMsg { + params: None, + admin: Some(init_admin.to_string()), // Admin provided + }; + + let info = message_info(&deps.api.addr_make(CREATOR), &[]); + + // Call the instantiate function + let res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); + + // Assert that no messages were sent + assert_eq!(0, res.messages.len()); + + // Use assert_admin to verify that the admin was set correctly + // This uses the assert_admin helper function provided by the Admin crate + ADMIN.assert_admin(deps.as_ref(), &init_admin).unwrap(); + + // ensure the admin is queryable as well + let res = query(deps.as_ref(), mock_env(), QueryMsg::Admin {}).unwrap(); + let admin: AdminResponse = from_json(res).unwrap(); + assert_eq!(admin.admin.unwrap(), init_admin.as_str()) + } + + #[test] + fn test_update_admin() { + 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 { + params: None, + admin: Some(init_admin.to_string()), // Admin provided + }; + + let info = message_info(&deps.api.addr_make(CREATOR), &[]); + + // Call the instantiate function + let res = instantiate(deps.as_mut(), mock_env(), info.clone(), instantiate_msg).unwrap(); + + // Assert that no messages were sent + assert_eq!(0, res.messages.len()); + + // Use assert_admin to verify that the admin was set correctly + ADMIN.assert_admin(deps.as_ref(), &init_admin).unwrap(); + + // Update the admin to new_admin + let update_admin_msg = ExecuteMsg::UpdateAdmin { + admin: Some(new_admin.to_string()), + }; + + // Execute the UpdateAdmin message with non-admin info + let non_admin_info = message_info(&deps.api.addr_make("non_admin"), &[]); + let err = execute( + deps.as_mut(), + mock_env(), + non_admin_info, + update_admin_msg.clone(), + ) + .unwrap_err(); + assert_eq!( + err, + ContractError::Admin(cw_controllers::AdminError::NotAdmin {}) + ); + + // Execute the UpdateAdmin message with the initial admin info + let admin_info = message_info(&init_admin, &[]); + let res = execute(deps.as_mut(), mock_env(), admin_info, update_admin_msg).unwrap(); + + // Assert that no messages were sent + assert_eq!(0, res.messages.len()); + + // Use assert_admin to verify that the admin was updated correctly + ADMIN.assert_admin(deps.as_ref(), &new_admin).unwrap(); + } +} diff --git a/contracts/btc-finality/src/error.rs b/contracts/btc-finality/src/error.rs new file mode 100644 index 00000000..b96481aa --- /dev/null +++ b/contracts/btc-finality/src/error.rs @@ -0,0 +1,97 @@ +use hex::FromHexError; +use prost::DecodeError; +use thiserror::Error; + +use bitcoin::hashes::FromSliceError; +use bitcoin::hex::HexToArrayError; + +use cosmwasm_std::StdError; +use cw_controllers::AdminError; +use cw_utils::PaymentError; + +use babylon_apis::error::StakingApiError; +use babylon_merkle::error::MerkleError; + +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + Admin(#[from] AdminError), + #[error("{0}")] + Std(#[from] StdError), + #[error("{0}")] + Payment(#[from] PaymentError), + #[error("{0}")] + BTCStaking(#[from] babylon_btcstaking::error::Error), + #[error("error converting from hex to array: {0}")] + HexArrayError(#[from] HexToArrayError), + #[error("{0}")] + SliceError(#[from] FromSliceError), + #[error("{0}")] + StakingError(#[from] StakingApiError), + #[error("{0}")] + MerkleError(#[from] MerkleError), + #[error("{0}")] + ProtoError(#[from] DecodeError), + #[error("{0}")] + HexError(#[from] FromHexError), + #[error("EOTS error: {0}")] + EotsError(#[from] eots::Error), + #[error("{0}")] + SecP256K1Error(String), // TODO: inherit errors from k256 + #[error("Unauthorized")] + Unauthorized, + #[error("Failed to verify the finality provider registration request: {0}")] + FinalityProviderVerificationError(String), + #[error("Finality provider already exists: {0}")] + FinalityProviderAlreadyExists(String), + #[error("No finality providers are registered in this Consumer")] + FinalityProviderNotRegistered, + #[error("Finality provider not found: {0}")] + FinalityProviderNotFound(String), + #[error("Staking tx hash already exists: {0}")] + DelegationAlreadyExists(String), + #[error("Invalid Btc tx: {0}")] + InvalidBtcTx(String), + #[error("Empty signature from the delegator")] + EmptySignature, + #[error("Invalid lock type: seconds")] + ErrInvalidLockType, + #[error("Invalid lock time blocks: {0}, max: {1}")] + ErrInvalidLockTime(u32, u32), + #[error("The finality provider {0} does not have voting power at height {1}")] + NoVotingPower(String, u64), + #[error("The chain has not reached the given height yet")] + HeightTooHigh, + #[error("The finality provider {0} signed two different blocks at height {1}")] + DuplicateFinalityVote(String, u64), + #[error("The request contains too few public randomness. Required minimum: {0}, actual: {1}")] + TooFewPubRand(u64, u64), + #[error("The start height ({0}) has overlap with the height of the highest public randomness committed ({1})")] + InvalidPubRandHeight(u64, u64), + #[error("Invalid signature over the public randomness list")] + InvalidPubRandSignature, + #[error("Public randomness not found for finality provider {0} at height {1}")] + MissingPubRandCommit(String, u64), + #[error("The inclusion proof for height {0} does not correspond to the given height ({1})")] + InvalidFinalitySigHeight(u64, u64), + #[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("Invalid finality signature: {0}")] + InvalidSignature(String), + #[error("Failed to verify signature: {0}")] + FailedSignatureVerification(String), + #[error("Block {0} is finalized, but last finalized height does not reach here")] + FinalisedBlockWithFinalityProviderSet(u64), + #[error("Block {0} is finalized, but does not have a finality provider set")] + FinalisedBlockWithoutFinalityProviderSet(u64), + #[error("Block {0} is not found: {1}")] + BlockNotFound(u64, String), + #[error("The finality provider {0} has already been slashed")] + FinalityProviderAlreadySlashed(String), + #[error("Failed to slash finality provider: {0}")] + FailedToSlashFinalityProvider(String), + #[error("Failed to extract secret key: {0}")] + SecretKeyExtractionError(String), + #[error("Hash length error: {0}")] + WrongHashLength(String), +} diff --git a/contracts/btc-finality/src/finality.rs b/contracts/btc-finality/src/finality.rs new file mode 100644 index 00000000..41a64fdd --- /dev/null +++ b/contracts/btc-finality/src/finality.rs @@ -0,0 +1,583 @@ +use k256::ecdsa::signature::Verifier; +use k256::schnorr::{Signature, VerifyingKey}; +use k256::sha2::{Digest, Sha256}; +use std::cmp::max; +use std::collections::HashSet; + +use crate::contract::encode_smart_query; +use crate::error::ContractError; +use crate::state::config::{CONFIG, PARAMS}; +use crate::state::finality::{BLOCKS, EVIDENCES, FP_SET, NEXT_HEIGHT, SIGNATURES, TOTAL_POWER}; +use crate::state::public_randomness::{ + get_last_pub_rand_commit, get_pub_rand_commit_for_height, PUB_RAND_COMMITS, PUB_RAND_VALUES, +}; +use babylon_apis::btc_staking_api::FinalityProvider; +use babylon_apis::finality_api::{Evidence, IndexedBlock, PubRandCommit}; +use babylon_bindings::BabylonMsg; +use babylon_merkle::Proof; +use btc_staking::msg::{FinalityProviderInfo, FinalityProvidersByPowerResponse}; +use cosmwasm_std::Order::Ascending; +use cosmwasm_std::{ + to_json_binary, Addr, DepsMut, Env, Event, QuerierWrapper, Response, StdResult, Storage, + WasmMsg, +}; + +pub fn handle_public_randomness_commit( + deps: DepsMut, + fp_pubkey_hex: &str, + start_height: u64, + num_pub_rand: u64, + commitment: &[u8], + signature: &[u8], +) -> Result, ContractError> { + // Ensure the request contains enough amounts of public randomness + let min_pub_rand = PARAMS.load(deps.storage)?.min_pub_rand; + if num_pub_rand < min_pub_rand { + return Err(ContractError::TooFewPubRand(min_pub_rand, num_pub_rand)); + } + // TODO: ensure log_2(num_pub_rand) is an integer? + + // Ensure the finality provider is registered + // TODO: Use a raw query for performance and cost + let _fp: FinalityProvider = deps + .querier + .query_wasm_smart( + CONFIG.load(deps.storage)?.staking, + &btc_staking::msg::QueryMsg::FinalityProvider { + btc_pk_hex: fp_pubkey_hex.to_string(), + }, + ) + .map_err(|_| ContractError::FinalityProviderNotFound(fp_pubkey_hex.to_string()))?; + // Verify signature over the list + verify_commitment_signature( + fp_pubkey_hex, + start_height, + num_pub_rand, + commitment, + signature, + )?; + + // Get last public randomness commitment + // TODO: allow committing public randomness earlier than existing ones? + let last_pr_commit = get_last_pub_rand_commit(deps.storage, fp_pubkey_hex) + .ok() // Turn error into None + .flatten(); + + // Check for overlapping heights if there is a last commit + if let Some(last_pr_commit) = last_pr_commit { + if start_height <= last_pr_commit.end_height() { + return Err(ContractError::InvalidPubRandHeight( + start_height, + last_pr_commit.end_height(), + )); + } + } + + // All good, store the given public randomness commitment + let pr_commit = PubRandCommit { + start_height, + num_pub_rand, + commitment: commitment.to_vec(), + }; + + PUB_RAND_COMMITS.save( + deps.storage, + (fp_pubkey_hex, pr_commit.start_height), + &pr_commit, + )?; + + // TODO: Add events + Ok(Response::new()) +} + +fn verify_commitment_signature( + fp_btc_pk_hex: &str, + start_height: u64, + num_pub_rand: u64, + commitment: &[u8], + signature: &[u8], +) -> Result<(), ContractError> { + // get BTC public key for verification + let btc_pk_raw = hex::decode(fp_btc_pk_hex)?; + let btc_pk = VerifyingKey::from_bytes(&btc_pk_raw) + .map_err(|e| ContractError::SecP256K1Error(e.to_string()))?; + + // get signature + if signature.is_empty() { + return Err(ContractError::EmptySignature); + } + let schnorr_sig = + Signature::try_from(signature).map_err(|e| ContractError::SecP256K1Error(e.to_string()))?; + + // get signed message + let mut msg: Vec = vec![]; + msg.extend_from_slice(&start_height.to_be_bytes()); + msg.extend_from_slice(&num_pub_rand.to_be_bytes()); + msg.extend_from_slice(commitment); + + // Verify the signature + btc_pk + .verify(&msg, &schnorr_sig) + .map_err(|e| ContractError::SecP256K1Error(e.to_string())) +} + +#[allow(clippy::too_many_arguments)] +pub fn handle_finality_signature( + mut deps: DepsMut, + env: Env, + fp_btc_pk_hex: &str, + height: u64, + pub_rand: &[u8], + proof: &Proof, + block_app_hash: &[u8], + signature: &[u8], +) -> Result, ContractError> { + // Ensure the finality provider exists + let staking_addr = CONFIG.load(deps.storage)?.staking; + let fp: FinalityProvider = deps.querier.query_wasm_smart( + staking_addr.clone(), + &btc_staking::msg::QueryMsg::FinalityProvider { + btc_pk_hex: fp_btc_pk_hex.to_string(), + }, + )?; + + // Ensure the finality provider is not slashed at this time point + // NOTE: It's possible that the finality provider equivocates for height h, and the signature is + // processed at height h' > h. In this case: + // - We should reject any new signature from this finality provider, since it's known to be adversarial. + // - We should set its voting power since height h'+1 to be zero, for the same reason. + // - We should NOT set its voting power between [h, h'] to be zero, since + // - Babylon BTC staking ensures safety upon 2f+1 votes, *even if* f of them are adversarial. + // This is because as long as a block gets 2f+1 votes, any other block with 2f+1 votes has a + // f+1 quorum intersection with this block, contradicting the assumption and leading to + // the safety proof. + // This ensures slashable safety together with EOTS, thus does not undermine Babylon's security guarantee. + // - Due to this reason, when tallying a block, Babylon finalises this block upon 2f+1 votes. If we + // modify voting power table in the history, some finality decisions might be contradicting to the + // signature set and voting power table. + // - To fix the above issue, Babylon has to allow finalised and not-finalised blocks. However, + // this means Babylon will lose safety under an adaptive adversary corrupting even 1 + // finality provider. It can simply corrupt a new finality provider and equivocate a + // historical block over and over again, making a previous block not finalisable forever + if fp.slashed_height > 0 && fp.slashed_height < height { + return Err(ContractError::FinalityProviderAlreadySlashed( + fp_btc_pk_hex.to_string(), + )); + } + + // Ensure the finality provider has voting power at this height + let fp: FinalityProviderInfo = deps + .querier + .query_wasm_smart( + staking_addr.clone(), + &btc_staking::msg::QueryMsg::FinalityProviderInfo { + btc_pk_hex: fp_btc_pk_hex.to_string(), + height: Some(height), + }, + ) + .map_err(|_| ContractError::NoVotingPower(fp_btc_pk_hex.to_string(), height))?; + if fp.power == 0 { + return Err(ContractError::NoVotingPower( + fp_btc_pk_hex.to_string(), + height, + )); + } + + // Ensure the signature is not empty + if signature.is_empty() { + return Err(ContractError::EmptySignature); + } + // Ensure the height is proper + if env.block.height < height { + return Err(ContractError::HeightTooHigh); + } + // Ensure the finality provider has not cast the same vote yet + let existing_sig = SIGNATURES.may_load(deps.storage, (height, fp_btc_pk_hex))?; + match existing_sig { + Some(existing_sig) if existing_sig == signature => { + deps.api.debug(&format!("Received duplicated finality vote. Height: {height}, Finality Provider: {fp_btc_pk_hex}")); + // Exactly the same vote already exists, return success to the provider + return Ok(Response::new()); + } + _ => {} + } + + // Find the public randomness commitment for this height from this finality provider + let pr_commit = get_pub_rand_commit_for_height(deps.storage, fp_btc_pk_hex, height)?; + + // Verify the finality signature message + verify_finality_signature( + fp_btc_pk_hex, + height, + pub_rand, + proof, + &pr_commit, + block_app_hash, + signature, + )?; + + // The public randomness value is good, save it. + // 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())?; + + // Verify whether the voted block is a fork or not + // TODO?: Do not rely on 'canonical' (i.e. BFT-consensus provided) blocks info + let indexed_block = BLOCKS + .load(deps.storage, height) + .map_err(|err| ContractError::BlockNotFound(height, err.to_string()))?; + + let mut res = Response::new(); + if indexed_block.app_hash != block_app_hash { + // The finality provider votes for a fork! + + // Construct evidence + let mut evidence = Evidence { + fp_btc_pk: hex::decode(fp_btc_pk_hex)?, + block_height: height, + pub_rand: pub_rand.to_vec(), + canonical_app_hash: indexed_block.app_hash, + canonical_finality_sig: vec![], + fork_app_hash: block_app_hash.to_vec(), + fork_finality_sig: signature.to_vec(), + }; + + // If this finality provider has also signed the canonical block, slash it + let canonical_sig = SIGNATURES.may_load(deps.storage, (height, fp_btc_pk_hex))?; + if let Some(canonical_sig) = canonical_sig { + // Set canonical sig + evidence.canonical_finality_sig = canonical_sig; + // Slash this finality provider, including setting its voting power to zero, extracting + // its BTC SK, and emitting an event + let (msg, ev) = slash_finality_provider(&mut deps, fp_btc_pk_hex, &evidence)?; + res = res.add_message(msg); + res = res.add_event(ev); + } + // TODO?: Also slash if this finality provider has signed another fork before + + // Save evidence + EVIDENCES.save(deps.storage, (fp_btc_pk_hex, height), &evidence)?; + + // NOTE: We should NOT return error here, otherwise the state change triggered in this tx + // (including the evidence) will be rolled back + return Ok(res); + } + + // This signature is good, save the vote to the store + SIGNATURES.save(deps.storage, (height, fp_btc_pk_hex), &signature.to_vec())?; + + // If this finality provider has signed the canonical block before, slash it via extracting its + // secret key, and emit an event + if let Some(mut evidence) = EVIDENCES.may_load(deps.storage, (fp_btc_pk_hex, height))? { + // The finality provider has voted for a fork before! + // This evidence is at the same height as this signature, slash this finality provider + + // Set canonical sig to this evidence + evidence.canonical_finality_sig = signature.to_vec(); + EVIDENCES.save(deps.storage, (fp_btc_pk_hex, height), &evidence)?; + + // Slash this finality provider, including setting its voting power to zero, extracting its + // BTC SK, and emitting an event + let (msg, ev) = slash_finality_provider(&mut deps, fp_btc_pk_hex, &evidence)?; + res = res.add_message(msg); + res = res.add_event(ev); + } + + Ok(res) +} + +/// `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( + deps: &mut DepsMut, + 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. + // Send to babylon-contract for forwarding + let msg = babylon_contract::ExecuteMsg::Slashing { + evidence: evidence.clone(), + }; + + let babylon_addr = CONFIG.load(deps.storage)?.babylon; + + let wasm_msg = WasmMsg::Execute { + contract_addr: babylon_addr.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)) +} + +/// Verifies the finality signature message w.r.t. the public randomness commitment: +/// - Public randomness inclusion proof. +/// - Finality signature +fn verify_finality_signature( + fp_btc_pk_hex: &str, + block_height: u64, + pub_rand: &[u8], + proof: &Proof, + pr_commit: &PubRandCommit, + app_hash: &[u8], + signature: &[u8], +) -> Result<(), ContractError> { + let proof_height = pr_commit.start_height + proof.index; + if block_height != proof_height { + return Err(ContractError::InvalidFinalitySigHeight( + proof_height, + block_height, + )); + } + // Verify the total amount of randomness is the same as in the commitment + if proof.total != pr_commit.num_pub_rand { + return Err(ContractError::InvalidFinalitySigAmount( + proof.total, + pr_commit.num_pub_rand, + )); + } + // Verify the proof of inclusion for this public randomness + proof.validate_basic()?; + proof.verify(&pr_commit.commitment, pub_rand)?; + + // Public randomness is good, verify finality signature + let pubkey = eots::PublicKey::from_hex(fp_btc_pk_hex)?; + let msg = msg_to_sign(block_height, app_hash); + let msg_hash = Sha256::digest(msg); + + if !pubkey.verify(pub_rand, &msg_hash, signature)? { + return Err(ContractError::FailedSignatureVerification("EOTS".into())); + } + Ok(()) +} + +/// `msg_to_sign` returns the message for an EOTS signature. +/// +/// The EOTS signature on a block will be (block_height || block_hash) +fn msg_to_sign(height: u64, block_hash: &[u8]) -> Vec { + let mut msg: Vec = height.to_be_bytes().to_vec(); + msg.extend_from_slice(block_hash); + msg +} + +pub fn index_block( + deps: &mut DepsMut, + height: u64, + app_hash: &[u8], +) -> Result { + let indexed_block = IndexedBlock { + height, + app_hash: app_hash.into(), + finalized: false, + }; + BLOCKS.save(deps.storage, height, &indexed_block)?; + + // Register the indexed block height + let ev = Event::new("index_block") + .add_attribute("module", "finality") + .add_attribute("last_height", height.to_string()); + Ok(ev) +} + +/// TallyBlocks tries to finalise all blocks that are non-finalised AND have a non-nil +/// finality provider set, from the earliest to the latest. +/// +/// This function is invoked upon each `EndBlock`, after the BTC staking protocol is activated. +/// It ensures that at height `h`, the ancestor chain `[activated_height, h-1]` contains either +/// - finalised blocks (i.e., blocks with a finality provider set AND QC of this finality provider set), +/// - non-finalisable blocks (i.e. blocks with no active finality providers), +/// but no blocks that have a finality provider set and do not receive a QC +/// +/// It must be invoked only after the BTC staking protocol is activated. +pub fn tally_blocks( + deps: &mut DepsMut, + activated_height: u64, + height: u64, +) -> Result, ContractError> { + // Start finalising blocks since max(activated_height, next_height) + let next_height = NEXT_HEIGHT.may_load(deps.storage)?.unwrap_or(0); + let start_height = max(activated_height, next_height); + + // Find all blocks that are non-finalised AND have a finality provider set since + // max(activated_height, last_finalized_height + 1) + // There are 4 different scenarios: + // - Has finality providers, non-finalised: Tally and try to finalise. + // - Does not have finality providers, non-finalised: Non-finalisable, continue. + // - Has finality providers, finalised: Impossible, panic. + // - Does not have finality providers, finalised: Impossible, panic. + // After this for loop, the blocks since the earliest activated height are either finalised or + // non-finalisable + let mut events = vec![]; + for h in start_height..=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)?; + + match (fp_set, indexed_block.finalized) { + (Some(fp_set), false) => { + // Has finality providers, non-finalised: tally and try to finalise the block + let voter_btc_pks = SIGNATURES + .prefix(indexed_block.height) + .keys(deps.storage, None, None, Ascending) + .collect::>>()?; + if tally(&fp_set, &voter_btc_pks) { + // If this block gets >2/3 votes, finalise it + let ev = finalize_block(deps.storage, &mut indexed_block, &voter_btc_pks)?; + events.push(ev); + } else { + // If not, then this block and all subsequent blocks should not be finalised. + // Thus, we need to break here + break; + } + } + (None, false) => { + // Does not have finality providers, non-finalised: not finalisable, + // Increment the next height to finalise and continue + NEXT_HEIGHT.save(deps.storage, &(indexed_block.height + 1))?; + continue; + } + (Some(_), true) => { + // Has finality providers and the block is finalised. + // This can only be a programming error + return Err(ContractError::FinalisedBlockWithFinalityProviderSet( + indexed_block.height, + )); + } + (None, true) => { + // Does not have finality providers, finalised: impossible to happen + return Err(ContractError::FinalisedBlockWithoutFinalityProviderSet( + indexed_block.height, + )); + } + } + } + Ok(events) +} + +/// `tally` checks whether a block with the given finality provider set and votes reaches a quorum +/// or not +fn tally(fp_set: &[FinalityProviderInfo], voters: &[String]) -> bool { + let voters: HashSet = voters.iter().cloned().collect(); + let mut total_power = 0; + let mut voted_power = 0; + for fp_info in fp_set { + total_power += fp_info.power; + if voters.contains(&fp_info.btc_pk_hex) { + voted_power += fp_info.power; + } + } + voted_power * 3 > total_power * 2 +} + +/// `finalize_block` sets a block to be finalised, and distributes rewards to finality providers +/// and delegators +fn finalize_block( + store: &mut dyn Storage, + block: &mut IndexedBlock, + _voters: &[String], +) -> Result { + // Set block to be finalised + block.finalized = true; + BLOCKS.save(store, block.height, 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") + .add_attribute("finalized_height", block.height.to_string()); + Ok(ev) +} + +const QUERY_LIMIT: Option = Some(30); + +/// `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( + deps: &mut DepsMut, + env: Env, + max_active_fps: usize, +) -> Result<(), ContractError> { + let cfg = CONFIG.load(deps.storage)?; + // Get all finality providers from the staking contract, filtered + let mut batch = list_fps_by_power(&cfg.staking, &deps.querier, None, QUERY_LIMIT)?; + + let mut finality_providers = vec![]; + let mut total_power: u64 = 0; + while !batch.is_empty() && finality_providers.len() < max_active_fps { + let last = batch.last().cloned(); + + let (filtered, running_total): (Vec<_>, Vec<_>) = batch + .into_iter() + .filter(|fp| { + // Filter out FPs with no voting power + fp.power > 0 + }) + .scan(total_power, |acc, fp| { + *acc += fp.power; + Some((fp, *acc)) + }) + .unzip(); + finality_providers.extend_from_slice(&filtered); + total_power = running_total.last().copied().unwrap_or_default(); + + // and get the next page + batch = list_fps_by_power(&cfg.staking, &deps.querier, last, QUERY_LIMIT)?; + } + + // TODO: Online FPs verification + // 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)?; + + Ok(()) +} + +pub fn list_fps_by_power( + staking_addr: &Addr, + querier: &QuerierWrapper, + start_after: Option, + limit: Option, +) -> StdResult> { + let query = encode_smart_query( + staking_addr, + &btc_staking::msg::QueryMsg::FinalityProvidersByPower { start_after, limit }, + )?; + let res: FinalityProvidersByPowerResponse = querier.query(&query)?; + Ok(res.fps) +} diff --git a/contracts/btc-finality/src/lib.rs b/contracts/btc-finality/src/lib.rs new file mode 100644 index 00000000..18690be3 --- /dev/null +++ b/contracts/btc-finality/src/lib.rs @@ -0,0 +1,10 @@ +mod finality; + +pub mod contract; +pub mod error; +pub mod msg; +pub mod queries; +pub mod state; + +#[cfg(test)] +mod multitest; diff --git a/contracts/btc-finality/src/msg.rs b/contracts/btc-finality/src/msg.rs new file mode 100644 index 00000000..8210420b --- /dev/null +++ b/contracts/btc-finality/src/msg.rs @@ -0,0 +1,100 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cw_controllers::AdminResponse; + +use babylon_apis::finality_api::{Evidence, IndexedBlock, PubRandCommit}; + +use crate::state::config::{Config, Params}; + +#[cw_serde] +#[derive(Default)] +pub struct InstantiateMsg { + pub params: Option, + pub admin: Option, +} + +pub type ExecuteMsg = babylon_apis::finality_api::ExecuteMsg; + +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + /// `Config` returns the current configuration of the btc-finality contract + #[returns(Config)] + Config {}, + /// `Params` returns the current Consumer-specific parameters of the btc-finality contract + #[returns(Params)] + Params {}, + /// `Admin` returns the current admin of the contract + #[returns(AdminResponse)] + Admin {}, + /// `FinalitySignature` returns the signature of the finality provider for a given block height + /// + #[returns(FinalitySignatureResponse)] + FinalitySignature { btc_pk_hex: String, height: u64 }, + /// `PubRandCommit` returns the public random commitments for a given FP. + /// + /// `btc_pk_hex` is the BTC public key of the finality provider, in hex format. + /// + /// `start_after` is the height of to start after (before, if `reverse` is `true`), + /// or `None` to start from the beginning (end, if `reverse` is `true`). + /// `limit` is the maximum number of commitments to return. + /// `reverse` is an optional flag to return the commitments in reverse order + #[returns(PubRandCommit)] + PubRandCommit { + btc_pk_hex: String, + start_after: Option, + limit: Option, + reverse: Option, + }, + /// `FirstPubRandCommit` returns the first public random commitment (if any) for a given FP. + /// + /// It's a convenience shortcut of `PubRandCommit` with a `limit` of 1, and `reverse` set to + /// false. + /// + /// `btc_pk_hex` is the BTC public key of the finality provider, in hex format. + #[returns(Option)] + FirstPubRandCommit { btc_pk_hex: String }, + /// `LastPubRandCommit` returns the last public random commitment (if any) for a given FP. + /// + /// It's a convenience shortcut of `PubRandCommit` with a `limit` of 1, and `reverse` set to + /// true. + /// + /// `btc_pk_hex` is the BTC public key of the finality provider, in hex format. + #[returns(Option)] + LastPubRandCommit { btc_pk_hex: String }, + /// `Block` returns the indexed block information at height + /// + #[returns(IndexedBlock)] + Block { height: u64 }, + /// `Blocks` return the list of indexed blocks. + /// + /// `start_after` is the height of the block to start after (before, if `reverse` is `true`), + /// or `None` to start from the beginning (end, if `reverse` is `true`). + /// `limit` is the maximum number of blocks to return. + /// `finalised` is an optional filter to return only finalised blocks. + /// `reverse` is an optional flag to return the blocks in reverse order + #[returns(BlocksResponse)] + Blocks { + start_after: Option, + limit: Option, + finalised: Option, + reverse: Option, + }, + /// `Evidence` returns the evidence for a given FP and block height + #[returns(EvidenceResponse)] + Evidence { btc_pk_hex: String, height: u64 }, +} + +#[cw_serde] +pub struct FinalitySignatureResponse { + pub signature: Vec, +} + +#[cw_serde] +pub struct BlocksResponse { + pub blocks: Vec, +} + +#[cw_serde] +pub struct EvidenceResponse { + pub evidence: Option, +} diff --git a/contracts/btc-finality/src/multitest.rs b/contracts/btc-finality/src/multitest.rs new file mode 100644 index 00000000..096c77e3 --- /dev/null +++ b/contracts/btc-finality/src/multitest.rs @@ -0,0 +1,415 @@ +mod suite; + +use cosmwasm_std::Addr; +use suite::SuiteBuilder; + +// Some multi-test default settings +// TODO: Replace these with their address generators +// Babylon contract +const CONTRACT0_ADDR: &str = "cosmwasm19mfs8tl4s396u7vqw9rrnsmrrtca5r66p7v8jvwdxvjn3shcmllqupdgxu"; +// BTC Staking contract +const CONTRACT1_ADDR: &str = "cosmwasm14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s8jef58"; +// BTC Finality contract +const CONTRACT2_ADDR: &str = "cosmwasm1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrqt8utkp"; + +mod instantiation { + use super::*; + + #[test] + fn instantiate_works() { + let suite = SuiteBuilder::new().build(); + + // Confirm the btc-staking contract has been instantiated and set + let config = suite.get_babylon_config(); + assert_eq!(config.btc_staking, Some(Addr::unchecked(CONTRACT1_ADDR))); + // Confirm the btc-finality contract has been instantiated and set + assert_eq!(config.btc_finality, Some(Addr::unchecked(CONTRACT2_ADDR))); + // Check that the btc-staking contract was initialized correctly + let btc_staking_config = suite.get_btc_staking_config(); + assert_eq!(btc_staking_config.babylon, Addr::unchecked(CONTRACT0_ADDR)); + + // Check that the btc-finality contract was initialized correctly + let btc_finality_config = suite.get_btc_finality_config(); + assert_eq!(btc_finality_config.babylon, Addr::unchecked(CONTRACT0_ADDR)); + } +} + +mod finality { + use super::*; + + use crate::contract::tests::{ + create_new_finality_provider, get_derived_btc_delegation, get_public_randomness_commitment, + }; + use crate::msg::FinalitySignatureResponse; + use babylon_apis::finality_api::IndexedBlock; + + use cosmwasm_std::Event; + use test_utils::{get_add_finality_sig, get_pub_rand_value}; + + #[test] + fn commit_public_randomness_works() { + let mut suite = SuiteBuilder::new().build(); + + // Read public randomness commitment test data + let (pk_hex, pub_rand, pubrand_signature) = get_public_randomness_commitment(); + + // Register one FP + // NOTE: the test data ensures that pub rand commit / finality sig are + // signed by the 1st FP + let new_fp = create_new_finality_provider(1); + assert_eq!(new_fp.btc_pk_hex, pk_hex); + + suite.register_finality_providers(&[new_fp]).unwrap(); + + // Now commit the public randomness for it + suite + .commit_public_randomness(&pk_hex, &pub_rand, &pubrand_signature) + .unwrap(); + } + + #[test] + fn finality_signature_happy_path() { + // Read public randomness commitment test data + let (pk_hex, pub_rand, pubrand_signature) = get_public_randomness_commitment(); + let pub_rand_one = get_pub_rand_value(); + // Read equivalent / consistent add finality signature test data + let add_finality_signature = get_add_finality_sig(); + let proof = add_finality_signature.proof.unwrap(); + + let initial_height = pub_rand.start_height; + + let mut suite = SuiteBuilder::new().with_height(initial_height).build(); + + // Register one FP + // NOTE: the test data ensures that pub rand commit / finality sig are + // signed by the 1st FP + let new_fp = create_new_finality_provider(1); + + suite.register_finality_providers(&[new_fp]).unwrap(); + + // Activated height is not set + let res = suite.get_activated_height(); + assert_eq!(res.height, 0); + + // Add a delegation, so that the finality provider has some power + let mut del1 = get_derived_btc_delegation(1, &[1]); + del1.fp_btc_pk_list = vec![pk_hex.clone()]; + + suite.add_delegations(&[del1]).unwrap(); + + // Activated height is now set + let res = suite.get_activated_height(); + assert_eq!(res.height, initial_height + 1); + + suite + .commit_public_randomness(&pk_hex, &pub_rand, &pubrand_signature) + .unwrap(); + + // Call the begin-block sudo handler(s), for completeness + let res = suite + .call_begin_block(&add_finality_signature.block_app_hash, initial_height + 1) + .unwrap(); + assert_eq!(1, res.events.len()); + assert_eq!( + res.events[0], + Event::new("sudo").add_attribute("_contract_address", CONTRACT2_ADDR) + ); + + // Call the end-block sudo handler(s), so that the block is indexed in the store + let res = suite + .call_end_block(&add_finality_signature.block_app_hash, initial_height + 1) + .unwrap(); + assert_eq!(2, res.events.len()); + assert_eq!( + res.events[0], + Event::new("sudo").add_attribute("_contract_address", CONTRACT2_ADDR) + ); + assert_eq!( + res.events[1], + Event::new("wasm-index_block") + .add_attribute("_contract_address", CONTRACT2_ADDR) + .add_attribute("module", "finality") + .add_attribute("last_height", (initial_height + 1).to_string()) + ); + + // Submit a finality signature from that finality provider at height initial_height + 1 + let finality_sig = add_finality_signature.finality_sig.to_vec(); + suite + .submit_finality_signature( + &pk_hex, + initial_height + 1, + &pub_rand_one, + &proof, + &add_finality_signature.block_app_hash, + &finality_sig, + ) + .unwrap(); + + // Query finality signature for that exact height + let sig = suite.get_finality_signature(&pk_hex, initial_height + 1); + assert_eq!( + sig, + FinalitySignatureResponse { + signature: finality_sig + } + ); + } + + #[test] + fn finality_round_works() { + // Read public randomness commitment test data + let (pk_hex, pub_rand, pubrand_signature) = get_public_randomness_commitment(); + let pub_rand_one = get_pub_rand_value(); + // Read equivalent / consistent add finality signature test data + let add_finality_signature = get_add_finality_sig(); + let proof = add_finality_signature.proof.unwrap(); + + let initial_height = pub_rand.start_height; + + let mut suite = SuiteBuilder::new().with_height(initial_height).build(); + + // signed by the 1st FP + let new_fp = create_new_finality_provider(1); + assert_eq!(new_fp.btc_pk_hex, pk_hex); + + suite + .register_finality_providers(&[new_fp.clone()]) + .unwrap(); + + // Add a delegation, so that the finality provider has some power + let mut del1 = get_derived_btc_delegation(1, &[1]); + del1.fp_btc_pk_list = vec![pk_hex.clone()]; + + suite.add_delegations(&[del1.clone()]).unwrap(); + + // Check that the finality provider power has been updated + let fp_info = suite.get_finality_provider_info(&new_fp.btc_pk_hex, None); + assert_eq!(fp_info.power, del1.total_sat); + + // Submit public randomness commitment for the FP and the involved heights + suite + .commit_public_randomness(&pk_hex, &pub_rand, &pubrand_signature) + .unwrap(); + + // Call the begin-block sudo handler, for completeness + suite + .call_begin_block(&add_finality_signature.block_app_hash, initial_height + 1) + .unwrap(); + + // Call the end-block sudo handler, so that the block is indexed in the store + suite + .call_end_block(&add_finality_signature.block_app_hash, initial_height + 1) + .unwrap(); + + // Submit a finality signature from that finality provider at height initial_height + 1 + let submit_height = initial_height + 1; + let finality_sig = add_finality_signature.finality_sig.to_vec(); + suite + .submit_finality_signature( + &pk_hex, + submit_height, + &pub_rand_one, + &proof, + &add_finality_signature.block_app_hash, + &finality_sig, + ) + .unwrap(); + + // Call the begin blocker, to compute the active FP set + suite + .call_begin_block(&add_finality_signature.block_app_hash, submit_height) + .unwrap(); + + // Call the end blocker, to process the finality signatures + let res = suite + .call_end_block(&add_finality_signature.block_app_hash, submit_height) + .unwrap(); + assert_eq!(3, res.events.len()); + assert_eq!( + res.events[0], + Event::new("sudo").add_attribute("_contract_address", CONTRACT2_ADDR) + ); + assert_eq!( + res.events[1], + Event::new("wasm-index_block") + .add_attribute("_contract_address", CONTRACT2_ADDR) + .add_attribute("module", "finality") + .add_attribute("last_height", submit_height.to_string()) + ); + assert_eq!( + res.events[2], + Event::new("wasm-finalize_block") + .add_attribute("_contract_address", CONTRACT2_ADDR) + .add_attribute("module", "finality") + .add_attribute("finalized_height", submit_height.to_string()) + ); + + // Assert the submitted block has been indexed and finalised + let indexed_block = suite.get_indexed_block(submit_height); + assert_eq!( + indexed_block, + IndexedBlock { + height: submit_height, + app_hash: add_finality_signature.block_app_hash.to_vec(), + finalized: true, + } + ); + } +} + +mod slashing { + use babylon_apis::finality_api::IndexedBlock; + use test_utils::{get_add_finality_sig, get_add_finality_sig_2, get_pub_rand_value}; + + use crate::contract::tests::{ + create_new_finality_provider, get_derived_btc_delegation, get_public_randomness_commitment, + }; + use crate::multitest::suite::SuiteBuilder; + + #[test] + fn slashing_works() { + // Read public randomness commitment test data + let (pk_hex, pub_rand, pubrand_signature) = get_public_randomness_commitment(); + let pub_rand_one = get_pub_rand_value(); + // Read equivalent / consistent add finality signature test data + let add_finality_signature = get_add_finality_sig(); + let proof = add_finality_signature.proof.unwrap(); + + let initial_height = pub_rand.start_height; + + let mut suite = SuiteBuilder::new().with_height(initial_height).build(); + + // Register one FP + // NOTE: the test data ensures that pub rand commit / finality sig are + // signed by the 1st FP + let new_fp = create_new_finality_provider(1); + + suite + .register_finality_providers(&[new_fp.clone()]) + .unwrap(); + + // Add a delegation, so that the finality provider has some power + let mut del1 = get_derived_btc_delegation(1, &[1]); + del1.fp_btc_pk_list = vec![pk_hex.clone()]; + + suite.add_delegations(&[del1.clone()]).unwrap(); + + // Check that the finality provider power has been updated + let fp_info = suite.get_finality_provider_info(&new_fp.btc_pk_hex, None); + assert_eq!(fp_info.power, del1.total_sat); + + // Submit public randomness commitment for the FP and the involved heights + suite + .commit_public_randomness(&pk_hex, &pub_rand, &pubrand_signature) + .unwrap(); + + // Call the begin-block sudo handler at the next height, for completeness + let next_height = initial_height + 1; + suite.app.advance_blocks(next_height - initial_height); + suite + .call_begin_block(&add_finality_signature.block_app_hash, next_height) + .unwrap(); + + // Call the end-block sudo handler, so that the block is indexed in the store + suite + .call_end_block(&add_finality_signature.block_app_hash, next_height) + .unwrap(); + + // Submit a finality signature from that finality provider at next height (initial_height + 1) + let submit_height = next_height; + // Increase block height + let next_height = next_height + 1; + suite.app.advance_blocks(next_height - submit_height); + // Call the begin-block sudo handler at the next height, for completeness + suite + .call_begin_block(&add_finality_signature.block_app_hash, next_height) + .unwrap(); + + let finality_signature = add_finality_signature.finality_sig.to_vec(); + suite + .submit_finality_signature( + &pk_hex, + submit_height, + &pub_rand_one, + &proof, + &add_finality_signature.block_app_hash, + &finality_signature, + ) + .unwrap(); + + // Submitting the same signature twice is tolerated + suite + .submit_finality_signature( + &pk_hex, + submit_height, + &pub_rand_one, + &proof, + &add_finality_signature.block_app_hash, + &finality_signature, + ) + .unwrap(); + + // Submit another (different and valid) finality signature, from the same finality provider + // at the same height, and with the same proof + let add_finality_signature_2 = get_add_finality_sig_2(); + let res = suite + .submit_finality_signature( + &pk_hex, + submit_height, + &pub_rand_one, + &proof, + &add_finality_signature_2.block_app_hash, + &add_finality_signature_2.finality_sig, + ) + .unwrap(); + + // Assert the double signing evidence is proper + let btc_pk = hex::decode(pk_hex.clone()).unwrap(); + let evidence = suite + .get_double_signing_evidence(&pk_hex, submit_height) + .evidence + .unwrap(); + assert_eq!(evidence.block_height, submit_height); + assert_eq!(evidence.fp_btc_pk, btc_pk); + + // Assert the slashing event is there + assert_eq!(4, res.events.len()); + // Assert the slashing event is proper (slashing is the 2nd event in the list) + assert_eq!( + res.events[1].ty, + "wasm-slashed_finality_provider".to_string() + ); + + // Call the end-block sudo handler for completeness / realism + suite + .call_end_block(&add_finality_signature_2.block_app_hash, next_height) + .unwrap(); + + // Call the next (final) block begin blocker, to compute the active FP set + let final_height = next_height + 1; + suite.app.advance_blocks(final_height - next_height); + suite + .call_begin_block("deadbeef02".as_bytes(), final_height) + .unwrap(); + + // Call the next (final) block end blocker, to process the finality signatures + suite + .call_end_block("deadbeef02".as_bytes(), final_height) + .unwrap(); + + // Assert the canonical block has been indexed (and finalised) + let indexed_block = suite.get_indexed_block(submit_height); + assert_eq!( + indexed_block, + IndexedBlock { + height: submit_height, + app_hash: add_finality_signature.block_app_hash.to_vec(), + finalized: true, + } + ); + + // Assert the finality provider has been slashed + let fp = suite.get_finality_provider(&pk_hex); + assert_eq!(fp.slashed_height, next_height); + } +} diff --git a/contracts/btc-finality/src/multitest/suite.rs b/contracts/btc-finality/src/multitest/suite.rs new file mode 100644 index 00000000..9c174333 --- /dev/null +++ b/contracts/btc-finality/src/multitest/suite.rs @@ -0,0 +1,373 @@ +use anyhow::Result as AnyResult; +use derivative::Derivative; +use hex::ToHex; + +use cosmwasm_std::Addr; + +use cw_multi_test::{AppResponse, Contract, ContractWrapper, Executor}; + +use babylon_apis::btc_staking_api::{ActiveBtcDelegation, FinalityProvider, NewFinalityProvider}; +use babylon_apis::finality_api::{IndexedBlock, PubRandCommit}; +use babylon_apis::{btc_staking_api, finality_api}; +use babylon_bindings::BabylonMsg; +use babylon_bindings_test::BabylonApp; +use babylon_bitcoin::chain_params::Network; +use btc_staking::msg::{ActivatedHeightResponse, FinalityProviderInfo}; + +use crate::msg::{EvidenceResponse, FinalitySignatureResponse}; +use crate::multitest::{CONTRACT1_ADDR, CONTRACT2_ADDR}; + +fn contract_btc_staking() -> Box> { + let contract = ContractWrapper::new( + btc_staking::contract::execute, + btc_staking::contract::instantiate, + btc_staking::contract::query, + ); + Box::new(contract) +} + +fn contract_btc_finality() -> Box> { + let contract = ContractWrapper::new( + crate::contract::execute, + crate::contract::instantiate, + crate::contract::query, + ) + .with_sudo(crate::contract::sudo); + Box::new(contract) +} + +fn contract_babylon() -> Box> { + let contract = ContractWrapper::new( + babylon_contract::execute, + babylon_contract::instantiate, + babylon_contract::query, + ) + .with_reply(babylon_contract::reply) + .with_migrate(babylon_contract::migrate); + Box::new(contract) +} + +#[derive(Derivative)] +#[derivative(Default = "new")] +pub struct SuiteBuilder { + height: Option, +} + +impl SuiteBuilder { + pub fn with_height(mut self, height: u64) -> Self { + self.height = Some(height); + self + } + + #[track_caller] + pub fn build(self) -> Suite { + let owner = Addr::unchecked("owner"); + + let mut app = BabylonApp::new_at_height(owner.as_str(), self.height.unwrap_or(1)); + + let _block_info = app.block_info(); + + app.init_modules(|_router, _api, _storage| -> AnyResult<()> { Ok(()) }) + .unwrap(); + + let btc_staking_code_id = + app.store_code_with_creator(owner.clone(), contract_btc_staking()); + let btc_finality_code_id = + app.store_code_with_creator(owner.clone(), contract_btc_finality()); + let contract_code_id = app.store_code_with_creator(owner.clone(), contract_babylon()); + let contract = app + .instantiate_contract( + contract_code_id, + owner.clone(), + &babylon_contract::msg::contract::InstantiateMsg { + network: Network::Testnet, + babylon_tag: "01020304".to_string(), + btc_confirmation_depth: 1, + checkpoint_finalization_timeout: 10, + notify_cosmos_zone: false, + btc_staking_code_id: Some(btc_staking_code_id), + btc_staking_msg: None, + btc_finality_code_id: Some(btc_finality_code_id), + btc_finality_msg: None, + admin: Some(owner.to_string()), + consumer_name: Some("TestConsumer".to_string()), + consumer_description: Some("Test Consumer Description".to_string()), + }, + &[], + "babylon", + Some(owner.to_string()), + ) + .unwrap(); + + Suite { + app, + code_id: contract_code_id, + babylon: contract, + staking: Addr::unchecked(CONTRACT1_ADDR), + finality: Addr::unchecked(CONTRACT2_ADDR), + owner, + } + } +} + +#[derive(Derivative)] +#[derivative(Debug)] +pub struct Suite { + #[derivative(Debug = "ignore")] + pub app: BabylonApp, + /// The code id of the babylon contract + code_id: u64, + /// Babylon contract address + pub babylon: Addr, + /// Staking contract address + pub staking: Addr, + /// Finality contract address + pub finality: Addr, + /// Admin of babylon and btc-staking contracts + pub owner: Addr, +} + +impl Suite { + #[allow(dead_code)] + pub fn admin(&self) -> &str { + self.owner.as_str() + } + + #[track_caller] + pub fn get_babylon_config(&self) -> babylon_contract::state::config::Config { + self.app + .wrap() + .query_wasm_smart( + self.babylon.clone(), + &babylon_contract::msg::contract::QueryMsg::Config {}, + ) + .unwrap() + } + + #[track_caller] + pub fn get_btc_staking_config(&self) -> btc_staking::state::config::Config { + self.app + .wrap() + .query_wasm_smart(self.staking.clone(), &btc_staking::msg::QueryMsg::Config {}) + .unwrap() + } + + #[track_caller] + pub fn get_btc_finality_config(&self) -> crate::state::config::Config { + self.app + .wrap() + .query_wasm_smart(self.finality.clone(), &crate::msg::QueryMsg::Config {}) + .unwrap() + } + + #[track_caller] + pub fn get_activated_height(&self) -> ActivatedHeightResponse { + self.app + .wrap() + .query_wasm_smart( + self.staking.clone(), + &btc_staking::msg::QueryMsg::ActivatedHeight {}, + ) + .unwrap() + } + + #[track_caller] + pub fn get_finality_provider(&self, pk_hex: &str) -> FinalityProvider { + self.app + .wrap() + .query_wasm_smart( + self.staking.clone(), + &btc_staking::msg::QueryMsg::FinalityProvider { + btc_pk_hex: pk_hex.to_string(), + }, + ) + .unwrap() + } + + #[track_caller] + pub fn get_finality_provider_info( + &self, + pk_hex: &str, + height: Option, + ) -> FinalityProviderInfo { + self.app + .wrap() + .query_wasm_smart( + self.staking.clone(), + &btc_staking::msg::QueryMsg::FinalityProviderInfo { + btc_pk_hex: pk_hex.to_string(), + height, + }, + ) + .unwrap() + } + + #[track_caller] + pub fn get_finality_signature(&self, pk_hex: &str, height: u64) -> FinalitySignatureResponse { + self.app + .wrap() + .query_wasm_smart( + self.finality.clone(), + &crate::msg::QueryMsg::FinalitySignature { + btc_pk_hex: pk_hex.to_string(), + height, + }, + ) + .unwrap() + } + + #[track_caller] + pub fn get_indexed_block(&self, height: u64) -> IndexedBlock { + self.app + .wrap() + .query_wasm_smart( + self.finality.clone(), + &crate::msg::QueryMsg::Block { height }, + ) + .unwrap() + } + + #[track_caller] + pub fn get_double_signing_evidence(&self, pk_hex: &str, height: u64) -> EvidenceResponse { + self.app + .wrap() + .query_wasm_smart( + self.finality.clone(), + &crate::msg::QueryMsg::Evidence { + btc_pk_hex: pk_hex.to_string(), + height, + }, + ) + .unwrap() + } + + #[track_caller] + pub fn register_finality_providers( + &mut self, + fps: &[NewFinalityProvider], + ) -> anyhow::Result { + self.app.execute_contract( + self.babylon.clone(), + self.staking.clone(), + &btc_staking_api::ExecuteMsg::BtcStaking { + new_fp: fps.to_vec(), + active_del: vec![], + slashed_del: vec![], + unbonded_del: vec![], + }, + &[], + ) + } + + #[track_caller] + pub fn add_delegations(&mut self, dels: &[ActiveBtcDelegation]) -> anyhow::Result { + self.app.execute_contract( + self.babylon.clone(), + self.staking.clone(), + &btc_staking_api::ExecuteMsg::BtcStaking { + new_fp: vec![], + active_del: dels.to_vec(), + slashed_del: vec![], + unbonded_del: vec![], + }, + &[], + ) + } + + #[track_caller] + pub fn commit_public_randomness( + &mut self, + pk_hex: &str, + pub_rand: &PubRandCommit, + pubrand_signature: &[u8], + ) -> anyhow::Result { + self.app.execute_contract( + Addr::unchecked("anyone"), + self.finality.clone(), + &finality_api::ExecuteMsg::CommitPublicRandomness { + fp_pubkey_hex: pk_hex.to_string(), + start_height: pub_rand.start_height, + num_pub_rand: pub_rand.num_pub_rand, + commitment: pub_rand.commitment.clone().into(), + signature: pubrand_signature.into(), + }, + &[], + ) + } + + #[track_caller] + pub fn call_begin_block( + &mut self, + app_hash: &[u8], + height: u64, + ) -> anyhow::Result { + // Set the block height + let mut block = self.app.block_info(); + block.height = height; + self.app.set_block(block); + + // Hash is not used in the begin-block handler + let hash_hex = format!("deadbeef{}", height); + let app_hash_hex: String = app_hash.encode_hex(); + + self.app.wasm_sudo( + self.finality.clone(), + &finality_api::SudoMsg::BeginBlock { + hash_hex: hash_hex.clone(), + app_hash_hex: app_hash_hex.clone(), + }, + ) + } + + #[track_caller] + pub fn call_end_block(&mut self, app_hash: &[u8], height: u64) -> anyhow::Result { + // Set the block height + let mut block = self.app.block_info(); + block.height = height; + self.app.set_block(block); + + // Hash is not used in the begin-block handler + let hash_hex = format!("deadbeef{}", height); + let app_hash_hex: String = app_hash.encode_hex(); + + self.app.wasm_sudo( + self.finality.clone(), + &finality_api::SudoMsg::EndBlock { + hash_hex: hash_hex.clone(), + app_hash_hex: app_hash_hex.clone(), + }, + ) + } + + #[track_caller] + pub fn submit_finality_signature( + &mut self, + pk_hex: &str, + height: u64, + pub_rand: &[u8], + proof: &tendermint_proto::crypto::Proof, + block_hash: &[u8], + finality_sig: &[u8], + ) -> anyhow::Result { + // Execute the message at a higher height, so that: + // 1. It's not rejected because of height being too low. + // 2. The FP has consolidated power at such height + let mut block = self.app.block_info(); + block.height = height + 1; + self.app.set_block(block); + + self.app.execute_contract( + Addr::unchecked("anyone"), + self.finality.clone(), + &finality_api::ExecuteMsg::SubmitFinalitySignature { + fp_pubkey_hex: pk_hex.to_string(), + height, + pub_rand: pub_rand.into(), + proof: proof.into(), + block_hash: block_hash.into(), + signature: finality_sig.into(), + }, + &[], + ) + } +} diff --git a/contracts/btc-finality/src/queries.rs b/contracts/btc-finality/src/queries.rs new file mode 100644 index 00000000..f6774f8e --- /dev/null +++ b/contracts/btc-finality/src/queries.rs @@ -0,0 +1,79 @@ +use cosmwasm_std::Order::{Ascending, Descending}; +use cosmwasm_std::{Deps, StdResult}; +use cw_storage_plus::Bound; + +use babylon_apis::finality_api::IndexedBlock; + +use crate::error::ContractError; +use crate::msg::{BlocksResponse, EvidenceResponse, FinalitySignatureResponse}; +use crate::state::config::{Config, Params}; +use crate::state::config::{CONFIG, PARAMS}; +use crate::state::finality::{BLOCKS, EVIDENCES, SIGNATURES}; + +pub fn config(deps: Deps) -> StdResult { + CONFIG.load(deps.storage) +} + +pub fn params(deps: Deps) -> StdResult { + PARAMS.load(deps.storage) +} + +// Settings for pagination +const MAX_LIMIT: u32 = 30; +const DEFAULT_LIMIT: u32 = 10; + +pub fn finality_signature( + deps: Deps, + btc_pk_hex: String, + height: u64, +) -> StdResult { + match SIGNATURES.may_load(deps.storage, (height, &btc_pk_hex))? { + Some(sig) => Ok(FinalitySignatureResponse { signature: sig }), + None => Ok(FinalitySignatureResponse { + signature: Vec::new(), + }), // Empty signature response + } +} + +pub fn block(deps: Deps, height: u64) -> StdResult { + BLOCKS.load(deps.storage, height) +} + +/// Get list of blocks. +/// `start_after`: The height to start after, if any. +/// `finalised`: List only finalised blocks if true, otherwise list all blocks. +/// `reverse`: List in descending order if present and true, otherwise in ascending order. +pub fn blocks( + deps: Deps, + start_after: Option, + limit: Option, + finalised: Option, + reverse: Option, +) -> Result { + let finalised = finalised.unwrap_or_default(); + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + let start_after = start_after.map(Bound::exclusive); + let (start, end, order) = if reverse.unwrap_or(false) { + (None, start_after, Descending) + } else { + (start_after, None, Ascending) + }; + let blocks = BLOCKS + .range_raw(deps.storage, start, end, order) + .filter(|item| { + if let Ok((_, block)) = item { + !finalised || block.finalized + } else { + true // don't filter errors + } + }) + .take(limit) + .map(|item| item.map(|(_, v)| v)) + .collect::, _>>()?; + Ok(BlocksResponse { blocks }) +} + +pub fn evidence(deps: Deps, btc_pk_hex: String, height: u64) -> StdResult { + let evidence = EVIDENCES.may_load(deps.storage, (&btc_pk_hex, height))?; + Ok(EvidenceResponse { evidence }) +} diff --git a/contracts/btc-finality/src/state/config.rs b/contracts/btc-finality/src/state/config.rs new file mode 100644 index 00000000..77c18b6b --- /dev/null +++ b/contracts/btc-finality/src/state/config.rs @@ -0,0 +1,35 @@ +use derivative::Derivative; + +use cosmwasm_schema::cw_serde; +use cosmwasm_std::Addr; + +use cw_controllers::Admin; +use cw_storage_plus::Item; + +pub(crate) const CONFIG: Item = Item::new("config"); +pub(crate) const PARAMS: Item = Item::new("params"); +/// Storage for admin +pub(crate) const ADMIN: Admin = Admin::new("admin"); + +/// Config are Babylon-selectable BTC finality configuration +// TODO: Add / enable config entries as needed +#[cw_serde] +pub struct Config { + pub babylon: Addr, + pub staking: Addr, +} + +// TODO: Add / enable param entries as needed +#[cw_serde] +#[derive(Derivative)] +#[derivative(Default)] +pub struct Params { + /// `max_active_finality_providers` is the maximum number of active finality providers in the + /// BTC staking protocol + #[derivative(Default(value = "100"))] + pub max_active_finality_providers: u32, + /// `min_pub_rand` is the minimum amount of public randomness each public randomness commitment + /// should commit + #[derivative(Default(value = "1"))] + pub min_pub_rand: u64, +} diff --git a/contracts/btc-finality/src/state/finality.rs b/contracts/btc-finality/src/state/finality.rs new file mode 100644 index 00000000..06202abf --- /dev/null +++ b/contracts/btc-finality/src/state/finality.rs @@ -0,0 +1,23 @@ +use cw_storage_plus::{Item, Map}; + +use babylon_apis::finality_api::{Evidence, IndexedBlock}; +use btc_staking::msg::FinalityProviderInfo; + +/// Map of signatures by block height and FP +pub const SIGNATURES: Map<(u64, &str), Vec> = Map::new("fp_sigs"); + +/// Map of blocks information by height +pub const BLOCKS: Map = Map::new("blocks"); + +/// Next height to finalise +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"); diff --git a/contracts/btc-finality/src/state/mod.rs b/contracts/btc-finality/src/state/mod.rs new file mode 100644 index 00000000..10d555c9 --- /dev/null +++ b/contracts/btc-finality/src/state/mod.rs @@ -0,0 +1,3 @@ +pub mod config; +pub mod finality; +pub mod public_randomness; diff --git a/contracts/btc-staking/src/state/public_randomness.rs b/contracts/btc-finality/src/state/public_randomness.rs similarity index 93% rename from contracts/btc-staking/src/state/public_randomness.rs rename to contracts/btc-finality/src/state/public_randomness.rs index 8c08d0c1..68227626 100644 --- a/contracts/btc-staking/src/state/public_randomness.rs +++ b/contracts/btc-finality/src/state/public_randomness.rs @@ -6,9 +6,9 @@ use crate::error::ContractError; use babylon_apis::finality_api::PubRandCommit; /// Map of public randomness commitments by fp and block height -pub(crate) const PUB_RAND_COMMITS: Map<(&str, u64), PubRandCommit> = Map::new("fp_pub_rand_commit"); +pub const PUB_RAND_COMMITS: Map<(&str, u64), PubRandCommit> = Map::new("fp_pub_rand_commit"); /// Map of public randomness values by fp and block height -pub(crate) const PUB_RAND_VALUES: Map<(&str, u64), Vec> = Map::new("fp_pub_rand"); +pub const PUB_RAND_VALUES: Map<(&str, u64), Vec> = Map::new("fp_pub_rand"); pub fn get_pub_rand_commit_for_height( storage: &dyn Storage, diff --git a/contracts/btc-finality/tests/integration.rs b/contracts/btc-finality/tests/integration.rs new file mode 100644 index 00000000..08477acf --- /dev/null +++ b/contracts/btc-finality/tests/integration.rs @@ -0,0 +1,35 @@ +use cosmwasm_std::{ContractResult, Response}; +use cosmwasm_vm::testing::{instantiate, mock_env, mock_info, mock_instance}; + +use btc_finality::msg::InstantiateMsg; + +// wasm binary lite version +static WASM: &[u8] = include_bytes!("../../../artifacts/btc_finality.wasm"); +/// Wasm size limit: https://github.com/CosmWasm/wasmd/blob/main/x/wasm/types/validation.go#L24-L25 +const MAX_WASM_SIZE: usize = 800 * 1024; // 800 KB + +const CREATOR: &str = "creator"; + +#[test] +fn wasm_size_limit_check() { + assert!( + WASM.len() < MAX_WASM_SIZE, + "BTC finality contract wasm binary is too large: {} (target: {})", + WASM.len(), + MAX_WASM_SIZE + ); +} + +#[test] +fn instantiate_works() { + let mut deps = mock_instance(WASM, &[]); + + let msg = InstantiateMsg { + params: None, + admin: None, + }; + let info = mock_info(CREATOR, &[]); + let res: ContractResult = instantiate(&mut deps, mock_env(), info, msg); + let msgs = res.unwrap().messages; + assert_eq!(0, msgs.len()); +} diff --git a/contracts/btc-staking/.cargo/config.toml b/contracts/btc-staking/.cargo/config.toml index 6cd8153d..06557618 100644 --- a/contracts/btc-staking/.cargo/config.toml +++ b/contracts/btc-staking/.cargo/config.toml @@ -1,4 +1,4 @@ [alias] unit-test = "test --lib" integration-test = "test --test integration" -schema = "run --bin btcstaking-schema" +schema = "run --bin btc-staking-schema" diff --git a/contracts/btc-staking/Cargo.toml b/contracts/btc-staking/Cargo.toml index 5cd447e0..6a2c4178 100644 --- a/contracts/btc-staking/Cargo.toml +++ b/contracts/btc-staking/Cargo.toml @@ -22,7 +22,7 @@ crate-type = ["cdylib", "rlib"] doctest = false [[bin]] -name = "btcstaking-schema" +name = "btc-staking-schema" path = "src/bin/schema.rs" test = false diff --git a/contracts/btc-staking/schema/btc-staking.json b/contracts/btc-staking/schema/btc-staking.json index 724e2ccf..b8835592 100644 --- a/contracts/btc-staking/schema/btc-staking.json +++ b/contracts/btc-staking/schema/btc-staking.json @@ -26,25 +26,54 @@ }, "additionalProperties": false, "definitions": { + "Network": { + "type": "string", + "enum": [ + "mainnet", + "testnet", + "signet", + "regtest" + ] + }, "Params": { "description": "Params define Consumer-selectable BTC staking parameters", "type": "object", "required": [ - "max_active_finality_providers", - "min_pub_rand" + "btc_network", + "covenant_pks", + "covenant_quorum", + "min_slashing_tx_fee_sat", + "slashing_address", + "slashing_rate" ], "properties": { - "max_active_finality_providers": { - "description": "`max_active_finality_providers` is the maximum number of active finality providers in the BTC staking protocol", + "btc_network": { + "$ref": "#/definitions/Network" + }, + "covenant_pks": { + "type": "array", + "items": { + "type": "string" + } + }, + "covenant_quorum": { "type": "integer", "format": "uint32", "minimum": 0.0 }, - "min_pub_rand": { - "description": "`min_pub_rand` is the minimum amount of public randomness each public randomness commitment should commit", + "min_slashing_tx_fee_sat": { + "description": "`min_slashing_tx_fee_sat` is the minimum amount of tx fee (quantified in Satoshi) needed for the pre-signed slashing tx", "type": "integer", "format": "uint64", "minimum": 0.0 + }, + "slashing_address": { + "description": "`slashing_address` is the address that the slashed BTC goes to. The address is in string format on Bitcoin.", + "type": "string" + }, + "slashing_rate": { + "description": "`slashing_rate` determines the portion of the staked amount to be slashed, expressed as a decimal (e.g. 0.5 for 50%).", + "type": "string" } }, "additionalProperties": false @@ -125,97 +154,20 @@ "additionalProperties": false }, { - "description": "Committing a sequence of public randomness for EOTS", - "type": "object", - "required": [ - "commit_public_randomness" - ], - "properties": { - "commit_public_randomness": { - "type": "object", - "required": [ - "commitment", - "fp_pubkey_hex", - "num_pub_rand", - "signature", - "start_height" - ], - "properties": { - "commitment": { - "description": "`commitment` is the commitment of these public randomness values. Currently, it's the root of the Merkle tree that includes the public randomness", - "allOf": [ - { - "$ref": "#/definitions/Binary" - } - ] - }, - "fp_pubkey_hex": { - "description": "`fp_pubkey_hex` is the BTC PK of the finality provider that commits the public randomness", - "type": "string" - }, - "num_pub_rand": { - "description": "`num_pub_rand` is the amount of public randomness committed", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "signature": { - "description": "`signature` is the signature on (start_height || num_pub_rand || commitment) signed by the SK corresponding to `fp_pubkey_hex`. This prevents others committing public randomness on behalf of `fp_pubkey_hex`", - "allOf": [ - { - "$ref": "#/definitions/Binary" - } - ] - }, - "start_height": { - "description": "`start_height` is the start block height of the list of public randomness", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Submit Finality Signature.\n\nThis is a message that can be called by a finality provider to submit their finality signature to the Consumer chain. The signature is verified by the Consumer chain using the finality provider's public key\n\nThis message is equivalent to the `MsgAddFinalitySig` message in the Babylon finality protobuf defs.", + "description": "Slash finality provider staking power. Used by the babylon-contract only. The Babylon contract will call this message to set the finality provider's staking power to zero when the finality provider is found to be malicious by the finality contract.", "type": "object", "required": [ - "submit_finality_signature" + "slash" ], "properties": { - "submit_finality_signature": { + "slash": { "type": "object", "required": [ - "block_hash", - "fp_pubkey_hex", - "height", - "proof", - "pub_rand", - "signature" + "fp_btc_pk_hex" ], "properties": { - "block_hash": { - "$ref": "#/definitions/Binary" - }, - "fp_pubkey_hex": { + "fp_btc_pk_hex": { "type": "string" - }, - "height": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "proof": { - "$ref": "#/definitions/Proof" - }, - "pub_rand": { - "$ref": "#/definitions/Binary" - }, - "signature": { - "$ref": "#/definitions/Binary" } }, "additionalProperties": false @@ -520,38 +472,6 @@ }, "additionalProperties": false }, - "Proof": { - "description": "A `Proof` is a proof of a leaf's existence in a Merkle tree.\n\nThe convention for proofs is to include leaf hashes, but to exclude the root hash. This convention is implemented across IAVL range proofs as well. Keep this consistent unless there's a very good reason to change everything. This affects the generalized proof system as well.\n\nEquivalent to / adapted from cometbft/crypto/merkle/proof.go.", - "type": "object", - "required": [ - "aunts", - "index", - "leaf_hash", - "total" - ], - "properties": { - "aunts": { - "type": "array", - "items": { - "$ref": "#/definitions/Binary" - } - }, - "index": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "leaf_hash": { - "$ref": "#/definitions/Binary" - }, - "total": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - }, "ProofOfPossessionBtc": { "description": "ProofOfPossessionBtc is the proof of possession that a Babylon secp256k1 secret key and a Bitcoin secp256k1 secret key are held by the same person", "type": "object", @@ -879,122 +799,6 @@ }, "additionalProperties": false }, - { - "description": "`FinalitySignature` returns the signature of the finality provider for a given block height", - "type": "object", - "required": [ - "finality_signature" - ], - "properties": { - "finality_signature": { - "type": "object", - "required": [ - "btc_pk_hex", - "height" - ], - "properties": { - "btc_pk_hex": { - "type": "string" - }, - "height": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "`PubRandCommit` returns the public random commitments for a given FP.\n\n`btc_pk_hex` is the BTC public key of the finality provider, in hex format.\n\n`start_after` is the height of to start after (before, if `reverse` is `true`), or `None` to start from the beginning (end, if `reverse` is `true`). `limit` is the maximum number of commitments to return. `reverse` is an optional flag to return the commitments in reverse order", - "type": "object", - "required": [ - "pub_rand_commit" - ], - "properties": { - "pub_rand_commit": { - "type": "object", - "required": [ - "btc_pk_hex" - ], - "properties": { - "btc_pk_hex": { - "type": "string" - }, - "limit": { - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "reverse": { - "type": [ - "boolean", - "null" - ] - }, - "start_after": { - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "`FirstPubRandCommit` returns the first public random commitment (if any) for a given FP.\n\nIt's a convenience shortcut of `PubRandCommit` with a `limit` of 1, and `reverse` set to false.\n\n`btc_pk_hex` is the BTC public key of the finality provider, in hex format.", - "type": "object", - "required": [ - "first_pub_rand_commit" - ], - "properties": { - "first_pub_rand_commit": { - "type": "object", - "required": [ - "btc_pk_hex" - ], - "properties": { - "btc_pk_hex": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "`LastPubRandCommit` returns the last public random commitment (if any) for a given FP.\n\nIt's a convenience shortcut of `PubRandCommit` with a `limit` of 1, and `reverse` set to true.\n\n`btc_pk_hex` is the BTC public key of the finality provider, in hex format.", - "type": "object", - "required": [ - "last_pub_rand_commit" - ], - "properties": { - "last_pub_rand_commit": { - "type": "object", - "required": [ - "btc_pk_hex" - ], - "properties": { - "btc_pk_hex": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, { "description": "`ActivatedHeight` returns the height at which the contract gets its first delegation, if any", "type": "object", @@ -1008,102 +812,6 @@ } }, "additionalProperties": false - }, - { - "description": "`Block` returns the indexed block information at height", - "type": "object", - "required": [ - "block" - ], - "properties": { - "block": { - "type": "object", - "required": [ - "height" - ], - "properties": { - "height": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "`Blocks` return the list of indexed blocks.\n\n`start_after` is the height of the block to start after (before, if `reverse` is `true`), or `None` to start from the beginning (end, if `reverse` is `true`). `limit` is the maximum number of blocks to return. `finalised` is an optional filter to return only finalised blocks. `reverse` is an optional flag to return the blocks in reverse order", - "type": "object", - "required": [ - "blocks" - ], - "properties": { - "blocks": { - "type": "object", - "properties": { - "finalised": { - "type": [ - "boolean", - "null" - ] - }, - "limit": { - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "reverse": { - "type": [ - "boolean", - "null" - ] - }, - "start_after": { - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "`Evidence` returns the evidence for a given FP and block height", - "type": "object", - "required": [ - "evidence" - ], - "properties": { - "evidence": { - "type": "object", - "required": [ - "btc_pk_hex", - "height" - ], - "properties": { - "btc_pk_hex": { - "type": "string" - }, - "height": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false } ], "definitions": { @@ -1168,104 +876,17 @@ }, "additionalProperties": false }, - "block": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "IndexedBlock", - "description": "`IndexedBlock` is the necessary metadata and finalization status of a block", - "type": "object", - "required": [ - "app_hash", - "finalized", - "height" - ], - "properties": { - "app_hash": { - "description": "`app_hash` is the AppHash of the block", - "type": "array", - "items": { - "type": "integer", - "format": "uint8", - "minimum": 0.0 - } - }, - "finalized": { - "description": "`finalized` indicates whether the IndexedBlock is finalised by 2/3 of the finality providers or not", - "type": "boolean" - }, - "height": { - "description": "`height` is the height of the block", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - }, - "blocks": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "BlocksResponse", - "type": "object", - "required": [ - "blocks" - ], - "properties": { - "blocks": { - "type": "array", - "items": { - "$ref": "#/definitions/IndexedBlock" - } - } - }, - "additionalProperties": false, - "definitions": { - "IndexedBlock": { - "description": "`IndexedBlock` is the necessary metadata and finalization status of a block", - "type": "object", - "required": [ - "app_hash", - "finalized", - "height" - ], - "properties": { - "app_hash": { - "description": "`app_hash` is the AppHash of the block", - "type": "array", - "items": { - "type": "integer", - "format": "uint8", - "minimum": 0.0 - } - }, - "finalized": { - "description": "`finalized` indicates whether the IndexedBlock is finalised by 2/3 of the finality providers or not", - "type": "boolean" - }, - "height": { - "description": "`height` is the height of the block", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - } - }, "config": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Config", "description": "Config are Babylon-selectable BTC staking configuration", "type": "object", "required": [ - "babylon", - "denom" + "babylon" ], "properties": { "babylon": { "$ref": "#/definitions/Addr" - }, - "denom": { - "type": "string" } }, "additionalProperties": false, @@ -1778,102 +1399,6 @@ }, "additionalProperties": false }, - "evidence": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "EvidenceResponse", - "type": "object", - "properties": { - "evidence": { - "anyOf": [ - { - "$ref": "#/definitions/Evidence" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Evidence": { - "description": "Evidence is the evidence that a finality provider has signed finality signatures with correct public randomness on two conflicting Babylon headers", - "type": "object", - "required": [ - "block_height", - "canonical_app_hash", - "canonical_finality_sig", - "fork_app_hash", - "fork_finality_sig", - "fp_btc_pk", - "pub_rand" - ], - "properties": { - "block_height": { - "description": "`block_height` is the height of the conflicting blocks", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "canonical_app_hash": { - "description": "`canonical_app_hash` is the AppHash of the canonical block", - "type": "array", - "items": { - "type": "integer", - "format": "uint8", - "minimum": 0.0 - } - }, - "canonical_finality_sig": { - "description": "`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`", - "type": "array", - "items": { - "type": "integer", - "format": "uint8", - "minimum": 0.0 - } - }, - "fork_app_hash": { - "description": "`fork_app_hash` is the AppHash of the fork block", - "type": "array", - "items": { - "type": "integer", - "format": "uint8", - "minimum": 0.0 - } - }, - "fork_finality_sig": { - "description": "`fork_finality_sig` is the finality signature to the fork block, where finality signature is an EOTS signature. Deserializes to `SchnorrEOTSSig`", - "type": "array", - "items": { - "type": "integer", - "format": "uint8", - "minimum": 0.0 - } - }, - "fp_btc_pk": { - "description": "`fp_btc_pk` is the BTC PK of the finality provider that casts this vote", - "type": "array", - "items": { - "type": "integer", - "format": "uint8", - "minimum": 0.0 - } - }, - "pub_rand": { - "description": "`pub_rand is` the public randomness the finality provider has committed to. Deserializes to `SchnorrPubRand`", - "type": "array", - "items": { - "type": "integer", - "format": "uint8", - "minimum": 0.0 - } - } - }, - "additionalProperties": false - } - } - }, "finality_provider": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "FinalityProvider", @@ -2224,178 +1749,61 @@ } } }, - "finality_signature": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "FinalitySignatureResponse", - "type": "object", - "required": [ - "signature" - ], - "properties": { - "signature": { - "type": "array", - "items": { - "type": "integer", - "format": "uint8", - "minimum": 0.0 - } - } - }, - "additionalProperties": false - }, - "first_pub_rand_commit": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Nullable_PubRandCommit", - "anyOf": [ - { - "$ref": "#/definitions/PubRandCommit" - }, - { - "type": "null" - } - ], - "definitions": { - "PubRandCommit": { - "description": "`PubRandCommit` is a commitment to a series of public randomness. Currently, the commitment is a root of a Merkle tree that includes a series of public randomness values", - "type": "object", - "required": [ - "commitment", - "num_pub_rand", - "start_height" - ], - "properties": { - "commitment": { - "description": "`commitment` is the value of the commitment. Currently, it's the root of the Merkle tree constructed by the public randomness", - "type": "array", - "items": { - "type": "integer", - "format": "uint8", - "minimum": 0.0 - } - }, - "num_pub_rand": { - "description": "`num_pub_rand` is the number of committed public randomness", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "start_height": { - "description": "`start_height` is the height of the first commitment", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - } - }, - "last_pub_rand_commit": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Nullable_PubRandCommit", - "anyOf": [ - { - "$ref": "#/definitions/PubRandCommit" - }, - { - "type": "null" - } - ], - "definitions": { - "PubRandCommit": { - "description": "`PubRandCommit` is a commitment to a series of public randomness. Currently, the commitment is a root of a Merkle tree that includes a series of public randomness values", - "type": "object", - "required": [ - "commitment", - "num_pub_rand", - "start_height" - ], - "properties": { - "commitment": { - "description": "`commitment` is the value of the commitment. Currently, it's the root of the Merkle tree constructed by the public randomness", - "type": "array", - "items": { - "type": "integer", - "format": "uint8", - "minimum": 0.0 - } - }, - "num_pub_rand": { - "description": "`num_pub_rand` is the number of committed public randomness", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "start_height": { - "description": "`start_height` is the height of the first commitment", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - } - }, "params": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Params", "description": "Params define Consumer-selectable BTC staking parameters", "type": "object", "required": [ - "max_active_finality_providers", - "min_pub_rand" + "btc_network", + "covenant_pks", + "covenant_quorum", + "min_slashing_tx_fee_sat", + "slashing_address", + "slashing_rate" ], "properties": { - "max_active_finality_providers": { - "description": "`max_active_finality_providers` is the maximum number of active finality providers in the BTC staking protocol", - "type": "integer", - "format": "uint32", - "minimum": 0.0 + "btc_network": { + "$ref": "#/definitions/Network" }, - "min_pub_rand": { - "description": "`min_pub_rand` is the minimum amount of public randomness each public randomness commitment should commit", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - }, - "pub_rand_commit": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PubRandCommit", - "description": "`PubRandCommit` is a commitment to a series of public randomness. Currently, the commitment is a root of a Merkle tree that includes a series of public randomness values", - "type": "object", - "required": [ - "commitment", - "num_pub_rand", - "start_height" - ], - "properties": { - "commitment": { - "description": "`commitment` is the value of the commitment. Currently, it's the root of the Merkle tree constructed by the public randomness", + "covenant_pks": { "type": "array", "items": { - "type": "integer", - "format": "uint8", - "minimum": 0.0 + "type": "string" } }, - "num_pub_rand": { - "description": "`num_pub_rand` is the number of committed public randomness", + "covenant_quorum": { "type": "integer", - "format": "uint64", + "format": "uint32", "minimum": 0.0 }, - "start_height": { - "description": "`start_height` is the height of the first commitment", + "min_slashing_tx_fee_sat": { + "description": "`min_slashing_tx_fee_sat` is the minimum amount of tx fee (quantified in Satoshi) needed for the pre-signed slashing tx", "type": "integer", "format": "uint64", "minimum": 0.0 + }, + "slashing_address": { + "description": "`slashing_address` is the address that the slashed BTC goes to. The address is in string format on Bitcoin.", + "type": "string" + }, + "slashing_rate": { + "description": "`slashing_rate` determines the portion of the staked amount to be slashed, expressed as a decimal (e.g. 0.5 for 50%).", + "type": "string" } }, - "additionalProperties": false + "additionalProperties": false, + "definitions": { + "Network": { + "type": "string", + "enum": [ + "mainnet", + "testnet", + "signet", + "regtest" + ] + } + } } } } diff --git a/contracts/btc-staking/schema/raw/execute.json b/contracts/btc-staking/schema/raw/execute.json index 67164ca2..11b583e7 100644 --- a/contracts/btc-staking/schema/raw/execute.json +++ b/contracts/btc-staking/schema/raw/execute.json @@ -72,97 +72,20 @@ "additionalProperties": false }, { - "description": "Committing a sequence of public randomness for EOTS", + "description": "Slash finality provider staking power. Used by the babylon-contract only. The Babylon contract will call this message to set the finality provider's staking power to zero when the finality provider is found to be malicious by the finality contract.", "type": "object", "required": [ - "commit_public_randomness" + "slash" ], "properties": { - "commit_public_randomness": { + "slash": { "type": "object", "required": [ - "commitment", - "fp_pubkey_hex", - "num_pub_rand", - "signature", - "start_height" + "fp_btc_pk_hex" ], "properties": { - "commitment": { - "description": "`commitment` is the commitment of these public randomness values. Currently, it's the root of the Merkle tree that includes the public randomness", - "allOf": [ - { - "$ref": "#/definitions/Binary" - } - ] - }, - "fp_pubkey_hex": { - "description": "`fp_pubkey_hex` is the BTC PK of the finality provider that commits the public randomness", + "fp_btc_pk_hex": { "type": "string" - }, - "num_pub_rand": { - "description": "`num_pub_rand` is the amount of public randomness committed", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "signature": { - "description": "`signature` is the signature on (start_height || num_pub_rand || commitment) signed by the SK corresponding to `fp_pubkey_hex`. This prevents others committing public randomness on behalf of `fp_pubkey_hex`", - "allOf": [ - { - "$ref": "#/definitions/Binary" - } - ] - }, - "start_height": { - "description": "`start_height` is the start block height of the list of public randomness", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Submit Finality Signature.\n\nThis is a message that can be called by a finality provider to submit their finality signature to the Consumer chain. The signature is verified by the Consumer chain using the finality provider's public key\n\nThis message is equivalent to the `MsgAddFinalitySig` message in the Babylon finality protobuf defs.", - "type": "object", - "required": [ - "submit_finality_signature" - ], - "properties": { - "submit_finality_signature": { - "type": "object", - "required": [ - "block_hash", - "fp_pubkey_hex", - "height", - "proof", - "pub_rand", - "signature" - ], - "properties": { - "block_hash": { - "$ref": "#/definitions/Binary" - }, - "fp_pubkey_hex": { - "type": "string" - }, - "height": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "proof": { - "$ref": "#/definitions/Proof" - }, - "pub_rand": { - "$ref": "#/definitions/Binary" - }, - "signature": { - "$ref": "#/definitions/Binary" } }, "additionalProperties": false @@ -467,38 +390,6 @@ }, "additionalProperties": false }, - "Proof": { - "description": "A `Proof` is a proof of a leaf's existence in a Merkle tree.\n\nThe convention for proofs is to include leaf hashes, but to exclude the root hash. This convention is implemented across IAVL range proofs as well. Keep this consistent unless there's a very good reason to change everything. This affects the generalized proof system as well.\n\nEquivalent to / adapted from cometbft/crypto/merkle/proof.go.", - "type": "object", - "required": [ - "aunts", - "index", - "leaf_hash", - "total" - ], - "properties": { - "aunts": { - "type": "array", - "items": { - "$ref": "#/definitions/Binary" - } - }, - "index": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "leaf_hash": { - "$ref": "#/definitions/Binary" - }, - "total": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - }, "ProofOfPossessionBtc": { "description": "ProofOfPossessionBtc is the proof of possession that a Babylon secp256k1 secret key and a Bitcoin secp256k1 secret key are held by the same person", "type": "object", diff --git a/contracts/btc-staking/schema/raw/instantiate.json b/contracts/btc-staking/schema/raw/instantiate.json index b2f3a6c3..a771d00c 100644 --- a/contracts/btc-staking/schema/raw/instantiate.json +++ b/contracts/btc-staking/schema/raw/instantiate.json @@ -22,25 +22,54 @@ }, "additionalProperties": false, "definitions": { + "Network": { + "type": "string", + "enum": [ + "mainnet", + "testnet", + "signet", + "regtest" + ] + }, "Params": { "description": "Params define Consumer-selectable BTC staking parameters", "type": "object", "required": [ - "max_active_finality_providers", - "min_pub_rand" + "btc_network", + "covenant_pks", + "covenant_quorum", + "min_slashing_tx_fee_sat", + "slashing_address", + "slashing_rate" ], "properties": { - "max_active_finality_providers": { - "description": "`max_active_finality_providers` is the maximum number of active finality providers in the BTC staking protocol", + "btc_network": { + "$ref": "#/definitions/Network" + }, + "covenant_pks": { + "type": "array", + "items": { + "type": "string" + } + }, + "covenant_quorum": { "type": "integer", "format": "uint32", "minimum": 0.0 }, - "min_pub_rand": { - "description": "`min_pub_rand` is the minimum amount of public randomness each public randomness commitment should commit", + "min_slashing_tx_fee_sat": { + "description": "`min_slashing_tx_fee_sat` is the minimum amount of tx fee (quantified in Satoshi) needed for the pre-signed slashing tx", "type": "integer", "format": "uint64", "minimum": 0.0 + }, + "slashing_address": { + "description": "`slashing_address` is the address that the slashed BTC goes to. The address is in string format on Bitcoin.", + "type": "string" + }, + "slashing_rate": { + "description": "`slashing_rate` determines the portion of the staked amount to be slashed, expressed as a decimal (e.g. 0.5 for 50%).", + "type": "string" } }, "additionalProperties": false diff --git a/contracts/btc-staking/schema/raw/query.json b/contracts/btc-staking/schema/raw/query.json index c6d174f1..891e0773 100644 --- a/contracts/btc-staking/schema/raw/query.json +++ b/contracts/btc-staking/schema/raw/query.json @@ -240,122 +240,6 @@ }, "additionalProperties": false }, - { - "description": "`FinalitySignature` returns the signature of the finality provider for a given block height", - "type": "object", - "required": [ - "finality_signature" - ], - "properties": { - "finality_signature": { - "type": "object", - "required": [ - "btc_pk_hex", - "height" - ], - "properties": { - "btc_pk_hex": { - "type": "string" - }, - "height": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "`PubRandCommit` returns the public random commitments for a given FP.\n\n`btc_pk_hex` is the BTC public key of the finality provider, in hex format.\n\n`start_after` is the height of to start after (before, if `reverse` is `true`), or `None` to start from the beginning (end, if `reverse` is `true`). `limit` is the maximum number of commitments to return. `reverse` is an optional flag to return the commitments in reverse order", - "type": "object", - "required": [ - "pub_rand_commit" - ], - "properties": { - "pub_rand_commit": { - "type": "object", - "required": [ - "btc_pk_hex" - ], - "properties": { - "btc_pk_hex": { - "type": "string" - }, - "limit": { - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "reverse": { - "type": [ - "boolean", - "null" - ] - }, - "start_after": { - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "`FirstPubRandCommit` returns the first public random commitment (if any) for a given FP.\n\nIt's a convenience shortcut of `PubRandCommit` with a `limit` of 1, and `reverse` set to false.\n\n`btc_pk_hex` is the BTC public key of the finality provider, in hex format.", - "type": "object", - "required": [ - "first_pub_rand_commit" - ], - "properties": { - "first_pub_rand_commit": { - "type": "object", - "required": [ - "btc_pk_hex" - ], - "properties": { - "btc_pk_hex": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "`LastPubRandCommit` returns the last public random commitment (if any) for a given FP.\n\nIt's a convenience shortcut of `PubRandCommit` with a `limit` of 1, and `reverse` set to true.\n\n`btc_pk_hex` is the BTC public key of the finality provider, in hex format.", - "type": "object", - "required": [ - "last_pub_rand_commit" - ], - "properties": { - "last_pub_rand_commit": { - "type": "object", - "required": [ - "btc_pk_hex" - ], - "properties": { - "btc_pk_hex": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, { "description": "`ActivatedHeight` returns the height at which the contract gets its first delegation, if any", "type": "object", @@ -369,102 +253,6 @@ } }, "additionalProperties": false - }, - { - "description": "`Block` returns the indexed block information at height", - "type": "object", - "required": [ - "block" - ], - "properties": { - "block": { - "type": "object", - "required": [ - "height" - ], - "properties": { - "height": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "`Blocks` return the list of indexed blocks.\n\n`start_after` is the height of the block to start after (before, if `reverse` is `true`), or `None` to start from the beginning (end, if `reverse` is `true`). `limit` is the maximum number of blocks to return. `finalised` is an optional filter to return only finalised blocks. `reverse` is an optional flag to return the blocks in reverse order", - "type": "object", - "required": [ - "blocks" - ], - "properties": { - "blocks": { - "type": "object", - "properties": { - "finalised": { - "type": [ - "boolean", - "null" - ] - }, - "limit": { - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "reverse": { - "type": [ - "boolean", - "null" - ] - }, - "start_after": { - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "`Evidence` returns the evidence for a given FP and block height", - "type": "object", - "required": [ - "evidence" - ], - "properties": { - "evidence": { - "type": "object", - "required": [ - "btc_pk_hex", - "height" - ], - "properties": { - "btc_pk_hex": { - "type": "string" - }, - "height": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false } ], "definitions": { diff --git a/contracts/btc-staking/schema/raw/response_to_config.json b/contracts/btc-staking/schema/raw/response_to_config.json index 96e6cbe3..6b405689 100644 --- a/contracts/btc-staking/schema/raw/response_to_config.json +++ b/contracts/btc-staking/schema/raw/response_to_config.json @@ -4,15 +4,11 @@ "description": "Config are Babylon-selectable BTC staking configuration", "type": "object", "required": [ - "babylon", - "denom" + "babylon" ], "properties": { "babylon": { "$ref": "#/definitions/Addr" - }, - "denom": { - "type": "string" } }, "additionalProperties": false, diff --git a/contracts/btc-staking/schema/raw/response_to_params.json b/contracts/btc-staking/schema/raw/response_to_params.json index b29d6558..89d6b26a 100644 --- a/contracts/btc-staking/schema/raw/response_to_params.json +++ b/contracts/btc-staking/schema/raw/response_to_params.json @@ -4,22 +4,53 @@ "description": "Params define Consumer-selectable BTC staking parameters", "type": "object", "required": [ - "max_active_finality_providers", - "min_pub_rand" + "btc_network", + "covenant_pks", + "covenant_quorum", + "min_slashing_tx_fee_sat", + "slashing_address", + "slashing_rate" ], "properties": { - "max_active_finality_providers": { - "description": "`max_active_finality_providers` is the maximum number of active finality providers in the BTC staking protocol", + "btc_network": { + "$ref": "#/definitions/Network" + }, + "covenant_pks": { + "type": "array", + "items": { + "type": "string" + } + }, + "covenant_quorum": { "type": "integer", "format": "uint32", "minimum": 0.0 }, - "min_pub_rand": { - "description": "`min_pub_rand` is the minimum amount of public randomness each public randomness commitment should commit", + "min_slashing_tx_fee_sat": { + "description": "`min_slashing_tx_fee_sat` is the minimum amount of tx fee (quantified in Satoshi) needed for the pre-signed slashing tx", "type": "integer", "format": "uint64", "minimum": 0.0 + }, + "slashing_address": { + "description": "`slashing_address` is the address that the slashed BTC goes to. The address is in string format on Bitcoin.", + "type": "string" + }, + "slashing_rate": { + "description": "`slashing_rate` determines the portion of the staked amount to be slashed, expressed as a decimal (e.g. 0.5 for 50%).", + "type": "string" } }, - "additionalProperties": false + "additionalProperties": false, + "definitions": { + "Network": { + "type": "string", + "enum": [ + "mainnet", + "testnet", + "signet", + "regtest" + ] + } + } } diff --git a/contracts/btc-staking/src/contract.rs b/contracts/btc-staking/src/contract.rs index 5e31e1e3..1fca33b3 100644 --- a/contracts/btc-staking/src/contract.rs +++ b/contracts/btc-staking/src/contract.rs @@ -7,16 +7,13 @@ use cosmwasm_std::{ use cw2::set_contract_version; use cw_utils::{maybe_addr, nonpayable}; -use babylon_apis::btc_staking_api::SudoMsg; use babylon_bindings::BabylonMsg; use crate::error::ContractError; -use crate::finality::{handle_finality_signature, handle_public_randomness_commit}; use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; -use crate::staking::{compute_active_finality_providers, handle_btc_staking}; +use crate::queries; +use crate::staking::{handle_btc_staking, handle_slash_fp}; use crate::state::config::{Config, ADMIN, CONFIG, PARAMS}; -use crate::state::staking::ACTIVATED_HEIGHT; -use crate::{finality, queries, state}; pub const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -29,9 +26,7 @@ pub fn instantiate( msg: InstantiateMsg, ) -> Result, ContractError> { nonpayable(&info)?; - let denom = deps.querier.query_bonded_denom()?; let config = Config { - denom, babylon: info.sender, }; CONFIG.save(deps.storage, &config)?; @@ -89,46 +84,7 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result Ok(to_json_binary( &queries::finality_providers_by_power(deps, start_after, limit)?, )?), - QueryMsg::FinalitySignature { btc_pk_hex, height } => Ok(to_json_binary( - &queries::finality_signature(deps, btc_pk_hex, height)?, - )?), - QueryMsg::PubRandCommit { - btc_pk_hex, - start_after, - limit, - reverse, - } => Ok(to_json_binary( - &state::public_randomness::get_pub_rand_commit( - deps.storage, - &btc_pk_hex, - start_after, - limit, - reverse, - )?, - )?), - QueryMsg::FirstPubRandCommit { btc_pk_hex } => Ok(to_json_binary( - &state::public_randomness::get_first_pub_rand_commit(deps.storage, &btc_pk_hex)?, - )?), - QueryMsg::LastPubRandCommit { btc_pk_hex } => Ok(to_json_binary( - &state::public_randomness::get_last_pub_rand_commit(deps.storage, &btc_pk_hex)?, - )?), QueryMsg::ActivatedHeight {} => Ok(to_json_binary(&queries::activated_height(deps)?)?), - QueryMsg::Block { height } => Ok(to_json_binary(&queries::block(deps, height)?)?), - QueryMsg::Blocks { - start_after, - limit, - finalised, - reverse, - } => Ok(to_json_binary(&queries::blocks( - deps, - start_after, - limit, - finalised, - reverse, - )?)?), - QueryMsg::Evidence { btc_pk_hex, height } => Ok(to_json_binary(&queries::evidence( - deps, btc_pk_hex, height, - )?)?), } } @@ -164,85 +120,12 @@ pub fn execute( &slashed_del, &unbonded_del, ), - ExecuteMsg::SubmitFinalitySignature { - fp_pubkey_hex, - height, - pub_rand, - proof, - block_hash, - signature, - } => handle_finality_signature( - deps, - env, - &fp_pubkey_hex, - height, - &pub_rand, - &proof, - &block_hash, - &signature, - ), - ExecuteMsg::CommitPublicRandomness { - fp_pubkey_hex, - start_height, - num_pub_rand, - commitment, - signature, - } => handle_public_randomness_commit( - deps, - &fp_pubkey_hex, - start_height, - num_pub_rand, - &commitment, - &signature, - ), - } -} - -#[cfg_attr(not(feature = "library"), entry_point)] -pub fn sudo( - mut deps: DepsMut, - env: Env, - msg: SudoMsg, -) -> Result, ContractError> { - match msg { - SudoMsg::BeginBlock { .. } => handle_begin_block(&mut deps, env), - SudoMsg::EndBlock { - hash_hex, - app_hash_hex, - } => handle_end_block(&mut deps, env, &hash_hex, &app_hash_hex), + ExecuteMsg::Slash { fp_btc_pk_hex } => handle_slash_fp(deps, env, &info, &fp_btc_pk_hex), } } -fn handle_begin_block(deps: &mut DepsMut, env: Env) -> Result, ContractError> { - // Compute active finality provider set - let max_active_fps = PARAMS.load(deps.storage)?.max_active_finality_providers as usize; - compute_active_finality_providers(deps.storage, env, max_active_fps)?; - - Ok(Response::new()) -} - -fn handle_end_block( - deps: &mut DepsMut, - env: Env, - _hash_hex: &str, - app_hash_hex: &str, -) -> Result, ContractError> { - // If the BTC staking protocol is activated i.e. there exists a height where at least one - // finality provider has voting power, start indexing and tallying blocks - let mut res = Response::new(); - if let Some(activated_height) = ACTIVATED_HEIGHT.may_load(deps.storage)? { - // Index the current 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 events = finality::tally_blocks(deps, activated_height, env.block.height)?; - res = res.add_events(events); - } - Ok(res) -} - #[cfg(test)] -pub(crate) mod tests { +pub mod tests { use std::str::FromStr; use super::*; @@ -252,7 +135,6 @@ pub(crate) mod tests { ActiveBtcDelegation, BtcUndelegationInfo, CovenantAdaptorSignatures, FinalityProviderDescription, NewFinalityProvider, ProofOfPossessionBtc, }; - use babylon_apis::finality_api::PubRandCommit; use babylon_bitcoin::chain_params::Network; use babylon_proto::babylon::btcstaking::v1::{ BtcDelegation, FinalityProvider, Params as ProtoParams, @@ -266,8 +148,7 @@ pub(crate) mod tests { use hex::ToHex; use k256::schnorr::{Signature, SigningKey}; use test_utils::{ - get_btc_del_unbonding_sig_bytes, get_btc_delegation, get_finality_provider, - get_fp_sk_bytes, get_pub_rand_commit, + get_btc_del_unbonding_sig_bytes, get_btc_delegation, get_finality_provider, get_fp_sk_bytes, }; pub(crate) const CREATOR: &str = "creator"; @@ -279,8 +160,6 @@ pub(crate) mod tests { covenant_pks: params.covenant_pks.iter().map(hex::encode).collect(), covenant_quorum: params.covenant_quorum, btc_network: Network::Regtest, // TODO: fix this - max_active_finality_providers: params.max_active_finality_providers, - min_pub_rand: 10, // TODO: fix this slashing_address: params.slashing_address, min_slashing_tx_fee_sat: params.min_slashing_tx_fee_sat as u64, slashing_rate: "0.01".to_string(), // TODO: fix this @@ -369,7 +248,7 @@ pub(crate) mod tests { } // Build a derived active BTC delegation from the base (from testdata) BTC delegation - pub(crate) fn get_derived_btc_delegation(del_id: i32, fp_ids: &[i32]) -> ActiveBtcDelegation { + pub fn get_derived_btc_delegation(del_id: i32, fp_ids: &[i32]) -> ActiveBtcDelegation { let del = get_btc_delegation(del_id, fp_ids.to_vec()); new_active_btc_delegation(del) } @@ -379,22 +258,6 @@ pub(crate) mod tests { Signature::try_from(sig_bytes.as_slice()).unwrap() } - /// Get public randomness public key, commitment, and signature information - /// - /// Signature is a Schnorr signature over the commitment - pub(crate) fn get_public_randomness_commitment() -> (String, PubRandCommit, Vec) { - let pub_rand_commitment_msg = get_pub_rand_commit(); - ( - pub_rand_commitment_msg.fp_btc_pk.encode_hex(), - PubRandCommit { - start_height: pub_rand_commitment_msg.start_height, - num_pub_rand: pub_rand_commitment_msg.num_pub_rand, - commitment: pub_rand_commitment_msg.commitment.to_vec(), - }, - pub_rand_commitment_msg.sig.to_vec(), - ) - } - pub(crate) fn create_new_finality_provider(id: i32) -> NewFinalityProvider { let fp = get_finality_provider(id); new_finality_provider(fp) diff --git a/contracts/btc-staking/src/finality.rs b/contracts/btc-staking/src/finality.rs deleted file mode 100644 index c401118f..00000000 --- a/contracts/btc-staking/src/finality.rs +++ /dev/null @@ -1,1104 +0,0 @@ -use k256::ecdsa::signature::Verifier; -use k256::schnorr::{Signature, VerifyingKey}; -use k256::sha2::{Digest, Sha256}; -use std::cmp::max; -use std::collections::HashSet; - -use cosmwasm_std::Order::Ascending; -use cosmwasm_std::{to_json_binary, DepsMut, Env, Event, Response, StdResult, Storage, WasmMsg}; - -use babylon_apis::finality_api::{Evidence, IndexedBlock, PubRandCommit}; -use babylon_bindings::BabylonMsg; -use babylon_merkle::Proof; - -use crate::error::ContractError; -use crate::msg::FinalityProviderInfo; -use crate::staking; -use crate::state::config::{CONFIG, PARAMS}; -use crate::state::finality::{BLOCKS, EVIDENCES, NEXT_HEIGHT, SIGNATURES}; -use crate::state::public_randomness::{ - get_last_pub_rand_commit, get_pub_rand_commit_for_height, PUB_RAND_COMMITS, PUB_RAND_VALUES, -}; -use crate::state::staking::{fps, FPS, FP_SET}; - -pub fn handle_public_randomness_commit( - deps: DepsMut, - fp_pubkey_hex: &str, - start_height: u64, - num_pub_rand: u64, - commitment: &[u8], - signature: &[u8], -) -> Result, ContractError> { - // Ensure the request contains enough amounts of public randomness - let min_pub_rand = PARAMS.load(deps.storage)?.min_pub_rand; - if num_pub_rand < min_pub_rand { - return Err(ContractError::TooFewPubRand(min_pub_rand, num_pub_rand)); - } - // TODO: ensure log_2(num_pub_rand) is an integer? - - // Ensure the finality provider is registered - if !FPS.has(deps.storage, fp_pubkey_hex) { - return Err(ContractError::FinalityProviderNotFound( - fp_pubkey_hex.to_string(), - )); - } - // Verify signature over the list - verify_commitment_signature( - fp_pubkey_hex, - start_height, - num_pub_rand, - commitment, - signature, - )?; - - // Get last public randomness commitment - // TODO: allow committing public randomness earlier than existing ones? - let last_pr_commit = get_last_pub_rand_commit(deps.storage, fp_pubkey_hex) - .ok() // Turn error into None - .flatten(); - - // Check for overlapping heights if there is a last commit - if let Some(last_pr_commit) = last_pr_commit { - if start_height <= last_pr_commit.end_height() { - return Err(ContractError::InvalidPubRandHeight( - start_height, - last_pr_commit.end_height(), - )); - } - } - - // All good, store the given public randomness commitment - let pr_commit = PubRandCommit { - start_height, - num_pub_rand, - commitment: commitment.to_vec(), - }; - - PUB_RAND_COMMITS.save( - deps.storage, - (fp_pubkey_hex, pr_commit.start_height), - &pr_commit, - )?; - - // TODO: Add events - Ok(Response::new()) -} - -fn verify_commitment_signature( - fp_btc_pk_hex: &str, - start_height: u64, - num_pub_rand: u64, - commitment: &[u8], - signature: &[u8], -) -> Result<(), ContractError> { - // get BTC public key for verification - let btc_pk_raw = hex::decode(fp_btc_pk_hex)?; - let btc_pk = VerifyingKey::from_bytes(&btc_pk_raw) - .map_err(|e| ContractError::SecP256K1Error(e.to_string()))?; - - // get signature - if signature.is_empty() { - return Err(ContractError::EmptySignature); - } - let schnorr_sig = - Signature::try_from(signature).map_err(|e| ContractError::SecP256K1Error(e.to_string()))?; - - // get signed message - let mut msg: Vec = vec![]; - msg.extend_from_slice(&start_height.to_be_bytes()); - msg.extend_from_slice(&num_pub_rand.to_be_bytes()); - msg.extend_from_slice(commitment); - - // Verify the signature - btc_pk - .verify(&msg, &schnorr_sig) - .map_err(|e| ContractError::SecP256K1Error(e.to_string())) -} - -#[allow(clippy::too_many_arguments)] -pub fn handle_finality_signature( - mut deps: DepsMut, - env: Env, - fp_btc_pk_hex: &str, - height: u64, - pub_rand: &[u8], - proof: &Proof, - block_app_hash: &[u8], - signature: &[u8], -) -> Result, ContractError> { - // Ensure the finality provider exists - let fp = FPS.load(deps.storage, fp_btc_pk_hex)?; - - // Ensure the finality provider is not slashed at this time point - // NOTE: It's possible that the finality provider equivocates for height h, and the signature is - // processed at height h' > h. In this case: - // - We should reject any new signature from this finality provider, since it's known to be adversarial. - // - We should set its voting power since height h'+1 to be zero, for the same reason. - // - We should NOT set its voting power between [h, h'] to be zero, since - // - Babylon BTC staking ensures safety upon 2f+1 votes, *even if* f of them are adversarial. - // This is because as long as a block gets 2f+1 votes, any other block with 2f+1 votes has a - // f+1 quorum intersection with this block, contradicting the assumption and leading to - // the safety proof. - // This ensures slashable safety together with EOTS, thus does not undermine Babylon's security guarantee. - // - Due to this reason, when tallying a block, Babylon finalises this block upon 2f+1 votes. If we - // modify voting power table in the history, some finality decisions might be contradicting to the - // signature set and voting power table. - // - To fix the above issue, Babylon has to allow finalised and not-finalised blocks. However, - // this means Babylon will lose safety under an adaptive adversary corrupting even 1 - // finality provider. It can simply corrupt a new finality provider and equivocate a - // historical block over and over again, making a previous block not finalisable forever - if fp.slashed_height > 0 && fp.slashed_height < height { - return Err(ContractError::FinalityProviderAlreadySlashed( - fp_btc_pk_hex.to_string(), - )); - } - - // Ensure the finality provider has voting power at this height - fps() - .may_load_at_height(deps.storage, fp_btc_pk_hex, height)? - .ok_or_else(|| ContractError::NoVotingPower(fp_btc_pk_hex.to_string(), height))?; - - // Ensure the signature is not empty - if signature.is_empty() { - return Err(ContractError::EmptySignature); - } - // Ensure the height is proper - if env.block.height < height { - return Err(ContractError::HeightTooHigh); - } - // Ensure the finality provider has not cast the same vote yet - let existing_sig = SIGNATURES.may_load(deps.storage, (height, fp_btc_pk_hex))?; - match existing_sig { - Some(existing_sig) if existing_sig == signature => { - deps.api.debug(&format!("Received duplicated finality vote. Height: {height}, Finality Provider: {fp_btc_pk_hex}")); - // Exactly the same vote already exists, return success to the provider - return Ok(Response::new()); - } - _ => {} - } - - // Find the public randomness commitment for this height from this finality provider - let pr_commit = get_pub_rand_commit_for_height(deps.storage, fp_btc_pk_hex, height)?; - - // Verify the finality signature message - verify_finality_signature( - fp_btc_pk_hex, - height, - pub_rand, - proof, - &pr_commit, - block_app_hash, - signature, - )?; - - // The public randomness value is good, save it. - // 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())?; - - // Verify whether the voted block is a fork or not - // TODO?: Do not rely on 'canonical' (i.e. BFT-consensus provided) blocks info - let indexed_block = BLOCKS - .load(deps.storage, height) - .map_err(|err| ContractError::BlockNotFound(height, err.to_string()))?; - - let mut res = Response::new(); - if indexed_block.app_hash != block_app_hash { - // The finality provider votes for a fork! - - // Construct evidence - let mut evidence = Evidence { - fp_btc_pk: hex::decode(fp_btc_pk_hex)?, - block_height: height, - pub_rand: pub_rand.to_vec(), - canonical_app_hash: indexed_block.app_hash, - canonical_finality_sig: vec![], - fork_app_hash: block_app_hash.to_vec(), - fork_finality_sig: signature.to_vec(), - }; - - // If this finality provider has also signed the canonical block, slash it - let canonical_sig = SIGNATURES.may_load(deps.storage, (height, fp_btc_pk_hex))?; - if let Some(canonical_sig) = canonical_sig { - // Set canonical sig - evidence.canonical_finality_sig = canonical_sig; - // Slash this finality provider, including setting its voting power to zero, extracting - // its BTC SK, and emitting an event - let (msg, ev) = slash_finality_provider(&mut deps, env, fp_btc_pk_hex, &evidence)?; - res = res.add_message(msg); - res = res.add_event(ev); - } - // TODO?: Also slash if this finality provider has signed another fork before - - // Save evidence - EVIDENCES.save(deps.storage, (fp_btc_pk_hex, height), &evidence)?; - - // NOTE: We should NOT return error here, otherwise the state change triggered in this tx - // (including the evidence) will be rolled back - return Ok(res); - } - - // This signature is good, save the vote to the store - SIGNATURES.save(deps.storage, (height, fp_btc_pk_hex), &signature.to_vec())?; - - // If this finality provider has signed the canonical block before, slash it via extracting its - // secret key, and emit an event - if let Some(mut evidence) = EVIDENCES.may_load(deps.storage, (fp_btc_pk_hex, height))? { - // The finality provider has voted for a fork before! - // This evidence is at the same height as this signature, slash this finality provider - - // Set canonical sig to this evidence - evidence.canonical_finality_sig = signature.to_vec(); - EVIDENCES.save(deps.storage, (fp_btc_pk_hex, height), &evidence)?; - - // Slash this finality provider, including setting its voting power to zero, extracting its - // BTC SK, and emitting an event - let (msg, ev) = slash_finality_provider(&mut deps, env, fp_btc_pk_hex, &evidence)?; - res = res.add_message(msg); - res = res.add_event(ev); - } - - Ok(res) -} - -/// `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( - deps: &mut DepsMut, - env: Env, - fp_btc_pk_hex: &str, - evidence: &Evidence, -) -> Result<(WasmMsg, Event), ContractError> { - // Slash this finality provider, i.e., set its slashing height to the block height - staking::slash_finality_provider(deps, env, fp_btc_pk_hex, evidence.block_height) - .map_err(|err| ContractError::FailedToSlashFinalityProvider(err.to_string()))?; - - // Extract BTC SK using the evidence - 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. - // Send to babylon-contract for forwarding - let msg = babylon_contract::ExecuteMsg::Slashing { - evidence: evidence.clone(), - }; - - let babylon_addr = CONFIG.load(deps.storage)?.babylon; - - let wasm_msg = WasmMsg::Execute { - contract_addr: babylon_addr.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)) -} - -/// Verifies the finality signature message w.r.t. the public randomness commitment: -/// - Public randomness inclusion proof. -/// - Finality signature -fn verify_finality_signature( - fp_btc_pk_hex: &str, - block_height: u64, - pub_rand: &[u8], - proof: &Proof, - pr_commit: &PubRandCommit, - app_hash: &[u8], - signature: &[u8], -) -> Result<(), ContractError> { - let proof_height = pr_commit.start_height + proof.index; - if block_height != proof_height { - return Err(ContractError::InvalidFinalitySigHeight( - proof_height, - block_height, - )); - } - // Verify the total amount of randomness is the same as in the commitment - if proof.total != pr_commit.num_pub_rand { - return Err(ContractError::InvalidFinalitySigAmount( - proof.total, - pr_commit.num_pub_rand, - )); - } - // Verify the proof of inclusion for this public randomness - proof.validate_basic()?; - proof.verify(&pr_commit.commitment, pub_rand)?; - - // Public randomness is good, verify finality signature - let pubkey = eots::PublicKey::from_hex(fp_btc_pk_hex)?; - let msg = msg_to_sign(block_height, app_hash); - let msg_hash = Sha256::digest(msg); - - if !pubkey.verify(pub_rand, &msg_hash, signature)? { - return Err(ContractError::FailedSignatureVerification("EOTS".into())); - } - Ok(()) -} - -/// `msg_to_sign` returns the message for an EOTS signature. -/// -/// The EOTS signature on a block will be (block_height || block_hash) -fn msg_to_sign(height: u64, block_hash: &[u8]) -> Vec { - let mut msg: Vec = height.to_be_bytes().to_vec(); - msg.extend_from_slice(block_hash); - msg -} - -pub fn index_block( - deps: &mut DepsMut, - height: u64, - app_hash: &[u8], -) -> Result { - let indexed_block = IndexedBlock { - height, - app_hash: app_hash.into(), - finalized: false, - }; - BLOCKS.save(deps.storage, height, &indexed_block)?; - - // Register the indexed block height - let ev = Event::new("index_block") - .add_attribute("module", "finality") - .add_attribute("last_height", height.to_string()); - Ok(ev) -} - -/// TallyBlocks tries to finalise all blocks that are non-finalised AND have a non-nil -/// finality provider set, from the earliest to the latest. -/// -/// This function is invoked upon each `EndBlock`, after the BTC staking protocol is activated. -/// It ensures that at height `h`, the ancestor chain `[activated_height, h-1]` contains either -/// - finalised blocks (i.e., blocks with a finality provider set AND QC of this finality provider set), -/// - non-finalisable blocks (i.e. blocks with no active finality providers), -/// but no blocks that have a finality provider set and do not receive a QC -/// -/// It must be invoked only after the BTC staking protocol is activated. -pub fn tally_blocks( - deps: &mut DepsMut, - activated_height: u64, - height: u64, -) -> Result, ContractError> { - // Start finalising blocks since max(activated_height, next_height) - let next_height = NEXT_HEIGHT.may_load(deps.storage)?.unwrap_or(0); - let start_height = max(activated_height, next_height); - - // Find all blocks that are non-finalised AND have a finality provider set since - // max(activated_height, last_finalized_height + 1) - // There are 4 different scenarios: - // - Has finality providers, non-finalised: Tally and try to finalise. - // - Does not have finality providers, non-finalised: Non-finalisable, continue. - // - Has finality providers, finalised: Impossible, panic. - // - Does not have finality providers, finalised: Impossible, panic. - // After this for loop, the blocks since the earliest activated height are either finalised or - // non-finalisable - let mut events = vec![]; - for h in start_height..=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)?; - - match (fp_set, indexed_block.finalized) { - (Some(fp_set), false) => { - // Has finality providers, non-finalised: tally and try to finalise the block - let voter_btc_pks = SIGNATURES - .prefix(indexed_block.height) - .keys(deps.storage, None, None, Ascending) - .collect::>>()?; - if tally(&fp_set, &voter_btc_pks) { - // If this block gets >2/3 votes, finalise it - let ev = finalize_block(deps.storage, &mut indexed_block, &voter_btc_pks)?; - events.push(ev); - } else { - // If not, then this block and all subsequent blocks should not be finalised. - // Thus, we need to break here - break; - } - } - (None, false) => { - // Does not have finality providers, non-finalised: not finalisable, - // Increment the next height to finalise and continue - NEXT_HEIGHT.save(deps.storage, &(indexed_block.height + 1))?; - continue; - } - (Some(_), true) => { - // Has finality providers and the block is finalised. - // This can only be a programming error - return Err(ContractError::FinalisedBlockWithFinalityProviderSet( - indexed_block.height, - )); - } - (None, true) => { - // Does not have finality providers, finalised: impossible to happen - return Err(ContractError::FinalisedBlockWithoutFinalityProviderSet( - indexed_block.height, - )); - } - } - } - Ok(events) -} - -/// `tally` checks whether a block with the given finality provider set and votes reaches a quorum -/// or not -fn tally(fp_set: &[FinalityProviderInfo], voters: &[String]) -> bool { - let voters: HashSet = voters.iter().cloned().collect(); - let mut total_power = 0; - let mut voted_power = 0; - for fp_info in fp_set { - total_power += fp_info.power; - if voters.contains(&fp_info.btc_pk_hex) { - voted_power += fp_info.power; - } - } - voted_power * 3 > total_power * 2 -} - -/// `finalize_block` sets a block to be finalised, and distributes rewards to finality providers -/// and delegators -fn finalize_block( - store: &mut dyn Storage, - block: &mut IndexedBlock, - _voters: &[String], -) -> Result { - // Set block to be finalised - block.finalized = true; - BLOCKS.save(store, block.height, 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") - .add_attribute("finalized_height", block.height.to_string()); - Ok(ev) -} - -#[cfg(test)] -pub(crate) mod tests { - use babylon_apis::btc_staking_api::SudoMsg; - use babylon_apis::finality_api::IndexedBlock; - use babylon_bindings::BabylonMsg; - use cosmwasm_std::testing::{ - message_info, mock_dependencies, mock_env, MockApi, MockQuerier, MockStorage, - }; - use cosmwasm_std::{to_json_binary, Binary, Env, Event, OwnedDeps, Response, SubMsg, WasmMsg}; - use hex::ToHex; - use test_utils::{get_add_finality_sig, get_add_finality_sig_2, get_pub_rand_value}; - - use crate::contract::tests::{ - create_new_finality_provider, get_params, get_public_randomness_commitment, CREATOR, - }; - use crate::contract::{execute, instantiate}; - use crate::msg::{ExecuteMsg, FinalitySignatureResponse, InstantiateMsg}; - use crate::queries; - - pub(crate) fn mock_env_height(height: u64) -> Env { - let mut env = mock_env(); - env.block.height = height; - - env - } - - #[track_caller] - pub(crate) fn call_begin_block( - deps: &mut OwnedDeps, - app_hash: &[u8], - height: u64, - ) -> Result, crate::error::ContractError> { - let env = mock_env_height(height); - // Hash is not used in the begin-block handler - let hash_hex = "deadbeef".to_string(); - let app_hash_hex = app_hash.encode_hex(); - - crate::contract::sudo( - deps.as_mut(), - env.clone(), - SudoMsg::BeginBlock { - hash_hex, - app_hash_hex, - }, - ) - } - - #[track_caller] - pub(crate) fn call_end_block( - deps: &mut OwnedDeps, - app_hash: &[u8], - height: u64, - ) -> Result, crate::error::ContractError> { - let env = mock_env_height(height); - // Hash is not used in the end-block handler - let hash_hex = "deadbeef".to_string(); - let app_hash_hex = app_hash.encode_hex(); - - crate::contract::sudo( - deps.as_mut(), - env.clone(), - SudoMsg::EndBlock { - hash_hex, - app_hash_hex, - }, - ) - } - - #[test] - fn commit_public_randomness_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(); - - // Read public randomness commitment test data - let (pk_hex, pub_rand, pubrand_signature) = get_public_randomness_commitment(); - - // Register one FP - // NOTE: the test data ensures that pub rand commit / finality sig are - // signed by the 1st FP - let new_fp = create_new_finality_provider(1); - assert_eq!(new_fp.btc_pk_hex, pk_hex); - - let msg = ExecuteMsg::BtcStaking { - new_fp: vec![new_fp.clone()], - active_del: vec![], - slashed_del: vec![], - unbonded_del: vec![], - }; - - let res = execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); - assert_eq!(0, res.messages.len()); - - // Now commit the public randomness for it - let msg = ExecuteMsg::CommitPublicRandomness { - fp_pubkey_hex: pk_hex, - start_height: pub_rand.start_height, - num_pub_rand: pub_rand.num_pub_rand, - commitment: pub_rand.commitment.into(), - signature: pubrand_signature.into(), - }; - - let res = execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); - assert_eq!(0, res.messages.len()); - } - - #[test] - fn finality_signature_happy_path() { - let mut deps = mock_dependencies(); - let info = message_info(&deps.api.addr_make(CREATOR), &[]); - - // Read public randomness commitment test data - let (pk_hex, pub_rand, pubrand_signature) = get_public_randomness_commitment(); - let pub_rand_one = get_pub_rand_value(); - // Read equivalent / consistent add finality signature test data - let add_finality_signature = get_add_finality_sig(); - let proof = add_finality_signature.proof.unwrap(); - - let initial_height = pub_rand.start_height; - - let initial_env = mock_env_height(initial_height); - - instantiate( - deps.as_mut(), - initial_env.clone(), - info.clone(), - InstantiateMsg { - params: Some(get_params()), - admin: None, - }, - ) - .unwrap(); - - // Register one FP - // NOTE: the test data ensures that pub rand commit / finality sig are - // signed by the 1st FP - let new_fp = create_new_finality_provider(1); - assert_eq!(new_fp.btc_pk_hex, pk_hex); - - let msg = ExecuteMsg::BtcStaking { - new_fp: vec![new_fp.clone()], - active_del: vec![], - slashed_del: vec![], - unbonded_del: vec![], - }; - - let _res = execute(deps.as_mut(), initial_env.clone(), info.clone(), msg).unwrap(); - - // Activated height is not set - let res = crate::queries::activated_height(deps.as_ref()).unwrap(); - assert_eq!(res.height, 0); - - // Add a delegation, so that the finality provider has some power - let mut del1 = crate::contract::tests::get_derived_btc_delegation(1, &[1]); - del1.fp_btc_pk_list = vec![pk_hex.clone()]; - - let msg = ExecuteMsg::BtcStaking { - new_fp: vec![], - active_del: vec![del1.clone()], - slashed_del: vec![], - unbonded_del: vec![], - }; - - execute(deps.as_mut(), initial_env, info.clone(), msg).unwrap(); - - // Activated height is now set - let activated_height = crate::queries::activated_height(deps.as_ref()).unwrap(); - assert_eq!(activated_height.height, initial_height + 1); - - // Submit public randomness commitment for the FP and the involved heights - let msg = ExecuteMsg::CommitPublicRandomness { - fp_pubkey_hex: pk_hex.clone(), - start_height: pub_rand.start_height, - num_pub_rand: pub_rand.num_pub_rand, - commitment: pub_rand.commitment.into(), - signature: pubrand_signature.into(), - }; - - let res = execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); - assert_eq!(0, res.messages.len()); - - // Call the begin-block sudo handler, for completeness - let res = call_begin_block( - &mut deps, - &add_finality_signature.block_app_hash, - initial_height + 1, - ) - .unwrap(); - assert_eq!(0, res.attributes.len()); - assert_eq!(0, res.messages.len()); - assert_eq!(0, res.events.len()); - - // Call the end-block sudo handler, so that the block is indexed in the store - let res = call_end_block( - &mut deps, - &add_finality_signature.block_app_hash, - initial_height + 1, - ) - .unwrap(); - assert_eq!(0, res.attributes.len()); - assert_eq!(0, res.messages.len()); - assert_eq!(1, res.events.len()); - assert_eq!( - res.events[0], - Event::new("index_block") - .add_attribute("module", "finality") - .add_attribute("last_height", (initial_height + 1).to_string()) - ); - - // Submit a finality signature from that finality provider at height initial_height + 1 - let finality_signature = add_finality_signature.finality_sig.to_vec(); - let msg = ExecuteMsg::SubmitFinalitySignature { - fp_pubkey_hex: pk_hex.clone(), - height: initial_height + 1, - pub_rand: pub_rand_one.into(), - proof: proof.into(), - block_hash: add_finality_signature.block_app_hash.to_vec().into(), - signature: Binary::new(finality_signature.clone()), - }; - - // Execute the message at a higher height, so that: - // 1. It's not rejected because of height being too high. - // 2. The FP has consolidated power at such height - let _res = execute( - deps.as_mut(), - mock_env_height(initial_height + 2), - info.clone(), - msg, - ) - .unwrap(); - - // Query finality signature for that exact height - let sig = crate::queries::finality_signature( - deps.as_ref(), - pk_hex.to_string(), - initial_height + 1, - ) - .unwrap(); - assert_eq!( - sig, - FinalitySignatureResponse { - signature: finality_signature - } - ); - } - - #[test] - fn finality_round_works() { - let mut deps = mock_dependencies(); - let info = message_info(&deps.api.addr_make(CREATOR), &[]); - - // Read public randomness commitment test data - let (pk_hex, pub_rand, pubrand_signature) = get_public_randomness_commitment(); - let pub_rand_one = get_pub_rand_value(); - // Read equivalent / consistent add finality signature test data - let add_finality_signature = get_add_finality_sig(); - let proof = add_finality_signature.proof.unwrap(); - - let initial_height = pub_rand.start_height; - - let initial_env = mock_env_height(initial_height); - - instantiate( - deps.as_mut(), - initial_env.clone(), - info.clone(), - InstantiateMsg { - params: Some(get_params()), - admin: None, - }, - ) - .unwrap(); - - // Register one FP - // NOTE: the test data ensures that pub rand commit / finality sig are - // signed by the 1st FP - let new_fp = create_new_finality_provider(1); - assert_eq!(new_fp.btc_pk_hex, pk_hex); - - let msg = ExecuteMsg::BtcStaking { - new_fp: vec![new_fp.clone()], - active_del: vec![], - slashed_del: vec![], - unbonded_del: vec![], - }; - - execute(deps.as_mut(), initial_env.clone(), info.clone(), msg).unwrap(); - - // Add a delegation, so that the finality provider has some power - let mut del1 = crate::contract::tests::get_derived_btc_delegation(1, &[1]); - del1.fp_btc_pk_list = vec![pk_hex.clone()]; - - let msg = ExecuteMsg::BtcStaking { - new_fp: vec![], - active_del: vec![del1.clone()], - slashed_del: vec![], - unbonded_del: vec![], - }; - execute(deps.as_mut(), initial_env, info.clone(), msg).unwrap(); - - // Check that the finality provider power has been updated - let fp_info = - queries::finality_provider_info(deps.as_ref(), new_fp.btc_pk_hex.clone(), None) - .unwrap(); - assert_eq!(fp_info.power, del1.total_sat); - - // Submit public randomness commitment for the FP and the involved heights - let msg = ExecuteMsg::CommitPublicRandomness { - fp_pubkey_hex: pk_hex.clone(), - start_height: pub_rand.start_height, - num_pub_rand: pub_rand.num_pub_rand, - commitment: pub_rand.commitment.into(), - signature: pubrand_signature.into(), - }; - - execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); - - // Call the begin-block sudo handler, for completeness - let res = call_begin_block( - &mut deps, - &add_finality_signature.block_app_hash, - initial_height + 1, - ) - .unwrap(); - assert_eq!(0, res.attributes.len()); - assert_eq!(0, res.messages.len()); - assert_eq!(0, res.events.len()); - - // Call the end-block sudo handler, so that the block is indexed in the store - let res = call_end_block( - &mut deps, - &add_finality_signature.block_app_hash, - initial_height + 1, - ) - .unwrap(); - assert_eq!(0, res.attributes.len()); - assert_eq!(0, res.messages.len()); - assert_eq!(1, res.events.len()); - assert_eq!( - res.events[0], - Event::new("index_block") - .add_attribute("module", "finality") - .add_attribute("last_height", (initial_height + 1).to_string()) - ); - - // Submit a finality signature from that finality provider at height initial_height + 1 - let submit_height = initial_height + 1; - let finality_signature = add_finality_signature.finality_sig.to_vec(); - let msg = ExecuteMsg::SubmitFinalitySignature { - fp_pubkey_hex: pk_hex.clone(), - height: submit_height, - pub_rand: pub_rand_one.into(), - proof: proof.into(), - block_hash: add_finality_signature.block_app_hash.to_vec().into(), - signature: Binary::new(finality_signature.clone()), - }; - - // Execute the message at the exact submit height, so that: - // 1. It's not rejected because of height being too high. - // 2. The FP has consolidated power at such height - // 3. There are no more pending / future blocks to process - let submit_env = mock_env_height(submit_height); - execute(deps.as_mut(), submit_env.clone(), info.clone(), msg).unwrap(); - - // Call the begin blocker, to compute the active FP set - let res = call_begin_block( - &mut deps, - &add_finality_signature.block_app_hash, - submit_height, - ) - .unwrap(); - assert_eq!(0, res.attributes.len()); - assert_eq!(0, res.events.len()); - assert_eq!(0, res.messages.len()); - - // Call the end blocker, to process the finality signatures - let res = call_end_block( - &mut deps, - &add_finality_signature.block_app_hash, - submit_height, - ) - .unwrap(); - assert_eq!(0, res.attributes.len()); - assert_eq!(2, res.events.len()); - assert_eq!( - res.events[0], - Event::new("index_block") - .add_attribute("module", "finality") - .add_attribute("last_height", submit_height.to_string()) - ); - assert_eq!( - res.events[1], - Event::new("finalize_block") - .add_attribute("module", "finality") - .add_attribute("finalized_height", submit_height.to_string()) - ); - assert_eq!(0, res.messages.len()); - - // Assert the submitted block has been indexed and finalised - let indexed_block = crate::queries::block(deps.as_ref(), submit_height).unwrap(); - assert_eq!( - indexed_block, - IndexedBlock { - height: submit_height, - app_hash: add_finality_signature.block_app_hash.to_vec(), - finalized: true, - } - ); - } - - #[test] - fn slashing_works() { - let mut deps = mock_dependencies(); - let info = message_info(&deps.api.addr_make(CREATOR), &[]); - - // Read public randomness commitment test data - let (pk_hex, pub_rand, pubrand_signature) = get_public_randomness_commitment(); - let pub_rand_one = get_pub_rand_value(); - // Read equivalent / consistent add finality signature test data - let add_finality_signature = get_add_finality_sig(); - let proof = add_finality_signature.proof.unwrap(); - - let initial_height = pub_rand.start_height; - let initial_env = mock_env_height(initial_height); - - instantiate( - deps.as_mut(), - initial_env.clone(), - info.clone(), - InstantiateMsg { - params: Some(get_params()), - admin: None, - }, - ) - .unwrap(); - - // Register one FP - // NOTE: the test data ensures that pub rand commit / finality sig are - // signed by the 1st FP - let new_fp = create_new_finality_provider(1); - assert_eq!(new_fp.btc_pk_hex, pk_hex); - - let msg = ExecuteMsg::BtcStaking { - new_fp: vec![new_fp.clone()], - active_del: vec![], - slashed_del: vec![], - unbonded_del: vec![], - }; - - let _res = execute(deps.as_mut(), initial_env.clone(), info.clone(), msg).unwrap(); - - // Add a delegation, so that the finality provider has some power - let mut del1 = crate::contract::tests::get_derived_btc_delegation(1, &[1]); - del1.fp_btc_pk_list = vec![pk_hex.clone()]; - - let msg = ExecuteMsg::BtcStaking { - new_fp: vec![], - active_del: vec![del1.clone()], - slashed_del: vec![], - unbonded_del: vec![], - }; - - execute(deps.as_mut(), initial_env.clone(), info.clone(), msg).unwrap(); - - // Check that the finality provider power has been updated - let fp_info = - queries::finality_provider_info(deps.as_ref(), new_fp.btc_pk_hex.clone(), None) - .unwrap(); - assert_eq!(fp_info.power, del1.total_sat); - - // Submit public randomness commitment for the FP and the involved heights - let msg = ExecuteMsg::CommitPublicRandomness { - fp_pubkey_hex: pk_hex.clone(), - start_height: pub_rand.start_height, - num_pub_rand: pub_rand.num_pub_rand, - commitment: pub_rand.commitment.into(), - signature: pubrand_signature.into(), - }; - - execute(deps.as_mut(), initial_env, info.clone(), msg).unwrap(); - - // Call the begin-block sudo handler at the next height, for completeness - let next_height = initial_height + 1; - call_begin_block( - &mut deps, - &add_finality_signature.block_app_hash, - next_height, - ) - .unwrap(); - - // Call the end-block sudo handler, so that the block is indexed in the store - call_end_block( - &mut deps, - &add_finality_signature.block_app_hash, - next_height, - ) - .unwrap(); - - // Submit a finality signature from that finality provider at next height (initial_height + 1) - let submit_height = next_height; - // Increase block height - let next_height = next_height + 1; - let next_env = mock_env_height(next_height); - // Call the begin-block sudo handler at the next height, for completeness - call_begin_block(&mut deps, "deadbeef01".as_bytes(), next_height).unwrap(); - - let finality_signature = add_finality_signature.finality_sig.to_vec(); - let msg = ExecuteMsg::SubmitFinalitySignature { - fp_pubkey_hex: pk_hex.clone(), - height: submit_height, - pub_rand: pub_rand_one.clone().into(), - proof: proof.clone().into(), - block_hash: add_finality_signature.block_app_hash.to_vec().into(), - signature: Binary::new(finality_signature.clone()), - }; - - let res = execute(deps.as_mut(), next_env.clone(), info.clone(), msg.clone()).unwrap(); - assert_eq!(0, res.messages.len()); - assert_eq!(0, res.events.len()); - - // Submitting the same signature twice is tolerated - let res = execute(deps.as_mut(), next_env.clone(), info.clone(), msg).unwrap(); - assert_eq!(0, res.messages.len()); - assert_eq!(0, res.events.len()); - - // Submit another (different and valid) finality signature, from the same finality provider - // at the same height - let add_finality_signature_2 = get_add_finality_sig_2(); - let msg = ExecuteMsg::SubmitFinalitySignature { - fp_pubkey_hex: pk_hex.clone(), - height: submit_height, - pub_rand: pub_rand_one.clone().into(), - proof: proof.into(), - block_hash: add_finality_signature_2.block_app_hash.to_vec().into(), - signature: Binary::new(add_finality_signature_2.finality_sig.to_vec()), - }; - let res = execute(deps.as_mut(), next_env.clone(), info.clone(), msg).unwrap(); - - // Assert the double signing evidence is proper - let btc_pk = hex::decode(pk_hex.clone()).unwrap(); - let evidence = crate::queries::evidence(deps.as_ref(), pk_hex.clone(), submit_height) - .unwrap() - .evidence - .unwrap(); - assert_eq!(evidence.block_height, submit_height); - assert_eq!(evidence.fp_btc_pk, btc_pk); - - // Assert the slashing propagation msg is there - assert_eq!(1, res.messages.len()); - // Assert the slashing propagation msg is proper - let babylon_addr = crate::queries::config(deps.as_ref()).unwrap().babylon; - // Assert the slashing event is there - assert_eq!(1, res.events.len()); - // Assert the slashing event is proper - assert_eq!(res.events[0].ty, "slashed_finality_provider".to_string()); - assert_eq!( - res.messages[0], - SubMsg::new(WasmMsg::Execute { - contract_addr: babylon_addr.to_string(), - msg: to_json_binary(&babylon_contract::ExecuteMsg::Slashing { evidence }).unwrap(), - funds: vec![] - }) - ); - - // Call the end-block sudo handler for completeness / realism - call_end_block(&mut deps, "deadbeef01".as_bytes(), next_height).unwrap(); - - // Call the next (final) block begin blocker, to compute the active FP set - let final_height = next_height + 1; - call_begin_block(&mut deps, "deadbeef02".as_bytes(), final_height).unwrap(); - - // Call the next (final) block end blocker, to process the finality signatures - call_end_block(&mut deps, "deadbeef02".as_bytes(), final_height).unwrap(); - - // Assert the canonical block has been indexed (and finalised) - let indexed_block = crate::queries::block(deps.as_ref(), submit_height).unwrap(); - assert_eq!( - indexed_block, - IndexedBlock { - height: submit_height, - app_hash: add_finality_signature.block_app_hash.to_vec(), - finalized: true, - } - ); - - // Assert the finality provider has been slashed - let fp = crate::queries::finality_provider(deps.as_ref(), pk_hex).unwrap(); - assert_eq!(fp.slashed_height, submit_height); - } -} diff --git a/contracts/btc-staking/src/lib.rs b/contracts/btc-staking/src/lib.rs index fb28b697..ecb8d13a 100644 --- a/contracts/btc-staking/src/lib.rs +++ b/contracts/btc-staking/src/lib.rs @@ -1,4 +1,3 @@ -mod finality; mod staking; mod validation; diff --git a/contracts/btc-staking/src/msg.rs b/contracts/btc-staking/src/msg.rs index 781d123f..5b9dd2e1 100644 --- a/contracts/btc-staking/src/msg.rs +++ b/contracts/btc-staking/src/msg.rs @@ -2,7 +2,6 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cw_controllers::AdminResponse; use babylon_apis::btc_staking_api::{ActiveBtcDelegation, FinalityProvider}; -use babylon_apis::finality_api::{Evidence, IndexedBlock, PubRandCommit}; use crate::state::config::{Config, Params}; use crate::state::staking::BtcDelegation; @@ -82,66 +81,10 @@ pub enum QueryMsg { start_after: Option, limit: Option, }, - /// `FinalitySignature` returns the signature of the finality provider for a given block height - /// - #[returns(FinalitySignatureResponse)] - FinalitySignature { btc_pk_hex: String, height: u64 }, - /// `PubRandCommit` returns the public random commitments for a given FP. - /// - /// `btc_pk_hex` is the BTC public key of the finality provider, in hex format. - /// - /// `start_after` is the height of to start after (before, if `reverse` is `true`), - /// or `None` to start from the beginning (end, if `reverse` is `true`). - /// `limit` is the maximum number of commitments to return. - /// `reverse` is an optional flag to return the commitments in reverse order - #[returns(PubRandCommit)] - PubRandCommit { - btc_pk_hex: String, - start_after: Option, - limit: Option, - reverse: Option, - }, - /// `FirstPubRandCommit` returns the first public random commitment (if any) for a given FP. - /// - /// It's a convenience shortcut of `PubRandCommit` with a `limit` of 1, and `reverse` set to - /// false. - /// - /// `btc_pk_hex` is the BTC public key of the finality provider, in hex format. - #[returns(Option)] - FirstPubRandCommit { btc_pk_hex: String }, - /// `LastPubRandCommit` returns the last public random commitment (if any) for a given FP. - /// - /// It's a convenience shortcut of `PubRandCommit` with a `limit` of 1, and `reverse` set to - /// true. - /// - /// `btc_pk_hex` is the BTC public key of the finality provider, in hex format. - #[returns(Option)] - LastPubRandCommit { btc_pk_hex: String }, /// `ActivatedHeight` returns the height at which the contract gets its first delegation, if any /// #[returns(ActivatedHeightResponse)] ActivatedHeight {}, - /// `Block` returns the indexed block information at height - /// - #[returns(IndexedBlock)] - Block { height: u64 }, - /// `Blocks` return the list of indexed blocks. - /// - /// `start_after` is the height of the block to start after (before, if `reverse` is `true`), - /// or `None` to start from the beginning (end, if `reverse` is `true`). - /// `limit` is the maximum number of blocks to return. - /// `finalised` is an optional filter to return only finalised blocks. - /// `reverse` is an optional flag to return the blocks in reverse order - #[returns(BlocksResponse)] - Blocks { - start_after: Option, - limit: Option, - finalised: Option, - reverse: Option, - }, - /// `Evidence` returns the evidence for a given FP and block height - #[returns(EvidenceResponse)] - Evidence { btc_pk_hex: String, height: u64 }, } #[cw_serde] @@ -174,22 +117,7 @@ pub struct FinalityProviderInfo { pub power: u64, } -#[cw_serde] -pub struct FinalitySignatureResponse { - pub signature: Vec, -} - #[cw_serde] pub struct ActivatedHeightResponse { pub height: u64, } - -#[cw_serde] -pub struct BlocksResponse { - pub blocks: Vec, -} - -#[cw_serde] -pub struct EvidenceResponse { - pub evidence: Option, -} diff --git a/contracts/btc-staking/src/queries.rs b/contracts/btc-staking/src/queries.rs index ad03bc42..279cd2b9 100644 --- a/contracts/btc-staking/src/queries.rs +++ b/contracts/btc-staking/src/queries.rs @@ -3,22 +3,19 @@ use std::str::FromStr; use bitcoin::hashes::Hash; use bitcoin::Txid; -use cosmwasm_std::Order::{Ascending, Descending}; +use cosmwasm_std::Order::Descending; use cosmwasm_std::{Deps, Order, StdResult}; use cw_storage_plus::Bound; use babylon_apis::btc_staking_api::FinalityProvider; -use babylon_apis::finality_api::IndexedBlock; use crate::error::ContractError; use crate::msg::{ - ActivatedHeightResponse, BlocksResponse, BtcDelegationsResponse, DelegationsByFPResponse, - EvidenceResponse, FinalityProviderInfo, FinalityProvidersByPowerResponse, - FinalityProvidersResponse, FinalitySignatureResponse, + ActivatedHeightResponse, BtcDelegationsResponse, DelegationsByFPResponse, FinalityProviderInfo, + FinalityProvidersByPowerResponse, FinalityProvidersResponse, }; use crate::state::config::{Config, Params}; use crate::state::config::{CONFIG, PARAMS}; -use crate::state::finality::{BLOCKS, EVIDENCES}; use crate::state::staking::{ fps, BtcDelegation, FinalityProviderState, ACTIVATED_HEIGHT, DELEGATIONS, FPS, FP_DELEGATIONS, }; @@ -173,19 +170,6 @@ pub fn finality_providers_by_power( Ok(FinalityProvidersByPowerResponse { fps }) } -pub fn finality_signature( - deps: Deps, - btc_pk_hex: String, - height: u64, -) -> StdResult { - match crate::state::finality::SIGNATURES.may_load(deps.storage, (height, &btc_pk_hex))? { - Some(sig) => Ok(FinalitySignatureResponse { signature: sig }), - None => Ok(FinalitySignatureResponse { - signature: Vec::new(), - }), // Empty signature response - } -} - pub fn activated_height(deps: Deps) -> Result { let activated_height = ACTIVATED_HEIGHT.may_load(deps.storage)?.unwrap_or_default(); Ok(ActivatedHeightResponse { @@ -193,56 +177,13 @@ pub fn activated_height(deps: Deps) -> Result StdResult { - BLOCKS.load(deps.storage, height) -} - -/// Get list of blocks. -/// `start_after`: The height to start after, if any. -/// `finalised`: List only finalised blocks if true, otherwise list all blocks. -/// `reverse`: List in descending order if present and true, otherwise in ascending order. -pub fn blocks( - deps: Deps, - start_after: Option, - limit: Option, - finalised: Option, - reverse: Option, -) -> Result { - let finalised = finalised.unwrap_or_default(); - let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; - let start_after = start_after.map(Bound::exclusive); - let (start, end, order) = if reverse.unwrap_or(false) { - (None, start_after, Descending) - } else { - (start_after, None, Ascending) - }; - let blocks = BLOCKS - .range_raw(deps.storage, start, end, order) - .filter(|item| { - if let Ok((_, block)) = item { - !finalised || block.finalized - } else { - true // don't filter errors - } - }) - .take(limit) - .map(|item| item.map(|(_, v)| v)) - .collect::, _>>()?; - Ok(BlocksResponse { blocks }) -} - -pub fn evidence(deps: Deps, btc_pk_hex: String, height: u64) -> StdResult { - let evidence = EVIDENCES.may_load(deps.storage, (&btc_pk_hex, height))?; - Ok(EvidenceResponse { evidence }) -} - #[cfg(test)] mod tests { use cosmwasm_std::storage_keys::namespace_with_key; use cosmwasm_std::testing::message_info; use cosmwasm_std::testing::{mock_dependencies, mock_env}; use cosmwasm_std::StdError::NotFound; - use cosmwasm_std::{from_json, Storage}; + use cosmwasm_std::{from_json, Env, Storage}; use babylon_apis::btc_staking_api::{FinalityProvider, UnbondedBtcDelegation}; @@ -251,7 +192,6 @@ mod tests { }; use crate::contract::{execute, instantiate}; use crate::error::ContractError; - use crate::finality::tests::mock_env_height; use crate::msg::{ExecuteMsg, FinalityProviderInfo, InstantiateMsg}; use crate::staking::tests::staking_tx_hash; use crate::state::config::PARAMS; @@ -259,6 +199,13 @@ mod tests { const CREATOR: &str = "creator"; + fn mock_env_height(height: u64) -> Env { + let mut env = mock_env(); + env.block.height = height; + + env + } + // Sort delegations by staking tx hash fn sort_delegations(dels: &[BtcDelegation]) -> Vec { let mut dels = dels.to_vec(); @@ -649,7 +596,7 @@ mod tests { let mut deps = mock_dependencies(); let info = message_info(&deps.api.addr_make(CREATOR), &[]); - let initial_env = crate::finality::tests::mock_env_height(10); + let initial_env = mock_env_height(10); instantiate( deps.as_mut(), diff --git a/contracts/btc-staking/src/staking.rs b/contracts/btc-staking/src/staking.rs index 6bf0aa8b..0a6267aa 100644 --- a/contracts/btc-staking/src/staking.rs +++ b/contracts/btc-staking/src/staking.rs @@ -2,17 +2,16 @@ use bitcoin::absolute::LockTime; use bitcoin::consensus::deserialize; use bitcoin::hashes::Hash; use bitcoin::{Transaction, Txid}; -use cosmwasm_std::{DepsMut, Env, Event, MessageInfo, Order, Response, Storage}; +use cosmwasm_std::{DepsMut, Env, Event, MessageInfo, Response, Storage}; use hex::ToHex; use std::str::FromStr; use crate::error::ContractError; -use crate::msg::FinalityProviderInfo; use crate::state::config::{ADMIN, CONFIG, PARAMS}; use crate::state::staking::{ fps, BtcDelegation, FinalityProviderState, ACTIVATED_HEIGHT, DELEGATIONS, DELEGATION_FPS, FPS, - FP_DELEGATIONS, FP_SET, TOTAL_POWER, + FP_DELEGATIONS, }; use crate::validation::{ verify_active_delegation, verify_new_fp, verify_slashed_delegation, verify_undelegation, @@ -314,6 +313,20 @@ fn handle_slashed_delegation( Ok(slashing_event) } +/// handle_slash_fp handles FP slashing at the staking level +pub fn handle_slash_fp( + deps: DepsMut, + env: Env, + info: &MessageInfo, + fp_btc_pk_hex: &str, +) -> Result, ContractError> { + let config = CONFIG.load(deps.storage)?; + if info.sender != config.babylon && !ADMIN.is_admin(deps.as_ref(), &info.sender)? { + return Err(ContractError::Unauthorized); + } + slash_finality_provider(deps, env, fp_btc_pk_hex) +} + /// btc_undelegate adds the signature of the unbonding tx signed by the staker to the given BTC /// delegation fn btc_undelegate( @@ -336,55 +349,13 @@ fn btc_undelegate( 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( - storage: &mut dyn Storage, - env: Env, - max_active_fps: usize, -) -> Result<(), ContractError> { - // Sort finality providers by power - let (finality_providers, running_total): (_, Vec<_>) = fps() - .idx - .power - .range(storage, None, None, Order::Descending) - .take(max_active_fps) - .scan(0u64, |acc, item| { - let (pk_hex, fp_state) = item.ok()?; // Error ends the iteration - - let fp_info = FinalityProviderInfo { - btc_pk_hex: pk_hex, - power: fp_state.power, - }; - *acc += fp_state.power; - Some((fp_info, *acc)) - }) - .filter(|(fp, _)| { - // Filter out FPs with no voting power - fp.power > 0 - }) - .unzip(); - - // TODO: Online FPs verification - // 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)?; - // Save the total voting power of the top n finality providers - let total_power = running_total.last().copied().unwrap_or_default(); - TOTAL_POWER.save(storage, &total_power)?; - - Ok(()) -} - /// `slash_finality_provider` slashes a finality provider with the given PK. /// A slashed finality provider will not have voting power pub(crate) fn slash_finality_provider( - deps: &mut DepsMut, + deps: DepsMut, env: Env, fp_btc_pk_hex: &str, - height: u64, -) -> Result<(), ContractError> { +) -> Result, ContractError> { // Ensure finality provider exists let mut fp = FPS.load(deps.storage, fp_btc_pk_hex)?; @@ -395,12 +366,12 @@ pub(crate) fn slash_finality_provider( )); } // Set the finality provider as slashed - fp.slashed_height = height; + fp.slashed_height = env.block.height; // Set BTC slashing height (if available from the babylon contract) // FIXME: Turn this into a hard error // return fmt.Errorf("failed to get current BTC tip") - let btc_height = get_btc_tip_height(deps).unwrap_or_default(); + let btc_height = get_btc_tip_height(&deps).unwrap_or_default(); fp.slashed_btc_height = btc_height; // Record slashed event. The next `BeginBlock` will consume this event for updating the active @@ -416,7 +387,8 @@ pub(crate) fn slash_finality_provider( // Save the finality provider back FPS.save(deps.storage, fp_btc_pk_hex, &fp)?; - Ok(()) + // TODO: Add events + Ok(Response::new()) } /// get_btc_tip_height queries the Babylon contract for the latest BTC tip height diff --git a/contracts/btc-staking/src/state/config.rs b/contracts/btc-staking/src/state/config.rs index cf3dbfa5..af413fa0 100644 --- a/contracts/btc-staking/src/state/config.rs +++ b/contracts/btc-staking/src/state/config.rs @@ -15,13 +15,7 @@ pub(crate) const ADMIN: Admin = Admin::new("admin"); // TODO: Add / enable config entries as needed #[cw_serde] pub struct Config { - pub denom: String, pub babylon: Addr, - // covenant_pks is the list of public keys held by the covenant committee each PK - // follows encoding in BIP-340 spec on Bitcoin - // pub covenant_pks: Vec, - // covenant_quorum is the minimum number of signatures needed for the covenant multi-signature - // pub covenant_quorum: u32, } /// Params define Consumer-selectable BTC staking parameters @@ -41,14 +35,6 @@ pub struct Params { // `min_commission_rate` is the chain-wide minimum commission rate that a finality provider // can charge their delegators // pub min_commission_rate: Decimal, - /// `max_active_finality_providers` is the maximum number of active finality providers in the - /// BTC staking protocol - #[derivative(Default(value = "100"))] - pub max_active_finality_providers: u32, - /// `min_pub_rand` is the minimum amount of public randomness each public randomness commitment - /// should commit - #[derivative(Default(value = "1"))] - pub min_pub_rand: u64, /// `slashing_address` is the address that the slashed BTC goes to. /// The address is in string format on Bitcoin. #[derivative(Default(value = "String::from(\"n4cV57jePmAAue2WTTBQzH3k3R2rgWBQwY\")"))] diff --git a/contracts/btc-staking/src/state/finality.rs b/contracts/btc-staking/src/state/finality.rs deleted file mode 100644 index 742d2a5d..00000000 --- a/contracts/btc-staking/src/state/finality.rs +++ /dev/null @@ -1,14 +0,0 @@ -use babylon_apis::finality_api::{Evidence, IndexedBlock}; -use cw_storage_plus::{Item, Map}; - -/// Map of signatures by block height and FP -pub(crate) const SIGNATURES: Map<(u64, &str), Vec> = Map::new("fp_sigs"); - -/// Map of blocks information by height -pub(crate) const BLOCKS: Map = Map::new("blocks"); - -/// Next height to finalise -pub(crate) const NEXT_HEIGHT: Item = Item::new("next_height"); - -/// Map of double signing evidence by FP and block height -pub(crate) const EVIDENCES: Map<(&str, u64), Evidence> = Map::new("evidences"); diff --git a/contracts/btc-staking/src/state/mod.rs b/contracts/btc-staking/src/state/mod.rs index 9a0de080..018e8070 100644 --- a/contracts/btc-staking/src/state/mod.rs +++ b/contracts/btc-staking/src/state/mod.rs @@ -1,6 +1,4 @@ pub mod config; -pub mod finality; -pub mod public_randomness; pub mod staking; mod fp_index; diff --git a/contracts/btc-staking/src/state/staking.rs b/contracts/btc-staking/src/state/staking.rs index fb842e89..6696af7c 100644 --- a/contracts/btc-staking/src/state/staking.rs +++ b/contracts/btc-staking/src/state/staking.rs @@ -1,7 +1,6 @@ use cosmwasm_schema::cw_serde; use cw_storage_plus::{IndexedSnapshotMap, Item, Map, MultiIndex, Strategy}; -use crate::msg::FinalityProviderInfo; use crate::state::fp_index::FinalityProviderIndexes; use babylon_apis::btc_staking_api::{BTCDelegationStatus, FinalityProvider, HASH_SIZE}; use babylon_apis::{btc_staking_api, Bytes}; @@ -216,12 +215,6 @@ pub const FP_POWER_KEY: &str = "fp_state__power"; /// The height at which the contract gets its first delegation pub const ACTIVATED_HEIGHT: Item = Item::new("activated_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"); - /// Indexed snapshot map for finality providers. /// /// This allows querying the map finality providers, sorted by their (aggregated) power. diff --git a/contracts/btc-staking/tests/integration.rs b/contracts/btc-staking/tests/integration.rs index bbdbfb8a..bc990493 100644 --- a/contracts/btc-staking/tests/integration.rs +++ b/contracts/btc-staking/tests/integration.rs @@ -9,9 +9,7 @@ static WASM: &[u8] = include_bytes!("../../../artifacts/btc_staking.wasm"); const MAX_WASM_SIZE: usize = 800 * 1024; // 800 KB // wasm binary with full validation -// TODO: optimise to 800 KB static WASM_FULL: &[u8] = include_bytes!("../../../artifacts/btc_staking-full-validation.wasm"); -const MAX_WASM_SIZE_FULL: usize = 1024 * 1024; // 1 MB const CREATOR: &str = "creator"; @@ -24,10 +22,10 @@ fn wasm_size_limit_check() { MAX_WASM_SIZE ); assert!( - WASM_FULL.len() < MAX_WASM_SIZE_FULL, + WASM_FULL.len() < MAX_WASM_SIZE, "BTC staking contract (with full validation) wasm binary is too large: {} (target: {})", WASM_FULL.len(), - MAX_WASM_SIZE_FULL + MAX_WASM_SIZE ); } diff --git a/packages/apis/src/btc_staking_api.rs b/packages/apis/src/btc_staking_api.rs index 78d4a663..b7663fa2 100644 --- a/packages/apis/src/btc_staking_api.rs +++ b/packages/apis/src/btc_staking_api.rs @@ -4,8 +4,6 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{Binary, Decimal}; -use babylon_merkle::Proof; - /// Hash size in bytes pub const HASH_SIZE: usize = 32; @@ -21,57 +19,11 @@ pub enum ExecuteMsg { slashed_del: Vec, unbonded_del: Vec, }, - /// Committing a sequence of public randomness for EOTS - // TODO: Move to its own module / contract - CommitPublicRandomness { - /// `fp_pubkey_hex` is the BTC PK of the finality provider that commits the public randomness - fp_pubkey_hex: String, - /// `start_height` is the start block height of the list of public randomness - start_height: u64, - /// `num_pub_rand` is the amount of public randomness committed - num_pub_rand: u64, - /// `commitment` is the commitment of these public randomness values. - /// Currently, it's the root of the Merkle tree that includes the public randomness - commitment: Binary, - /// `signature` is the signature on (start_height || num_pub_rand || commitment) signed by - /// the SK corresponding to `fp_pubkey_hex`. - /// This prevents others committing public randomness on behalf of `fp_pubkey_hex` - signature: Binary, - }, - /// Submit Finality Signature. - /// - /// This is a message that can be called by a finality provider to submit their finality - /// signature to the Consumer chain. - /// The signature is verified by the Consumer chain using the finality provider's public key - /// - /// This message is equivalent to the `MsgAddFinalitySig` message in the Babylon finality protobuf - /// defs. - // TODO: Move to its own module / contract - SubmitFinalitySignature { - fp_pubkey_hex: String, - height: u64, - pub_rand: Binary, - proof: Proof, - block_hash: Binary, - signature: Binary, - }, -} - -#[cw_serde] -pub enum SudoMsg { - /// The SDK should call SudoMsg::BeginBlock{} once per block (in BeginBlock). - /// It allows the staking module to index the BTC height, and update the power - /// distribution of the active Finality Providers. - BeginBlock { - hash_hex: String, - app_hash_hex: String, - }, - /// The SDK should call SudoMsg::EndBlock{} once per block (in EndBlock). - /// It allows the finality module to index blocks and tally the finality provider votes - EndBlock { - hash_hex: String, - app_hash_hex: String, - }, + /// Slash finality provider staking power. + /// Used by the babylon-contract only. + /// The Babylon contract will call this message to set the finality provider's staking power to + /// zero when the finality provider is found to be malicious by the finality contract. + Slash { fp_btc_pk_hex: String }, } #[cw_serde] diff --git a/packages/apis/src/finality_api.rs b/packages/apis/src/finality_api.rs index f60fbfa3..38a3c6de 100644 --- a/packages/apis/src/finality_api.rs +++ b/packages/apis/src/finality_api.rs @@ -2,9 +2,54 @@ /// The definitions here roughly follow the same structure as the equivalent IBC protobuf pub struct types, /// defined in `packages/proto/src/gen/babylon.finality.v1.rs` use cosmwasm_schema::cw_serde; +use cosmwasm_std::Binary; + +use babylon_merkle::Proof; use crate::Bytes; +#[cw_serde] +/// babylon_finality execution handlers +pub enum ExecuteMsg { + /// Change the admin + UpdateAdmin { admin: Option }, + /// Set the BTC staking addr. + /// Only admin or the babylon contract can set this + UpdateStaking { staking: String }, + /// Committing a sequence of public randomness for EOTS + CommitPublicRandomness { + /// `fp_pubkey_hex` is the BTC PK of the finality provider that commits the public randomness + fp_pubkey_hex: String, + /// `start_height` is the start block height of the list of public randomness + start_height: u64, + /// `num_pub_rand` is the amount of public randomness committed + num_pub_rand: u64, + /// `commitment` is the commitment of these public randomness values. + /// Currently, it's the root of the Merkle tree that includes the public randomness + commitment: Binary, + /// `signature` is the signature on (start_height || num_pub_rand || commitment) signed by + /// the SK corresponding to `fp_pubkey_hex`. + /// This prevents others committing public randomness on behalf of `fp_pubkey_hex` + signature: Binary, + }, + /// Submit Finality Signature. + /// + /// This is a message that can be called by a finality provider to submit their finality + /// signature to the Consumer chain. + /// The signature is verified by the Consumer chain using the finality provider's public key + /// + /// This message is equivalent to the `MsgAddFinalitySig` message in the Babylon finality protobuf + /// defs. + SubmitFinalitySignature { + fp_pubkey_hex: String, + height: u64, + pub_rand: Binary, + proof: Proof, + block_hash: Binary, + signature: Binary, + }, +} + /// `IndexedBlock` is the necessary metadata and finalization status of a block #[cw_serde] pub struct IndexedBlock { @@ -69,3 +114,20 @@ pub struct Evidence { /// Deserializes to `SchnorrEOTSSig` pub fork_finality_sig: Bytes, } + +#[cw_serde] +pub enum SudoMsg { + /// The SDK should call SudoMsg::BeginBlock{} once per block (in BeginBlock). + /// It allows the staking module to index the BTC height, and update the power + /// distribution of the active Finality Providers. + BeginBlock { + hash_hex: String, + app_hash_hex: String, + }, + /// The SDK should call SudoMsg::EndBlock{} once per block (in EndBlock). + /// It allows the finality module to index blocks and tally the finality provider votes + EndBlock { + hash_hex: String, + app_hash_hex: String, + }, +} diff --git a/packages/bindings-test/src/multitest.rs b/packages/bindings-test/src/multitest.rs index ad5c0192..731cca46 100644 --- a/packages/bindings-test/src/multitest.rs +++ b/packages/bindings-test/src/multitest.rs @@ -165,9 +165,13 @@ impl BabylonApp { } pub fn new_genesis(owner: &str) -> Self { + BabylonApp::new_at_height(owner, 0) + } + + pub fn new_at_height(owner: &str, height: u64) -> Self { let owner = Addr::unchecked(owner); let block_info = BlockInfo { - height: 0, + height, time: Timestamp::from_seconds(1714119228), chain_id: "babylon-testnet-phase-3".to_owned(), }; @@ -208,7 +212,7 @@ impl BabylonApp { } /// This advances BlockInfo by given number of blocks. - /// It does not do any callbacks, but keeps the ratio of seconds/blokc + /// It does not do any callbacks, but keeps the ratio of seconds/block pub fn advance_blocks(&mut self, blocks: u64) { self.update_block(|block| { block.time = block.time.plus_seconds(BLOCK_TIME * blocks);