diff --git a/Cargo.lock b/Cargo.lock index bbc91403..3ef4a360 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -386,7 +386,7 @@ dependencies = [ [[package]] name = "external-staking" -version = "0.2.0" +version = "0.3.0-alpha.1" dependencies = [ "anyhow", "cosmwasm-schema", @@ -553,7 +553,7 @@ checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "mesh-apis" -version = "0.2.0" +version = "0.3.0-alpha.1" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -566,7 +566,7 @@ dependencies = [ [[package]] name = "mesh-bindings" -version = "0.2.0" +version = "0.3.0-alpha.1" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -574,7 +574,7 @@ dependencies = [ [[package]] name = "mesh-converter" -version = "0.2.0" +version = "0.3.0-alpha.1" dependencies = [ "anyhow", "cosmwasm-schema", @@ -595,7 +595,7 @@ dependencies = [ [[package]] name = "mesh-mocks" -version = "0.2.0" +version = "0.3.0-alpha.1" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -609,7 +609,7 @@ dependencies = [ [[package]] name = "mesh-native-staking" -version = "0.2.0" +version = "0.3.0-alpha.1" dependencies = [ "anyhow", "cosmwasm-schema", @@ -632,7 +632,7 @@ dependencies = [ [[package]] name = "mesh-native-staking-proxy" -version = "0.2.0" +version = "0.3.0-alpha.1" dependencies = [ "anyhow", "cosmwasm-schema", @@ -655,7 +655,7 @@ dependencies = [ [[package]] name = "mesh-simple-price-feed" -version = "0.2.0" +version = "0.3.0-alpha.1" dependencies = [ "anyhow", "cosmwasm-schema", @@ -676,7 +676,7 @@ dependencies = [ [[package]] name = "mesh-sync" -version = "0.2.0" +version = "0.3.0-alpha.1" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -688,7 +688,7 @@ dependencies = [ [[package]] name = "mesh-vault" -version = "0.2.0" +version = "0.3.0-alpha.1" dependencies = [ "anyhow", "cosmwasm-schema", @@ -710,7 +710,7 @@ dependencies = [ [[package]] name = "mesh-virtual-staking" -version = "0.2.0" +version = "0.3.0-alpha.1" dependencies = [ "anyhow", "cosmwasm-schema", diff --git a/Cargo.toml b/Cargo.toml index f777def3..3cc65b8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ resolver = "2" [workspace.package] edition = "2021" -version = "0.2.0" +version = "0.3.0-alpha.1" license = "MIT" repository = "https://github.com/osmosis-labs/mesh-security" diff --git a/contracts/consumer/converter/src/ibc.rs b/contracts/consumer/converter/src/ibc.rs index 2e29331e..f085633a 100644 --- a/contracts/consumer/converter/src/ibc.rs +++ b/contracts/consumer/converter/src/ibc.rs @@ -2,13 +2,17 @@ use cosmwasm_std::entry_point; use cosmwasm_std::{ - from_slice, DepsMut, Env, Ibc3ChannelOpenResponse, IbcBasicResponse, IbcChannel, - IbcChannelCloseMsg, IbcChannelConnectMsg, IbcChannelOpenMsg, IbcChannelOpenResponse, - IbcPacketAckMsg, IbcPacketReceiveMsg, IbcPacketTimeoutMsg, IbcReceiveResponse, + from_slice, to_binary, DepsMut, Env, Event, Ibc3ChannelOpenResponse, IbcBasicResponse, + IbcChannel, IbcChannelCloseMsg, IbcChannelConnectMsg, IbcChannelOpenMsg, + IbcChannelOpenResponse, IbcMsg, IbcPacketAckMsg, IbcPacketReceiveMsg, IbcPacketTimeoutMsg, + IbcReceiveResponse, IbcTimeout, }; use cw_storage_plus::Item; -use mesh_apis::ibc::{validate_channel_order, ProtocolVersion, PROTOCOL_NAME}; +use mesh_apis::ibc::{ + validate_channel_order, AckWrapper, AddValidator, ConsumerPacket, ProtocolVersion, + PROTOCOL_NAME, +}; use crate::error::ContractError; @@ -20,6 +24,16 @@ const MIN_IBC_PROTOCOL_VERSION: &str = "1.0.0"; // IBC specific state const IBC_CHANNEL: Item = Item::new("ibc_channel"); +// Let those validator syncs take a day... +const DEFAULT_TIMEOUT: u64 = 24 * 60 * 60; + +fn packet_timeout(env: &Env) -> IbcTimeout { + // No idea about their blocktime, but 24 hours ahead of our view of the clock + // should be decently in the future. + let timeout = env.block.time.plus_seconds(DEFAULT_TIMEOUT); + IbcTimeout::with_timestamp(timeout) +} + #[cfg_attr(not(feature = "library"), entry_point)] /// enforces ordering and versioning constraints pub fn ibc_channel_open( @@ -63,7 +77,7 @@ pub fn ibc_channel_open( /// once it's established, we store data pub fn ibc_channel_connect( deps: DepsMut, - _env: Env, + env: Env, msg: IbcChannelConnectMsg, ) -> Result { // ensure we have no channel yet @@ -89,8 +103,27 @@ pub fn ibc_channel_connect( // store the channel IBC_CHANNEL.save(deps.storage, &channel)?; - // FIXME: later we start with sending the validator sync packets - Ok(IbcBasicResponse::default()) + // Send a validator sync packet to arrive with the newly established channel + let validators = deps.querier.query_all_validators()?; + let updates = validators + .into_iter() + .map(|v| AddValidator { + valoper: v.address, + // TODO: not yet available in CosmWasm APIs + pub_key: "TODO".to_string(), + // Use current height/time as start height/time (no slashing before mesh starts) + start_height: env.block.height, + start_time: env.block.time.seconds(), + }) + .collect(); + let packet = ConsumerPacket::AddValidators(updates); + let msg = IbcMsg::SendPacket { + channel_id: channel.endpoint.channel_id, + data: to_binary(&packet)?, + timeout: packet_timeout(&env), + }; + + Ok(IbcBasicResponse::new().add_message(msg)) } #[cfg_attr(not(feature = "library"), entry_point)] @@ -117,21 +150,43 @@ pub fn ibc_packet_receive( } #[cfg_attr(not(feature = "library"), entry_point)] -/// never should be called as we do not send packets +/// We get acks on sync state without much to do. +/// If it succeeded, take no action. If it errored, we can't do anything else and let it go. +/// We just log the error cases so they can be detected. pub fn ibc_packet_ack( _deps: DepsMut, _env: Env, - _msg: IbcPacketAckMsg, + msg: IbcPacketAckMsg, ) -> Result { - Ok(IbcBasicResponse::new().add_attribute("action", "ibc_packet_ack")) + let ack: AckWrapper = from_slice(&msg.acknowledgement.data)?; + let mut res = IbcBasicResponse::new(); + match ack { + AckWrapper::Result(_) => {} + AckWrapper::Error(e) => { + // The wasmd framework will label this with the contract_addr, which helps us find the port and issue. + // Provide info to find the actual packet. + let event = Event::new("mesh_ibc_error") + .add_attribute("error", e) + .add_attribute("channel", msg.original_packet.src.channel_id) + .add_attribute("sequence", msg.original_packet.sequence.to_string()); + res = res.add_event(event); + } + } + Ok(res) } #[cfg_attr(not(feature = "library"), entry_point)] -/// never should be called as we do not send packets +/// The most we can do here is retry the packet, hoping it will eventually arrive. pub fn ibc_packet_timeout( _deps: DepsMut, - _env: Env, - _msg: IbcPacketTimeoutMsg, + env: Env, + msg: IbcPacketTimeoutMsg, ) -> Result { - Ok(IbcBasicResponse::new().add_attribute("action", "ibc_packet_timeout")) + // Play it again, Sam. + let msg = IbcMsg::SendPacket { + channel_id: msg.packet.src.channel_id, + data: msg.packet.data, + timeout: packet_timeout(&env), + }; + Ok(IbcBasicResponse::new().add_message(msg)) } diff --git a/contracts/provider/external-staking/src/contract.rs b/contracts/provider/external-staking/src/contract.rs index f6d0a560..bdaa97b9 100644 --- a/contracts/provider/external-staking/src/contract.rs +++ b/contracts/provider/external-staking/src/contract.rs @@ -13,9 +13,10 @@ use sylvia::contract; use sylvia::types::{ExecCtx, InstantiateCtx, QueryCtx}; use crate::error::ContractError; +use crate::ibc::VAL_CRDT; use crate::msg::{ - AuthorizedEndpointResponse, ConfigResponse, IbcChannelResponse, PendingRewards, - ReceiveVirtualStake, StakeInfo, StakesResponse, + AuthorizedEndpointResponse, ConfigResponse, IbcChannelResponse, ListRemoteValidatorsResponse, + PendingRewards, ReceiveVirtualStake, StakeInfo, StakesResponse, }; use crate::state::{Config, Distribution, PendingUnbond, Stake}; @@ -297,6 +298,20 @@ impl ExternalStakingContract<'_> { Ok(IbcChannelResponse { channel }) } + /// Show all external validators that we know to be active (and can delegate to) + #[msg(query)] + pub fn list_remote_validators( + &self, + ctx: QueryCtx, + start_after: Option, + limit: Option, + ) -> Result { + let limit = limit.unwrap_or(100) as usize; + let validators = + VAL_CRDT.list_active_validators(ctx.deps.storage, start_after.as_deref(), limit)?; + Ok(ListRemoteValidatorsResponse { validators }) + } + /// Queries for stake info /// /// If stake is not existing in the system is queried, the zero-stake is returned diff --git a/contracts/provider/external-staking/src/crdt.rs b/contracts/provider/external-staking/src/crdt.rs new file mode 100644 index 00000000..c1ca2962 --- /dev/null +++ b/contracts/provider/external-staking/src/crdt.rs @@ -0,0 +1,221 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Order, StdError, StdResult, Storage}; +use cw_storage_plus::{Bound, Map}; + +// Question: Do we need to add more info here if we want to keep historical info for slashing. +// Would we ever need the pubkeys for a Tombstoned validator? Or do we consider it already slashed and therefore unslashable? +#[cw_serde] +pub enum ValidatorState { + Active(ActiveState), + Tombstoned {}, +} + +impl ValidatorState { + pub fn is_active(&self) -> bool { + matches!(self, ValidatorState::Active(_)) + } +} + +#[cw_serde] +/// Active state maintains a sorted list of updates with no duplicates. +/// The first one is the one with the highest start_height. +pub struct ActiveState(Vec); + +impl ActiveState { + /// Add one more element to this list, maintaining the constraints + pub fn insert_unique(&mut self, update: ValUpdate) { + self.0.push(update); + self.0.sort_by(|a, b| b.start_height.cmp(&a.start_height)); + self.0.dedup(); + } +} + +#[cw_serde] +pub struct ValUpdate { + pub pub_key: String, + pub start_height: u64, + pub start_time: u64, +} + +impl ValUpdate { + pub fn new(pub_key: impl Into, start_height: u64, start_time: u64) -> Self { + ValUpdate { + pub_key: pub_key.into(), + start_height, + start_time, + } + } +} + +/// This holds all CRDT related state and logic (related to validators) +pub struct CrdtState<'a> { + validators: Map<'a, &'a str, ValidatorState>, +} + +impl<'a> CrdtState<'a> { + pub const fn new() -> Self { + CrdtState { + validators: Map::new("crdt.validators"), + } + } + + pub fn add_validator( + &self, + storage: &mut dyn Storage, + valoper: &str, + update: ValUpdate, + ) -> Result<(), StdError> { + let mut state = self + .validators + .may_load(storage, valoper)? + .unwrap_or_else(|| ValidatorState::Active(ActiveState(vec![]))); + + match &mut state { + ValidatorState::Active(active) => { + // add to the set, ensuring there are no duplicates + active.insert_unique(update); + } + ValidatorState::Tombstoned {} => { + // we just silently ignore it here + } + } + + self.validators.save(storage, valoper, &state) + } + + pub fn remove_validator( + &self, + storage: &mut dyn Storage, + valoper: &str, + ) -> Result<(), StdError> { + let state = ValidatorState::Tombstoned {}; + self.validators.save(storage, valoper, &state) + } + + pub fn is_active_validator(&self, storage: &dyn Storage, valoper: &str) -> StdResult { + let active = self + .validators + .may_load(storage, valoper)? + .map(|s| s.is_active()) + .unwrap_or(false); + Ok(active) + } + + /// This returns the valoper address of all active validators + pub fn list_active_validators( + &self, + storage: &dyn Storage, + start_after: Option<&str>, + limit: usize, + ) -> StdResult> { + let start = start_after.map(Bound::exclusive); + self.validators + .range(storage, start, None, Order::Ascending) + .filter_map(|r| match r { + Ok((valoper, ValidatorState::Active(_))) => Some(Ok(valoper)), + Ok((_, ValidatorState::Tombstoned {})) => None, + Err(e) => Some(Err(e)), + }) + .take(limit) + .collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use cosmwasm_std::MemoryStorage; + + fn mock_update(start_height: u64) -> ValUpdate { + ValUpdate { + pub_key: "TODO".to_string(), + start_height, + start_time: 1687339542, + } + } + + // We add three new validators, and remove one + #[test] + fn happy_path() { + let mut storage = MemoryStorage::new(); + let crdt = CrdtState::new(); + + crdt.add_validator(&mut storage, "alice", mock_update(123)) + .unwrap(); + crdt.add_validator(&mut storage, "bob", mock_update(200)) + .unwrap(); + crdt.add_validator(&mut storage, "carl", mock_update(303)) + .unwrap(); + crdt.remove_validator(&mut storage, "bob").unwrap(); + + assert!(crdt.is_active_validator(&storage, "alice").unwrap()); + assert!(!crdt.is_active_validator(&storage, "bob").unwrap()); + assert!(crdt.is_active_validator(&storage, "carl").unwrap()); + + let active = crdt.list_active_validators(&storage, None, 10).unwrap(); + assert_eq!(active, vec!["alice".to_string(), "carl".to_string()]); + } + + // Like happy path, but we remove bob before he was ever added + #[test] + fn remove_before_add_works() { + let mut storage = MemoryStorage::new(); + let crdt = CrdtState::new(); + + crdt.remove_validator(&mut storage, "bob").unwrap(); + crdt.add_validator(&mut storage, "alice", mock_update(123)) + .unwrap(); + crdt.add_validator(&mut storage, "bob", mock_update(200)) + .unwrap(); + crdt.add_validator(&mut storage, "carl", mock_update(303)) + .unwrap(); + + assert!(crdt.is_active_validator(&storage, "alice").unwrap()); + assert!(!crdt.is_active_validator(&storage, "bob").unwrap()); + assert!(crdt.is_active_validator(&storage, "carl").unwrap()); + + let active = crdt.list_active_validators(&storage, None, 10).unwrap(); + assert_eq!(active, vec!["alice".to_string(), "carl".to_string()]); + } + + // add and remove many validators, then iterate over them + #[test] + fn pagination_works() { + let mut storage = MemoryStorage::new(); + let crdt = CrdtState::new(); + + // use two digits so numeric and alphabetic sort match (-2 is after -11, but -02 is before -11) + let mut validators: Vec<_> = (0..20).map(|i| format!("validator-{:02}", i)).collect(); + for v in &validators { + crdt.add_validator(&mut storage, v, mock_update(123)) + .unwrap(); + } + // in reverse order, so remove doesn't shift the indexes we will later read + for i in [19, 17, 12, 11, 7, 4, 3] { + crdt.remove_validator(&mut storage, &validators[i]).unwrap(); + validators.remove(i); + } + + // total of 13 if we get them all + let active = crdt.list_active_validators(&storage, None, 20).unwrap(); + assert_eq!(active, validators); + assert_eq!(active.len(), 13); + + // paginate by 7 + let active = crdt.list_active_validators(&storage, None, 7).unwrap(); + assert_eq!(active.len(), 7); + assert_eq!(active, validators[0..7]); + let active = crdt + .list_active_validators(&storage, Some(&active[6]), 7) + .unwrap(); + assert_eq!(active.len(), 6); + assert_eq!(active, validators[7..]); + let active = crdt + .list_active_validators(&storage, Some(&active[5]), 7) + .unwrap(); + assert_eq!(active, Vec::::new()); + } + + // TODO: test key rotation later +} diff --git a/contracts/provider/external-staking/src/ibc.rs b/contracts/provider/external-staking/src/ibc.rs index f40e459d..cf65ded3 100644 --- a/contracts/provider/external-staking/src/ibc.rs +++ b/contracts/provider/external-staking/src/ibc.rs @@ -7,9 +7,16 @@ use cosmwasm_std::{ IbcPacketAckMsg, IbcPacketReceiveMsg, IbcPacketTimeoutMsg, IbcReceiveResponse, }; use cw_storage_plus::Item; -use mesh_apis::ibc::{validate_channel_order, ProtocolVersion}; +use mesh_apis::ibc::{ + ack_success, validate_channel_order, AddValidator, AddValidatorsAck, ConsumerPacket, + ProtocolVersion, RemoveValidatorsAck, +}; -use crate::{error::ContractError, msg::AuthorizedEndpoint}; +use crate::{ + crdt::{CrdtState, ValUpdate}, + error::ContractError, + msg::AuthorizedEndpoint, +}; /// This is the maximum version of the Mesh Security protocol that we support const SUPPORTED_IBC_PROTOCOL_VERSION: &str = "1.0.0"; @@ -22,6 +29,8 @@ pub const AUTH_ENDPOINT: Item = Item::new("auth_endpoint"); // TODO: expected endpoint pub const IBC_CHANNEL: Item = Item::new("ibc_channel"); +pub const VAL_CRDT: CrdtState = CrdtState::new(); + #[cfg_attr(not(feature = "library"), entry_point)] /// enforces ordering and versioning constraints pub fn ibc_channel_open( @@ -89,8 +98,6 @@ pub fn ibc_channel_connect( } #[cfg_attr(not(feature = "library"), entry_point)] -/// On closed channel, we take all tokens from reflect contract to this contract. -/// We also delete the channel entry from accounts. pub fn ibc_channel_close( _deps: DepsMut, _env: Env, @@ -100,15 +107,43 @@ pub fn ibc_channel_close( } #[cfg_attr(not(feature = "library"), entry_point)] -/// we look for a the proper reflect contract to relay to and send the message -/// We cannot return any meaningful response value as we do not know the response value -/// of execution. We just return ok if we dispatched, error if we failed to dispatch +// this accepts validator sync packets and updates the crdt state pub fn ibc_packet_receive( - _deps: DepsMut, + deps: DepsMut, _env: Env, - _msg: IbcPacketReceiveMsg, + msg: IbcPacketReceiveMsg, ) -> Result { - todo!(); + // There is only one channel, so we don't need to switch. + // We also don't care about packet sequence as this is fully commutative. + let packet: ConsumerPacket = from_slice(&msg.packet.data)?; + let ack = match packet { + ConsumerPacket::AddValidators(to_add) => { + for AddValidator { + valoper, + pub_key, + start_height, + start_time, + } in to_add + { + let update = ValUpdate { + pub_key, + start_height, + start_time, + }; + VAL_CRDT.add_validator(deps.storage, &valoper, update)?; + } + ack_success(&AddValidatorsAck {})? + } + ConsumerPacket::RemoveValidators(to_remove) => { + for valoper in to_remove { + VAL_CRDT.remove_validator(deps.storage, &valoper)?; + } + ack_success(&RemoveValidatorsAck {})? + } + }; + + // return empty success ack + Ok(IbcReceiveResponse::new().set_ack(ack)) } #[cfg_attr(not(feature = "library"), entry_point)] diff --git a/contracts/provider/external-staking/src/lib.rs b/contracts/provider/external-staking/src/lib.rs index 5b13f321..64546451 100644 --- a/contracts/provider/external-staking/src/lib.rs +++ b/contracts/provider/external-staking/src/lib.rs @@ -1,4 +1,5 @@ pub mod contract; +pub mod crdt; pub mod error; pub mod ibc; pub mod msg; diff --git a/contracts/provider/external-staking/src/msg.rs b/contracts/provider/external-staking/src/msg.rs index e34f8134..3220b37a 100644 --- a/contracts/provider/external-staking/src/msg.rs +++ b/contracts/provider/external-staking/src/msg.rs @@ -33,6 +33,11 @@ pub struct IbcChannelResponse { pub channel: IbcChannel, } +#[cw_serde] +pub struct ListRemoteValidatorsResponse { + pub validators: Vec, +} + /// Config information returned with query #[cw_serde] pub struct ConfigResponse { diff --git a/packages/apis/src/ibc/packet.rs b/packages/apis/src/ibc/packet.rs index 7dfd7a19..6236e4eb 100644 --- a/packages/apis/src/ibc/packet.rs +++ b/packages/apis/src/ibc/packet.rs @@ -1,11 +1,13 @@ +use std::error::Error; + use cosmwasm_schema::cw_serde; -use cosmwasm_std::Coin; +use cosmwasm_std::{to_binary, Binary, Coin, StdResult}; /// These are messages sent from provider -> consumer /// ibc_packet_receive in converter must handle them all. /// Each one has a different ack to be used in the reply. #[cw_serde] -pub enum ProviderMsg { +pub enum ProviderPacket { /// This should be called when we lock more tokens to virtually stake on a given validator Stake { validator: String, @@ -13,6 +15,8 @@ pub enum ProviderMsg { /// It will be converted to the consumer-side staking token in the converter with help /// of the price feed. stake: Coin, + /// This is local to the sending side to track the transaction, should be passed through opaquely on the consumer + tx_id: u64, }, /// This should be called when we begin the unbonding period of some more tokens previously virtually staked Unstake { @@ -21,61 +25,85 @@ pub enum ProviderMsg { /// It will be converted to the consumer-side staking token in the converter with help /// of the price feed. unstake: Coin, + /// This is local to the sending side to track the transaction, should be passed through opaquely on the consumer + tx_id: u64, }, } -/// Ack sent for ProviderMsg::Stake +/// Ack sent for ProviderPacket::Stake #[cw_serde] -pub struct StakeAck {} +pub struct StakeAck { + /// Return the value from the original packet + tx_id: u64, +} -/// Ack sent for ProviderMsg::Unstake +/// Ack sent for ProviderPacket::Unstake #[cw_serde] -pub struct UnstakeAck {} +pub struct UnstakeAck { + /// Return the value from the original packet + tx_id: u64, +} /// These are messages sent from consumer -> provider /// ibc_packet_receive in external-staking must handle them all. #[cw_serde] -pub enum ConsumerMsg { +pub enum ConsumerPacket { /// This is sent when a new validator registers and is available to receive - /// delegations. - /// This packet is sent right after the channel is opened to sync initial state - AddValidators(Vec), + /// delegations. This is also sent when a validator changes pubkey. + /// One such packet is sent right after the channel is opened to sync initial state + AddValidators(Vec), /// This is sent when a validator is tombstoned. Not just leaving the active state, /// but when it is no longer a valid target to delegate to. /// It contains a list of `valoper_address` to be removed RemoveValidators(Vec), - /// This is sent a validator changes the pubkey - UpdatePubkey { - /// This is the validator address that is changing the pubkey - valoper_address: String, - /// This is the block height (on the consumer) at which the pubkey was changed - height: u64, - /// This is the pubkey signing all blocks after `height` - new_pubkey: String, - /// This is the pubkey signing all blocks up to and including `height` - old_pubkey: String, - }, } #[cw_serde] -pub struct Validator { - /// This is the validator address used for delegations and rewards - pub valoper_address: String, +pub struct AddValidator { + /// This is the validator operator (valoper) address used for delegations and rewards + pub valoper: String, // TODO: is there a better type for this? what encoding is used /// This is the *Tendermint* public key, used for signing blocks. /// This is needed to detect slashing conditions pub pub_key: String, + + /// This is the first height the validator was active. + /// It is used to detect slashing conditions, eg which header heights are punishable. + pub start_height: u64, + + /// This is the timestamp of the first block the validator was active. + /// It may be used for unbonding_period issues, maybe just for informational purposes. + /// Stored as unix seconds. + pub start_time: u64, } -/// Ack sent for ConsumerMsg::AddValidators +/// Ack sent for ConsumerPacket::AddValidators #[cw_serde] pub struct AddValidatorsAck {} -/// Ack sent for ConsumerMsg::RemoveValidators +/// Ack sent for ConsumerPacket::RemoveValidators #[cw_serde] pub struct RemoveValidatorsAck {} -/// Ack sent for ConsumerMsg::UpdatePubkey +/// This is a generic ICS acknowledgement format. +/// Proto defined here: https://github.com/cosmos/cosmos-sdk/blob/v0.42.0/proto/ibc/core/channel/v1/channel.proto#L141-L147 +/// This is compatible with the JSON serialization. +/// Wasmd uses this same wrapper for unhandled errors. #[cw_serde] -pub struct UpdatePubkeyAck {} +pub enum AckWrapper { + Result(Binary), + Error(String), +} + +// create a serialized success message +pub fn ack_success(data: &T) -> StdResult { + let res = AckWrapper::Result(to_binary(data)?); + to_binary(&res) +} + +// create a serialized error message +pub fn ack_fail(err: E) -> StdResult { + let res = AckWrapper::Error(err.to_string()); + to_binary(&res) +}