diff --git a/gateway/src/checkpoint.rs b/gateway/src/checkpoint.rs index 4e94371..2671c48 100644 --- a/gateway/src/checkpoint.rs +++ b/gateway/src/checkpoint.rs @@ -6,10 +6,11 @@ use fvm_ipld_encoding::{serde_bytes, to_vec}; use fvm_shared::clock::ChainEpoch; use fvm_shared::econ::TokenAmount; use ipc_sdk::subnet_id::SubnetID; +use num_traits::Zero; use primitives::{TCid, TLink}; use serde_tuple::{Deserialize_tuple, Serialize_tuple}; -use crate::{CrossMsg, CrossMsgs}; +use crate::{ensure_message_sorted, CrossMsg, CrossMsgs}; #[derive(PartialEq, Eq, Clone, Debug, Serialize_tuple, Deserialize_tuple)] pub struct Checkpoint { @@ -62,19 +63,44 @@ impl Checkpoint { &self.data.prev_check } - /// return cross_msg included in the checkpoint. - pub fn cross_msgs(&self) -> &Vec { - self.data.cross_msgs.as_ref() + /// Take the cross messages out of the checkpoint. This will empty the `self.data.cross_msgs` + /// and replace with None. + pub fn take_cross_msgs(&mut self) -> Option> { + self.data.cross_msgs.cross_msgs.take() } - /// set cross_msg included in the checkpoint. - pub fn set_cross_msgs(&mut self, cm: Vec) { - self.data.cross_msgs = cm + pub fn ensure_cross_msgs_sorted(&self) -> anyhow::Result<()> { + match self.data.cross_msgs.cross_msgs.as_ref() { + None => Ok(()), + Some(v) => ensure_message_sorted(v), + } } - /// return cross_msg included in the checkpoint as mutable reference - pub fn cross_msgs_mut(&mut self) -> &mut Vec { - self.data.cross_msgs.as_mut() + /// Get the sum of values in cross messages + pub fn total_value(&self) -> TokenAmount { + match &self.data.cross_msgs.cross_msgs { + None => TokenAmount::zero(), + Some(cross_msgs) => { + let mut value = TokenAmount::zero(); + cross_msgs.iter().for_each(|cross_msg| { + value += &cross_msg.msg.value; + }); + value + } + } + } + + /// Get the total fee of the cross messages + pub fn total_fee(&self) -> &TokenAmount { + &self.data.cross_msgs.fee + } + + pub fn push_cross_msgs(&mut self, cross_msg: CrossMsg, fee: &TokenAmount) { + self.data.cross_msgs.fee += fee; + match self.data.cross_msgs.cross_msgs.as_mut() { + None => self.data.cross_msgs.cross_msgs = Some(vec![cross_msg]), + Some(v) => v.push(cross_msg), + }; } /// Add the cid of a checkpoint from a child subnet for further propagation @@ -121,8 +147,15 @@ pub struct CheckData { pub epoch: ChainEpoch, pub prev_check: TCid>, pub children: Vec, - pub cross_msgs: Vec, + pub cross_msgs: BatchCrossMsgs, +} + +#[derive(Default, PartialEq, Eq, Clone, Debug, Serialize_tuple, Deserialize_tuple)] +pub struct BatchCrossMsgs { + pub cross_msgs: Option>, + pub fee: TokenAmount, } + impl CheckData { pub fn new(id: SubnetID, epoch: ChainEpoch) -> Self { Self { @@ -131,7 +164,7 @@ impl CheckData { epoch, prev_check: TCid::default(), children: Vec::new(), - cross_msgs: vec![], + cross_msgs: BatchCrossMsgs::default(), } } } diff --git a/gateway/src/cron.rs b/gateway/src/cron.rs index 6408de2..d971482 100644 --- a/gateway/src/cron.rs +++ b/gateway/src/cron.rs @@ -1,4 +1,4 @@ -use crate::StorableMsg; +use crate::{ensure_message_sorted, StorableMsg}; use anyhow::anyhow; use cid::multihash::Code; use cid::multihash::MultihashDigest; @@ -13,7 +13,6 @@ use ipc_sdk::ValidatorSet; use lazy_static::lazy_static; use num_traits::Zero; use primitives::{TCid, THamt}; -use std::cmp::Ordering; use std::ops::Mul; pub type HashOutput = Vec; @@ -75,17 +74,7 @@ impl CronCheckpoint { /// /// Actor will not perform sorting to save gas. Client should do it, actor just check. pub fn hash(&self) -> anyhow::Result { - // check top down msgs - for i in 1..self.top_down_msgs.len() { - match self.top_down_msgs[i - 1] - .nonce - .cmp(&self.top_down_msgs[i].nonce) - { - Ordering::Less => {} - Ordering::Equal => return Err(anyhow!("top down messages not distinct")), - Ordering::Greater => return Err(anyhow!("top down messages not sorted")), - }; - } + ensure_message_sorted(&self.top_down_msgs)?; let mh_code = Code::Blake2b256; // TODO: to avoid serialization again, maybe we should perform deserialization in the actor diff --git a/gateway/src/cross.rs b/gateway/src/cross.rs index 6b639fb..ea0c058 100644 --- a/gateway/src/cross.rs +++ b/gateway/src/cross.rs @@ -1,6 +1,6 @@ -use crate::ApplyMsgParams; use crate::State; use crate::SUBNET_ACTOR_REWARD_METHOD; +use crate::{ApplyMsgParams, ExecutableMessage}; use anyhow::anyhow; use fil_actors_runtime::runtime::Runtime; use fil_actors_runtime::ActorError; @@ -33,12 +33,24 @@ pub struct StorableMsg { pub nonce: u64, } +impl ExecutableMessage for StorableMsg { + fn nonce(&self) -> u64 { + self.nonce + } +} + #[derive(PartialEq, Eq, Clone, Debug, Serialize_tuple, Deserialize_tuple)] pub struct CrossMsg { pub msg: StorableMsg, pub wrapped: bool, } +impl ExecutableMessage for CrossMsg { + fn nonce(&self) -> u64 { + self.msg.nonce() + } +} + #[derive(PartialEq, Eq)] pub enum IPCMsgType { BottomUp, @@ -69,7 +81,6 @@ impl StorableMsg { sub_id: &SubnetID, sig_addr: &Address, value: TokenAmount, - nonce: u64, ) -> anyhow::Result { let to = IPCAddress::new( &match sub_id.parent() { @@ -85,7 +96,7 @@ impl StorableMsg { method: METHOD_SEND, params: RawBytes::default(), value, - nonce, + nonce: 0, }) } diff --git a/gateway/src/lib.rs b/gateway/src/lib.rs index ebe28b5..f9f1d00 100644 --- a/gateway/src/lib.rs +++ b/gateway/src/lib.rs @@ -3,7 +3,6 @@ extern crate core; -use std::ops::Mul; pub use self::checkpoint::{Checkpoint, CrossMsgMeta}; pub use self::cross::{is_bottomup, CrossMsg, CrossMsgs, IPCMsgType, StorableMsg}; pub use self::state::*; @@ -68,7 +67,6 @@ pub enum Method { Fund = frc42_dispatch::method_hash!("Fund"), Release = frc42_dispatch::method_hash!("Release"), SendCross = frc42_dispatch::method_hash!("SendCross"), - ApplyMessage = frc42_dispatch::method_hash!("ApplyMessage"), Propagate = frc42_dispatch::method_hash!("Propagate"), WhiteListPropagator = frc42_dispatch::method_hash!("WhiteListPropagator"), SubmitCron = frc42_dispatch::method_hash!("SubmitCron"), @@ -279,11 +277,16 @@ impl Actor { /// CommitChildCheck propagates the commitment of a checkpoint from a child subnet, /// process the cross-messages directed to the subnet. - fn commit_child_check(rt: &mut impl Runtime, params: Checkpoint) -> Result<(), ActorError> { + fn commit_child_check(rt: &mut impl Runtime, mut commit: Checkpoint) -> Result<(), ActorError> { + // This must be called by a subnet actor, once we have a way to identify subnet actor, + // we should update here. rt.validate_immediate_caller_accept_any()?; + commit + .ensure_cross_msgs_sorted() + .map_err(|_| actor_error!(illegal_argument, "cross messages not ordered by nonce"))?; + let subnet_addr = rt.message().caller(); - let commit = params; let subnet_actor = commit.source().subnet_actor(); // check if the checkpoint belongs to the subnet @@ -294,7 +297,7 @@ impl Actor { )); } - let fee = rt.transaction(|st: &mut State, rt| { + let (fee, cross_msgs) = rt.transaction(|st: &mut State, rt| { let shid = SubnetID::new_from_parent(&st.network_name, subnet_addr); let sub = st.get_subnet(rt.store(), &shid).map_err(|e| { e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "failed to load subnet") @@ -333,15 +336,14 @@ impl Actor { if commit.prev_check().cid() != prev_checkpoint.cid() { return Err(actor_error!( illegal_argument, - "previous checkpoint not consistente with previous one" + "previous checkpoint not consistent with previous one" )); } } - // commit cross-message in checkpoint to either execute them or - // queue them for propagation if there are cross-msgs availble. - let mut value = TokenAmount::zero(); - commit.cross_msgs().iter().for_each(|m| value += &m.msg.value); + // commit cross-message in checkpoint to execute them. + let fee = commit.total_fee().clone(); + let value = commit.total_value() + &fee; // release circulating supply sub.release_supply(&value).map_err(|e| { @@ -351,8 +353,6 @@ impl Actor { ) })?; - let fee = CROSS_MSG_FEE.clone().mul(commit.cross_msgs().len()); - // append new checkpoint to the list of childs ch.add_child_check(&commit).map_err(|e| { e.downcast_default( @@ -366,24 +366,30 @@ impl Actor { e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "error flushing checkpoint") })?; + let cross_msgs = commit.take_cross_msgs(); + // update prev_check for child sub.prev_checkpoint = Some(commit); // flush subnet st.flush_subnet(rt.store(), &sub).map_err(|e| { e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "error flushing subnet") })?; - Ok(fee) - } - None => { - Err(actor_error!( - illegal_argument, - "subnet with id {} not registered", - shid - )) + Ok((fee, cross_msgs)) } + None => Err(actor_error!( + illegal_argument, + "subnet with id {} not registered", + shid + )), } })?; + if let Some(msgs) = cross_msgs { + for cross_msg in msgs { + Self::apply_msg_inner(rt, cross_msg)?; + } + } + // distribute rewards distribute_crossmsg_fee(rt, &subnet_actor, fee) } @@ -448,9 +454,6 @@ impl Actor { // funds can only be moved between subnets by signable addresses rt.validate_immediate_caller_type(CALLER_TYPES_SIGNABLE.iter())?; - // FIXME: Only supporting cross-messages initiated by signable addresses for - // now. Consider supporting also send-cross messages initiated by actors. - let mut value = rt.message().value_received(); if value <= TokenAmount::zero() { return Err(actor_error!( @@ -468,23 +471,18 @@ impl Actor { // Create release message let r_msg = CrossMsg { - msg: StorableMsg::new_release_msg( - &st.network_name, - &sig_addr, - value.clone(), - st.nonce, - ) - .map_err(|e| { - e.downcast_default( - ExitCode::USR_ILLEGAL_STATE, - "error creating release cross-message", - ) - })?, + msg: StorableMsg::new_release_msg(&st.network_name, &sig_addr, value.clone()) + .map_err(|e| { + e.downcast_default( + ExitCode::USR_ILLEGAL_STATE, + "error creating release cross-message", + ) + })?, wrapped: false, }; // Commit bottom-up message. - st.commit_bottomup_msg(rt.store(), &r_msg, rt.curr_epoch()) + st.commit_bottomup_msg(rt.store(), &r_msg, rt.curr_epoch(), fee) .map_err(|e| { e.downcast_default( ExitCode::USR_ILLEGAL_STATE, @@ -585,123 +583,6 @@ impl Actor { Ok(()) } - /// ApplyMessage triggers the execution of a cross-subnet message validated through the consensus. - /// - /// This function can only be triggered using `ApplyImplicitMessage`, and the source needs to - /// be the SystemActor. Cross messages are applied similarly to how rewards are applied once - /// a block has been validated. This function: - /// - Determines the type of cross-message. - /// - Performs the corresponding state changes. - /// - And updated the latest nonce applied for future checks. - fn apply_msg(rt: &mut impl Runtime, params: ApplyMsgParams) -> Result { - rt.validate_immediate_caller_is([&SYSTEM_ACTOR_ADDR as &Address])?; - let ApplyMsgParams { cross_msg } = params; - Self::apply_msg_inner(rt, cross_msg) - } - - fn apply_msg_inner(rt: &mut impl Runtime, cross_msg: CrossMsg) -> Result { - let rto = match cross_msg.msg.to.raw_addr() { - Ok(to) => to, - Err(_) => { - return Err(actor_error!( - illegal_argument, - "error getting raw address from msg" - )); - } - }; - let sto = match cross_msg.msg.to.subnet() { - Ok(to) => to, - Err(_) => { - return Err(actor_error!( - illegal_argument, - "error getting subnet from msg" - )); - } - }; - - let st: State = rt.state()?; - - log::debug!("sto: {:?}, network: {:?}", sto, st.network_name); - - match cross_msg.msg.apply_type(&st.network_name) { - Ok(IPCMsgType::BottomUp) => { - // if directed to current network, execute message. - if sto == st.network_name { - rt.transaction(|st: &mut State, _| { - st.bottomup_state_transition(&cross_msg.msg).map_err(|e| { - e.downcast_default( - ExitCode::USR_ILLEGAL_STATE, - "failed applying bottomup message", - ) - })?; - Ok(()) - })?; - return cross_msg.send(rt, &rto); - } - } - Ok(IPCMsgType::TopDown) => { - // Mint funds for the gateway, as any topdown message - // including tokens traversing the subnet will use - // some balance from the gateway to increase the circ_supply. - // check if the gateway has enough funds to mint new FIL, - // if not fail right-away and do not allow the execution of - // the message. - // TODO: It may be a good idea in the future to decouple the - // balance provisioned in the gateway to mint new circulating supply - // in an independent actor. Minting tokens would require a call to the new actor actor to unlock - // additional circulating supply. This prevents an attacker from being able token - // vulnerabilities in the gateway - // from draining the whole balance. - if rt.current_balance() < cross_msg.msg.value { - return Err(actor_error!( - illegal_state, - "not enough balance to mint new tokens as part of the cross-message" - )); - } - - if sto == st.network_name { - if st.applied_topdown_nonce != cross_msg.msg.nonce { - return Err(actor_error!( - illegal_state, - "the top-down message being applied doesn't hold the subsequent nonce" - )); - } - - rt.transaction(|st: &mut State, _| { - st.applied_topdown_nonce += 1; - Ok(()) - })?; - - // We can return the send result - return cross_msg.send(rt, &rto); - } - } - _ => { - return Err(actor_error!( - illegal_argument, - "cross-message to apply dosen't have the right type" - )) - } - }; - - let cid = rt.transaction(|st: &mut State, rt| { - let owner = cross_msg - .msg - .from - .raw_addr() - .map_err(|_| actor_error!(illegal_argument, "invalid address"))?; - let r = st - .insert_postbox(rt.store(), Some(vec![owner]), cross_msg) - .map_err(|e| { - e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "error save topdown messages") - })?; - Ok(r) - })?; - - // it is safe to just unwrap. If `transaction` fails, cid is None and wont reach here. - Ok(RawBytes::new(cid.to_bytes())) - } - /// Whitelist a series of addresses as propagator of a cross net message. /// This is basically adding this list of addresses to the `PostBoxItem::owners`. /// Only existing owners can perform this operation. @@ -905,7 +786,7 @@ impl Actor { if cross_msg.msg.value > TokenAmount::zero() { do_burn = true; } - st.commit_bottomup_msg(rt.store(), cross_msg, rt.curr_epoch()) + st.commit_bottomup_msg(rt.store(), cross_msg, rt.curr_epoch(), &fee) }; r.map_err(|e| { @@ -957,6 +838,112 @@ impl Actor { /// Contains private method invocation impl Actor { + fn apply_msg_inner(rt: &mut impl Runtime, cross_msg: CrossMsg) -> Result { + let rto = match cross_msg.msg.to.raw_addr() { + Ok(to) => to, + Err(_) => { + return Err(actor_error!( + illegal_argument, + "error getting raw address from msg" + )); + } + }; + let sto = match cross_msg.msg.to.subnet() { + Ok(to) => to, + Err(_) => { + return Err(actor_error!( + illegal_argument, + "error getting subnet from msg" + )); + } + }; + + let st: State = rt.state()?; + + log::debug!("sto: {:?}, network: {:?}", sto, st.network_name); + + match cross_msg.msg.apply_type(&st.network_name) { + Ok(IPCMsgType::BottomUp) => { + // if directed to current network, execute message. + if sto == st.network_name { + rt.transaction(|st: &mut State, _| { + if st.applied_bottomup_nonce != cross_msg.msg.nonce { + return Err(actor_error!( + illegal_state, + "the bottom-up message being applied doesn't hold the subsequent nonce" + )); + } + + st.applied_bottomup_nonce += 1; + + Ok(()) + })?; + return cross_msg.send(rt, &rto); + } + } + Ok(IPCMsgType::TopDown) => { + // Mint funds for the gateway, as any topdown message + // including tokens traversing the subnet will use + // some balance from the gateway to increase the circ_supply. + // check if the gateway has enough funds to mint new FIL, + // if not fail right-away and do not allow the execution of + // the message. + // TODO: It may be a good idea in the future to decouple the + // balance provisioned in the gateway to mint new circulating supply + // in an independent actor. Minting tokens would require a call to the new actor actor to unlock + // additional circulating supply. This prevents an attacker from being able token + // vulnerabilities in the gateway + // from draining the whole balance. + if rt.current_balance() < cross_msg.msg.value { + return Err(actor_error!( + illegal_state, + "not enough balance to mint new tokens as part of the cross-message" + )); + } + + if sto == st.network_name { + rt.transaction(|st: &mut State, _| { + if st.applied_topdown_nonce != cross_msg.msg.nonce { + return Err(actor_error!( + illegal_state, + "the top-down message being applied doesn't hold the subsequent nonce" + )); + } + + st.applied_topdown_nonce += 1; + Ok(()) + })?; + + // We can return the send result + return cross_msg.send(rt, &rto); + } + } + _ => { + return Err(actor_error!( + illegal_argument, + "cross-message to apply dosen't have the right type" + )) + } + }; + + let cid = rt.transaction(|st: &mut State, rt| { + let owner = cross_msg + .msg + .from + .raw_addr() + .map_err(|_| actor_error!(illegal_argument, "invalid address"))?; + let r = st + .insert_postbox(rt.store(), Some(vec![owner]), cross_msg) + .map_err(|e| { + e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "error save topdown messages") + })?; + Ok(r) + })?; + + // it is safe to just unwrap. If `transaction` fails, cid is None and wont reach here. + Ok(RawBytes::new(cid.to_bytes())) + } + fn handle_cron_submission( store: &BS, st: &mut State, @@ -1109,7 +1096,6 @@ impl ActorCode for Actor { Fund => fund, Release => release, SendCross => send_cross, - ApplyMessage => apply_msg, Propagate => propagate, WhiteListPropagator => whitelist_propagator, SubmitCron => submit_cron, diff --git a/gateway/src/state.rs b/gateway/src/state.rs index 4728373..9a16f69 100644 --- a/gateway/src/state.rs +++ b/gateway/src/state.rs @@ -12,7 +12,7 @@ use fvm_shared::econ::TokenAmount; use fvm_shared::error::ExitCode; use lazy_static::lazy_static; use num_traits::Zero; -use primitives::{TAmt, TCid, THamt, TLink}; +use primitives::{TCid, THamt}; use serde_tuple::{Deserialize_tuple, Serialize_tuple}; use std::collections::BTreeSet; use std::str::FromStr; @@ -40,13 +40,10 @@ pub struct State { pub subnets: TCid>, pub check_period: ChainEpoch, pub checkpoints: TCid>, - pub check_msg_registry: TCid>, CrossMsgs>>, /// `postbox` keeps track for an EOA of all the cross-net messages triggered by /// an actor that need to be propagated further through the hierarchy. pub postbox: PostBox, - pub nonce: u64, pub bottomup_nonce: u64, - pub bottomup_msg_meta: TCid>, pub applied_bottomup_nonce: u64, pub applied_topdown_nonce: u64, /// The epoch that the subnet actor is deployed @@ -80,14 +77,11 @@ impl State { false => DEFAULT_CHECKPOINT_PERIOD, }, checkpoints: TCid::new_hamt(store)?, - check_msg_registry: TCid::new_hamt(store)?, postbox: TCid::new_hamt(store)?, - nonce: Default::default(), bottomup_nonce: Default::default(), - bottomup_msg_meta: TCid::new_amt(store)?, // This way we ensure that the first message to execute has nonce= 0, if not it would expect 1 and fail for the first nonce // We first increase to the subsequent and then execute for bottom-up messages - applied_bottomup_nonce: MAX_NONCE, + applied_bottomup_nonce: Default::default(), applied_topdown_nonce: Default::default(), genesis_epoch: params.genesis_epoch, cron_period: params.cron_period, @@ -130,7 +124,7 @@ impl State { top_down_msgs: TCid::new_amt(rt.store())?, circ_supply: TokenAmount::zero(), status: Status::Active, - nonce: 0, + topdown_nonce: 0, prev_checkpoint: None, }; set_subnet(subnets, id, subnet)?; @@ -206,10 +200,17 @@ impl State { store: &BS, cross_msg: &CrossMsg, curr_epoch: ChainEpoch, + fee: &TokenAmount, ) -> anyhow::Result<()> { let mut ch = self.get_window_checkpoint(store, curr_epoch)?; - ch.cross_msgs_mut().push(cross_msg.clone()); + let mut cross_msg = cross_msg.clone(); + cross_msg.msg.nonce = self.bottomup_nonce; + + ch.push_cross_msgs(cross_msg, fee); + + // increment nonce + self.bottomup_nonce += 1; // flush checkpoint self.flush_checkpoint(store, &ch).map_err(|e| { @@ -241,9 +242,9 @@ impl State { })?; match sub { Some(mut sub) => { - cross_msg.msg.nonce = sub.nonce; + cross_msg.msg.nonce = sub.topdown_nonce; sub.store_topdown_msg(store, cross_msg)?; - sub.nonce += 1; + sub.topdown_nonce += 1; sub.circ_supply += &cross_msg.msg.value; self.flush_subnet(store, &sub)?; } @@ -262,35 +263,10 @@ impl State { store: &BS, msg: &CrossMsg, curr_epoch: ChainEpoch, + fee: &TokenAmount, ) -> anyhow::Result<()> { // store bottom-up msg and fee in checkpoint for propagation - self.store_msg_in_checkpoint(store, msg, curr_epoch)?; - // increment nonce - self.nonce += 1; - - Ok(()) - } - - pub fn bottomup_state_transition(&mut self, msg: &StorableMsg) -> anyhow::Result<()> { - // Bottom-up messages include the nonce of their message meta. Several messages - // will include the same nonce. They need to be applied in order of nonce. - - // As soon as we see a message with the next msgMeta nonce, we increment the nonce - // and start accepting the one for the next nonce. - if self.applied_bottomup_nonce == u64::MAX && msg.nonce == 0 { - self.applied_bottomup_nonce = 0; - } else if self.applied_bottomup_nonce.wrapping_add(1) == msg.nonce { - // wrapping add is used to prevent overflow. - self.applied_bottomup_nonce = self.applied_bottomup_nonce.wrapping_add(1); - }; - - if self.applied_bottomup_nonce != msg.nonce { - return Err(anyhow!( - "the bottom-up message being applied doesn't hold the subsequent nonce: nonce={} applied={}", - msg.nonce, - self.applied_bottomup_nonce, - )); - } + self.store_msg_in_checkpoint(store, msg, curr_epoch, fee)?; Ok(()) } diff --git a/gateway/src/subnet.rs b/gateway/src/subnet.rs index 13b7331..bef22b1 100644 --- a/gateway/src/subnet.rs +++ b/gateway/src/subnet.rs @@ -26,7 +26,7 @@ pub struct Subnet { pub id: SubnetID, pub stake: TokenAmount, pub top_down_msgs: TCid>, - pub nonce: u64, + pub topdown_nonce: u64, pub circ_supply: TokenAmount, pub status: Status, pub prev_checkpoint: Option, diff --git a/gateway/src/types.rs b/gateway/src/types.rs index 1389fe6..aab7fe2 100644 --- a/gateway/src/types.rs +++ b/gateway/src/types.rs @@ -1,3 +1,4 @@ +use anyhow::anyhow; use cid::multihash::Code; use cid::{multihash, Cid}; use fil_actors_runtime::{cbor, ActorError, Array}; @@ -9,6 +10,7 @@ use fvm_shared::econ::TokenAmount; use ipc_sdk::subnet_id::SubnetID; use multihash::MultihashDigest; use primitives::CodeType; +use std::cmp::Ordering; use crate::checkpoint::{Checkpoint, CrossMsgMeta}; use crate::cross::CrossMsg; @@ -18,7 +20,6 @@ pub const MANIFEST_ID: &str = "ipc_gateway"; pub const CROSSMSG_AMT_BITWIDTH: u32 = 3; pub const DEFAULT_CHECKPOINT_PERIOD: ChainEpoch = 10; -pub const MAX_NONCE: u64 = u64::MAX; pub const MIN_COLLATERAL_AMOUNT: u64 = 10_u64.pow(18); pub const SUBNET_ACTOR_REWARD_METHOD: u64 = frc42_dispatch::method_hash!("Reward"); @@ -26,6 +27,12 @@ pub const SUBNET_ACTOR_REWARD_METHOD: u64 = frc42_dispatch::method_hash!("Reward pub type CrossMsgMetaArray<'bs, BS> = Array<'bs, CrossMsgMeta, BS>; pub type CrossMsgArray<'bs, BS> = Array<'bs, CrossMsg, BS>; +/// The executable message trait +pub trait ExecutableMessage { + /// Get the nonce of the message + fn nonce(&self) -> u64; +} + #[derive(Serialize_tuple, Deserialize_tuple)] pub struct ConstructorParams { pub network_name: String, @@ -101,6 +108,18 @@ impl PostBoxItem { } } +pub(crate) fn ensure_message_sorted(messages: &[E]) -> anyhow::Result<()> { + // check top down msgs + for i in 1..messages.len() { + match messages[i - 1].nonce().cmp(&messages[i].nonce()) { + Ordering::Less => {} + Ordering::Equal => return Err(anyhow!("top down messages not distinct")), + Ordering::Greater => return Err(anyhow!("top down messages not sorted")), + }; + } + Ok(()) +} + #[cfg(test)] mod tests { use crate::ConstructorParams; diff --git a/gateway/tests/gateway_test.rs b/gateway/tests/gateway_test.rs index 52fca28..9c247eb 100644 --- a/gateway/tests/gateway_test.rs +++ b/gateway/tests/gateway_test.rs @@ -10,10 +10,11 @@ use fvm_shared::clock::ChainEpoch; use fvm_shared::econ::TokenAmount; use fvm_shared::error::ExitCode; use fvm_shared::METHOD_SEND; +use ipc_gateway::checkpoint::BatchCrossMsgs; use ipc_gateway::Status::{Active, Inactive}; use ipc_gateway::{ - get_topdown_msg, Checkpoint, CronCheckpoint, CronSubmission, CrossMsg, IPCAddress, State, - StorableMsg, CROSS_MSG_FEE, DEFAULT_CHECKPOINT_PERIOD, SUBNET_ACTOR_REWARD_METHOD, + get_topdown_msg, Checkpoint, CronCheckpoint, CronSubmission, CrossMsg, IPCAddress, PostBoxItem, + State, StorableMsg, CROSS_MSG_FEE, DEFAULT_CHECKPOINT_PERIOD, SUBNET_ACTOR_REWARD_METHOD, }; use ipc_sdk::subnet_id::SubnetID; use ipc_sdk::{Validator, ValidatorSet}; @@ -335,18 +336,30 @@ fn checkpoint_crossmsgs() { assert_eq!(subnet.status, Active); h.check_state(); + // found some to the subnet + let funder = Address::new_id(1001); + let amount = TokenAmount::from_atto(10_u64.pow(18)); + h.fund( + &mut rt, + &funder, + &shid, + ExitCode::OK, + amount.clone(), + 1, + &amount, + ) + .unwrap(); + // Commit first checkpoint for first window in first subnet let epoch: ChainEpoch = 10; rt.set_epoch(epoch); let mut ch = Checkpoint::new(shid.clone(), epoch + 9); - // and include some fees in msgmeta. + // and include some fees. let fee = TokenAmount::from_atto(5); - set_msg_meta( - &mut ch, - "rand1".as_bytes().to_vec(), - TokenAmount::zero(), - fee.clone(), - ); + ch.data.cross_msgs = BatchCrossMsgs { + cross_msgs: None, + fee: fee.clone(), + }; rt.expect_send( shid.subnet_actor(), @@ -457,27 +470,10 @@ fn test_release() { // Release funds let r_amount = TokenAmount::from_atto(5_u64.pow(18)); rt.set_balance(2 * r_amount.clone()); - let prev_cid = h - .release( - &mut rt, - &releaser, - ExitCode::OK, - r_amount.clone(), - 0, - &Cid::default(), - CROSS_MSG_FEE.clone(), - ) + h.release(&mut rt, &releaser, ExitCode::OK, r_amount.clone(), 0) + .unwrap(); + h.release(&mut rt, &releaser, ExitCode::OK, r_amount, 1) .unwrap(); - h.release( - &mut rt, - &releaser, - ExitCode::OK, - r_amount, - 1, - &prev_cid, - 2 * CROSS_MSG_FEE.clone(), - ) - .unwrap(); } #[test] @@ -585,11 +581,29 @@ fn test_send_cross() { /// This test covers the case where a bottom up cross_msg's target subnet is the SAME as that of /// the gateway. It should directly commit the message and will not save in postbox. #[test] -fn test_apply_msg_bu_target_subnet() { +fn test_commit_child_check_bu_target_subnet() { // ============== Register subnet ============== let shid = SubnetID::new_from_parent(&ROOTNET_ID, *SUBNET_ONE); let (h, mut rt) = setup(ROOTNET_ID.clone()); + h.register( + &mut rt, + &SUBNET_ONE, + &TokenAmount::from_atto(10_u64.pow(18)), + ExitCode::OK, + ) + .unwrap(); + h.fund( + &mut rt, + &Address::new_id(1001), + &shid, + ExitCode::OK, + TokenAmount::from_atto(10_u64.pow(18)), + 1, + &TokenAmount::from_atto(10_u64.pow(18)), + ) + .unwrap(); + let from = Address::new_bls(&[3; fvm_shared::address::BLS_PUB_LEN]).unwrap(); let to = Address::new_bls(&[4; fvm_shared::address::BLS_PUB_LEN]).unwrap(); @@ -604,7 +618,7 @@ fn test_apply_msg_bu_target_subnet() { let msg_nonce = 0; // Only system code is allowed to this method - let params = StorableMsg { + let msg = StorableMsg { to: tt.clone(), from: ff.clone(), method: METHOD_SEND, @@ -612,51 +626,82 @@ fn test_apply_msg_bu_target_subnet() { params: RawBytes::default(), nonce: msg_nonce, }; - let sto = tt.raw_addr().unwrap(); - - let cid = h - .apply_cross_execute_only( - &mut rt, - value.clone(), - params, - Some(Box::new(move |rt| { - rt.expect_send( - sto.clone(), - METHOD_SEND, - None, - value.clone(), - None, - ExitCode::OK, - ); - })), - ) + + let epoch: ChainEpoch = 10; + rt.set_epoch(epoch); + let mut ch = Checkpoint::new(shid.clone(), epoch + 9); + // and include some fees. + let fee = TokenAmount::from_atto(5); + ch.data.cross_msgs = BatchCrossMsgs { + cross_msgs: Some(vec![CrossMsg { + msg: msg.clone(), + wrapped: false, + }]), + fee: fee.clone(), + }; + + // execute bottom up + rt.expect_send( + msg.to.raw_addr().unwrap(), + msg.method, + None, + msg.value, + None, + ExitCode::OK, + ); + // distribute fee + rt.expect_send( + shid.subnet_actor(), + SUBNET_ACTOR_REWARD_METHOD, + None, + fee, + None, + ExitCode::OK, + ); + h.commit_child_check(&mut rt, &shid, &ch, ExitCode::OK) .unwrap(); - assert_eq!(cid.is_none(), true); } /// This test covers the case where a bottom up cross_msg's target subnet is NOT the same as that of /// the gateway. It will save it in the postbox. #[test] -fn test_apply_msg_bu_not_target_subnet() { +fn test_commit_child_check_bu_not_target_subnet() { // ============== Register subnet ============== - let shid = SubnetID::new_from_parent(&ROOTNET_ID, *SUBNET_ONE); - let (h, mut rt) = setup(shid.clone()); + let parent = SubnetID::new_from_parent(&ROOTNET_ID, *SUBNET_ONE); + let shid = SubnetID::new_from_parent(&parent, *SUBNET_TWO); + let (h, mut rt) = setup(parent); + + h.register( + &mut rt, + &shid.subnet_actor(), + &TokenAmount::from_atto(10_u64.pow(18)), + ExitCode::OK, + ) + .unwrap(); + h.fund( + &mut rt, + &Address::new_id(1001), + &shid, + ExitCode::OK, + TokenAmount::from_atto(10_u64.pow(18)), + 1, + &TokenAmount::from_atto(10_u64.pow(18)), + ) + .unwrap(); let from = Address::new_bls(&[3; fvm_shared::address::BLS_PUB_LEN]).unwrap(); let to = Address::new_bls(&[4; fvm_shared::address::BLS_PUB_LEN]).unwrap(); - let sub = shid.clone(); - // ================ Setup =============== let value = TokenAmount::from_atto(10_u64.pow(17)); // ================= Bottom-Up =============== - let ff = IPCAddress::new(&sub, &to).unwrap(); + let ff = IPCAddress::new(&shid.clone(), &to).unwrap(); let tt = IPCAddress::new(&ROOTNET_ID, &from).unwrap(); let msg_nonce = 0; // Only system code is allowed to this method - let params = StorableMsg { + let msg = StorableMsg { to: tt.clone(), from: ff.clone(), method: METHOD_SEND, @@ -664,43 +709,72 @@ fn test_apply_msg_bu_not_target_subnet() { params: RawBytes::default(), nonce: msg_nonce, }; - let cid = h - .apply_cross_execute_only(&mut rt, value.clone(), params.clone(), None) - .unwrap() + + let epoch: ChainEpoch = 10; + rt.set_epoch(epoch); + let mut ch = Checkpoint::new(shid.clone(), epoch + 9); + // and include some fees. + let fee = TokenAmount::from_atto(5); + ch.data.cross_msgs = BatchCrossMsgs { + cross_msgs: Some(vec![CrossMsg { + msg: msg.clone(), + wrapped: false, + }]), + fee: fee.clone(), + }; + + // distribute fee + rt.expect_send( + shid.subnet_actor(), + SUBNET_ACTOR_REWARD_METHOD, + None, + fee, + None, + ExitCode::OK, + ); + h.commit_child_check(&mut rt, &shid, &ch, ExitCode::OK) .unwrap(); // Part 1: test the message is stored in postbox let st: State = rt.get_state(); assert_ne!(tt.subnet().unwrap(), st.network_name); - // Check 1: `tt` is in `sub1`, which is not in that of `runtime` of gateway, will store in postbox - let item = st.load_from_postbox(rt.store(), cid.clone()).unwrap(); - assert_eq!(item.owners, Some(vec![ff.clone().raw_addr().unwrap()])); - let msg = item.cross_msg.msg; - assert_eq!(msg.to, tt); - // the nonce should not have changed at all - assert_eq!(msg.nonce, msg_nonce); - assert_eq!(msg.value, value); + // Check 1: `tt` is in `parent`, which is not in that of `runtime` of gateway, will store in postbox + let postbox = st.postbox.load(rt.store()).unwrap(); + let mut cid = None; + postbox + .for_each(|k, v| { + let item = PostBoxItem::deserialize(v.clone()).unwrap(); + assert_eq!(item.owners, Some(vec![ff.clone().raw_addr().unwrap()])); + let msg = item.cross_msg.msg; + assert_eq!(msg.to, tt); + // the nonce should not have changed at all + assert_eq!(msg.nonce, msg_nonce); + assert_eq!(msg.value, value); + + cid = Some(Cid::try_from(k.clone().to_vec()).unwrap()); + Ok(()) + }) + .unwrap(); // Part 2: Now we propagate from postbox // get the original subnet nonce first let caller = ff.clone().raw_addr().unwrap(); - let old_state: State = rt.get_state(); // propagating a bottom-up message triggers the // funds included in the message to be burnt. rt.expect_send( BURNT_FUNDS_ACTOR_ADDR, METHOD_SEND, None, - params.clone().value, + msg.clone().value, None, ExitCode::OK, ); h.propagate( &mut rt, caller, - cid.clone(), - ¶ms.value, + cid.unwrap().clone(), + &msg.value, TokenAmount::zero(), ) .unwrap(); @@ -709,11 +783,10 @@ fn test_apply_msg_bu_not_target_subnet() { let new_state: State = rt.get_state(); // cid should be removed from postbox - let r = new_state.load_from_postbox(rt.store(), cid.clone()); + let r = new_state.load_from_postbox(rt.store(), cid.unwrap()); assert_eq!(r.is_err(), true); let err = r.unwrap_err(); assert_eq!(err.to_string(), "cid not found in postbox"); - assert_eq!(new_state.nonce, old_state.nonce + 1); } /// This test covers the case where the amount send in the propagate @@ -722,8 +795,27 @@ fn test_apply_msg_bu_not_target_subnet() { #[test] fn test_propagate_with_remainder() { // ============== Register subnet ============== - let shid = SubnetID::new_from_parent(&ROOTNET_ID, *SUBNET_ONE); - let (h, mut rt) = setup(shid.clone()); + let parent = SubnetID::new_from_parent(&ROOTNET_ID, *SUBNET_ONE); + let shid = SubnetID::new_from_parent(&parent, *SUBNET_TWO); + + let (h, mut rt) = setup(parent); + h.register( + &mut rt, + &shid.subnet_actor(), + &TokenAmount::from_atto(10_u64.pow(18)), + ExitCode::OK, + ) + .unwrap(); + h.fund( + &mut rt, + &Address::new_id(1001), + &shid, + ExitCode::OK, + TokenAmount::from_atto(10_u64.pow(18)), + 1, + &TokenAmount::from_atto(10_u64.pow(18)), + ) + .unwrap(); let from = Address::new_bls(&[3; fvm_shared::address::BLS_PUB_LEN]).unwrap(); let to = Address::new_bls(&[4; fvm_shared::address::BLS_PUB_LEN]).unwrap(); @@ -747,30 +839,59 @@ fn test_propagate_with_remainder() { params: RawBytes::default(), nonce: msg_nonce, }; - let cid = h - .apply_cross_execute_only(&mut rt, value.clone(), params.clone(), None) - .unwrap() + + let epoch: ChainEpoch = 10; + rt.set_epoch(epoch); + let mut ch = Checkpoint::new(shid.clone(), epoch + 9); + // and include some fees. + let fee = TokenAmount::from_atto(5); + ch.data.cross_msgs = BatchCrossMsgs { + cross_msgs: Some(vec![CrossMsg { + msg: params.clone(), + wrapped: false, + }]), + fee: fee.clone(), + }; + + // distribute fee + rt.expect_send( + shid.subnet_actor(), + SUBNET_ACTOR_REWARD_METHOD, + None, + fee, + None, + ExitCode::OK, + ); + h.commit_child_check(&mut rt, &shid, &ch, ExitCode::OK) .unwrap(); // Part 1: test the message is stored in postbox let st: State = rt.get_state(); assert_ne!(tt.subnet().unwrap(), st.network_name); - // Check 1: `tt` is in `sub1`, which is not in that of `runtime` of gateway, will store in postbox - let item = st.load_from_postbox(rt.store(), cid.clone()).unwrap(); - assert_eq!(item.owners, Some(vec![ff.clone().raw_addr().unwrap()])); - let msg = item.cross_msg.msg; - assert_eq!(msg.to, tt); - // the nonce should not have changed at all - assert_eq!(msg.nonce, msg_nonce); - assert_eq!(msg.value, value); + // Check 1: `tt` is in `parent`, which is not in that of `runtime` of gateway, will store in postbox + let postbox = st.postbox.load(rt.store()).unwrap(); + let mut cid = None; + postbox + .for_each(|k, v| { + let item = PostBoxItem::deserialize(v.clone()).unwrap(); + assert_eq!(item.owners, Some(vec![ff.clone().raw_addr().unwrap()])); + let msg = item.cross_msg.msg; + assert_eq!(msg.to, tt); + // the nonce should not have changed at all + assert_eq!(msg.nonce, msg_nonce); + assert_eq!(msg.value, value); + + cid = Some(Cid::try_from(k.clone().to_vec()).unwrap()); + Ok(()) + }) + .unwrap(); // Part 2: Now we propagate from postbox // get the original subnet nonce first with an // excess to check that there is a remainder // to be returned let caller = ff.clone().raw_addr().unwrap(); - let old_state: State = rt.get_state(); // propagating a bottom-up message triggers the // funds included in the message to be burnt. rt.expect_send( @@ -781,25 +902,30 @@ fn test_propagate_with_remainder() { None, ExitCode::OK, ); - h.propagate(&mut rt, caller, cid.clone(), ¶ms.value, value.clone()) - .unwrap(); + h.propagate( + &mut rt, + caller, + cid.clone().unwrap(), + ¶ms.value, + value.clone(), + ) + .unwrap(); // state should be updated, load again let new_state: State = rt.get_state(); // cid should be removed from postbox - let r = new_state.load_from_postbox(rt.store(), cid.clone()); + let r = new_state.load_from_postbox(rt.store(), cid.unwrap()); assert_eq!(r.is_err(), true); let err = r.unwrap_err(); assert_eq!(err.to_string(), "cid not found in postbox"); - assert_eq!(new_state.nonce, old_state.nonce + 1); } /// This test covers the case where a bottom up cross_msg's target subnet is NOT the same as that of /// the gateway. It would save in postbox. Also, the gateway is the nearest parent, a switch to /// top down cross msg should occur. #[test] -fn test_apply_msg_bu_switch_td() { +fn test_commit_child_check_bu_switch_td() { // ============== Register subnet ============== let parent_sub = SubnetID::new_from_parent(&ROOTNET_ID, *SUBNET_ONE); let (h, mut rt) = setup(parent_sub.clone()); @@ -856,7 +982,7 @@ fn test_apply_msg_bu_switch_td() { let starting_nonce = get_subnet(&rt, &tt.subnet().unwrap().down(&h.net_name).unwrap()) .unwrap() - .nonce; + .topdown_nonce; // propagated as top-down, so it should distribute a fee in this subnet rt.expect_send( @@ -893,7 +1019,7 @@ fn test_apply_msg_bu_switch_td() { // the cross msg should have been committed to the next subnet, check this! let sub = get_subnet(&rt, &tt.subnet().unwrap().down(&h.net_name).unwrap()).unwrap(); - assert_eq!(sub.nonce, starting_nonce + 1); + assert_eq!(sub.topdown_nonce, starting_nonce + 1); let crossmsgs = sub.top_down_msgs.load(rt.store()).unwrap(); let msg = get_topdown_msg(&crossmsgs, starting_nonce).unwrap(); assert_eq!(msg.is_some(), true); @@ -907,22 +1033,38 @@ fn test_apply_msg_bu_switch_td() { /// This test covers the case where the cross_msg's target subnet is the SAME as that of /// the gateway. It would directly commit the message and will not save in postbox. #[test] -fn test_apply_msg_tp_target_subnet() { +fn test_commit_child_check_tp_target_subnet() { // ============== Register subnet ============== let shid = SubnetID::new_from_parent(&ROOTNET_ID, *SUBNET_ONE); - let (h, mut rt) = setup(shid.clone()); + let (h, mut rt) = setup(ROOTNET_ID.clone()); + + h.register( + &mut rt, + &SUBNET_ONE, + &TokenAmount::from_atto(10_u64.pow(18)), + ExitCode::OK, + ) + .unwrap(); + h.fund( + &mut rt, + &Address::new_id(1001), + &shid, + ExitCode::OK, + TokenAmount::from_atto(10_u64.pow(18)), + 1, + &TokenAmount::from_atto(10_u64.pow(18)), + ) + .unwrap(); let from = Address::new_bls(&[3; fvm_shared::address::BLS_PUB_LEN]).unwrap(); let to = Address::new_bls(&[4; fvm_shared::address::BLS_PUB_LEN]).unwrap(); - let sub = shid.clone(); - // ================ Setup =============== let value = TokenAmount::from_atto(10_u64.pow(17)); // ================= Top-Down =============== let ff = IPCAddress::new(&ROOTNET_ID, &from).unwrap(); - let tt = IPCAddress::new(&sub, &to).unwrap(); + let tt = IPCAddress::new(&shid.clone(), &to).unwrap(); let msg_nonce = 0; // Only system code is allowed to this method @@ -934,32 +1076,36 @@ fn test_apply_msg_tp_target_subnet() { params: RawBytes::default(), nonce: msg_nonce, }; - let sto = tt.raw_addr().unwrap(); - let v = value.clone(); - let cid = h - .apply_cross_execute_only( - &mut rt, - value.clone(), - params, - Some(Box::new(move |rt| { - rt.expect_send( - sto.clone(), - METHOD_SEND, - None, - v.clone(), - None, - ExitCode::OK, - ); - })), - ) + let epoch: ChainEpoch = 10; + rt.set_epoch(epoch); + let mut ch = Checkpoint::new(shid.clone(), epoch + 9); + // and include some fees. + let fee = TokenAmount::from_atto(5); + ch.data.cross_msgs = BatchCrossMsgs { + cross_msgs: Some(vec![CrossMsg { + msg: params.clone(), + wrapped: false, + }]), + fee: fee.clone(), + }; + + // distribute fee + rt.expect_send( + shid.subnet_actor(), + SUBNET_ACTOR_REWARD_METHOD, + None, + fee, + None, + ExitCode::OK, + ); + h.commit_child_check(&mut rt, &shid, &ch, ExitCode::OK) .unwrap(); - assert_eq!(cid.is_none(), true); } /// This test covers the case where the cross_msg's target subnet is not the same as that of /// the gateway. #[test] -fn test_apply_msg_tp_not_target_subnet() { +fn test_commit_child_check_tp_not_target_subnet() { // ============== Define Parameters ============== // gateway: /root/sub1 let shid = SubnetID::new_from_parent(&ROOTNET_ID, *SUBNET_ONE); @@ -1004,30 +1150,58 @@ fn test_apply_msg_tp_not_target_subnet() { params: RawBytes::default(), nonce: msg_nonce, }; - // cid is expected, should not be None - let cid = h - .apply_cross_execute_only(&mut rt, value.clone(), params.clone(), None) - .unwrap() + let epoch: ChainEpoch = 10; + rt.set_epoch(epoch); + let mut ch = Checkpoint::new(shid.clone(), epoch + 9); + // and include some fees. + let fee = TokenAmount::from_atto(5); + ch.data.cross_msgs = BatchCrossMsgs { + cross_msgs: Some(vec![CrossMsg { + msg: params.clone(), + wrapped: false, + }]), + fee: fee.clone(), + }; + + // distribute fee + rt.expect_send( + shid.subnet_actor(), + SUBNET_ACTOR_REWARD_METHOD, + None, + fee, + None, + ExitCode::OK, + ); + h.commit_child_check(&mut rt, &shid, &ch, ExitCode::OK) .unwrap(); // Part 1: test the message is stored in postbox let st: State = rt.get_state(); assert_ne!(tt.subnet().unwrap(), st.network_name); - // Check 1: `tt` is in `sub1`, which is not in that of `runtime` of gateway, will store in postbox - let item = st.load_from_postbox(rt.store(), cid.clone()).unwrap(); - assert_eq!(item.owners, Some(vec![ff.clone().raw_addr().unwrap()])); - let msg = item.cross_msg.msg; - assert_eq!(msg.to, tt); - // the nonce should not have changed at all - assert_eq!(msg.nonce, msg_nonce); - assert_eq!(msg.value, value); + // Check 1: `tt` is in `parent`, which is not in that of `runtime` of gateway, will store in postbox + let postbox = st.postbox.load(rt.store()).unwrap(); + let mut cid = None; + postbox + .for_each(|k, v| { + let item = PostBoxItem::deserialize(v.clone()).unwrap(); + assert_eq!(item.owners, Some(vec![ff.clone().raw_addr().unwrap()])); + let msg = item.cross_msg.msg; + assert_eq!(msg.to, tt); + // the nonce should not have changed at all + assert_eq!(msg.nonce, msg_nonce); + assert_eq!(msg.value, value); + + cid = Some(Cid::try_from(k.clone().to_vec()).unwrap()); + Ok(()) + }) + .unwrap(); // Part 2: Now we propagate from postbox // get the original subnet nonce first let starting_nonce = get_subnet(&rt, &tt.subnet().unwrap().down(&h.net_name).unwrap()) .unwrap() - .nonce; + .topdown_nonce; let caller = ff.clone().raw_addr().unwrap(); // propagated as top-down, so it should distribute a fee in this subnet @@ -1047,7 +1221,7 @@ fn test_apply_msg_tp_not_target_subnet() { h.propagate( &mut rt, caller, - cid.clone(), + cid.clone().unwrap(), ¶ms.value, TokenAmount::zero(), ) @@ -1057,14 +1231,14 @@ fn test_apply_msg_tp_not_target_subnet() { let st: State = rt.get_state(); // cid should be removed from postbox - let r = st.load_from_postbox(rt.store(), cid.clone()); + let r = st.load_from_postbox(rt.store(), cid.unwrap()); assert_eq!(r.is_err(), true); let err = r.unwrap_err(); assert_eq!(err.to_string(), "cid not found in postbox"); // the cross msg should have been committed to the next subnet, check this! let sub = get_subnet(&rt, &tt.subnet().unwrap().down(&h.net_name).unwrap()).unwrap(); - assert_eq!(sub.nonce, starting_nonce + 1); + assert_eq!(sub.topdown_nonce, starting_nonce + 1); let crossmsgs = sub.top_down_msgs.load(rt.store()).unwrap(); let msg = get_topdown_msg(&crossmsgs, starting_nonce).unwrap(); assert_eq!(msg.is_some(), true); @@ -1075,79 +1249,6 @@ fn test_apply_msg_tp_not_target_subnet() { assert_eq!(msg.value, value); } -#[test] -fn test_apply_msg_match_target_subnet() { - let (h, mut rt) = setup_root(); - - // Register a subnet with 1FIL collateral - let value = TokenAmount::from_atto(10_u64.pow(18)); - h.register(&mut rt, &SUBNET_ONE, &value, ExitCode::OK) - .unwrap(); - let shid = SubnetID::new_from_parent(&h.net_name, *SUBNET_ONE); - - // inject some funds - let funder_id = Address::new_id(1001); - let funder = IPCAddress::new( - &shid.parent().unwrap(), - &Address::new_bls(&[3; fvm_shared::address::BLS_PUB_LEN]).unwrap(), - ) - .unwrap(); - let amount = TokenAmount::from_atto(10_u64.pow(18)); - h.fund( - &mut rt, - &funder_id, - &shid, - ExitCode::OK, - amount.clone(), - 1, - &amount, - ) - .unwrap(); - - // Apply fund messages - for i in 0..5 { - h.apply_cross_msg(&mut rt, &funder, &funder, value.clone(), i, i, ExitCode::OK) - .unwrap(); - } - // Apply release messages - let from = IPCAddress::new(&shid, &BURNT_FUNDS_ACTOR_ADDR).unwrap(); - // with the same nonce - for _ in 0..5 { - h.apply_cross_msg(&mut rt, &from, &funder, value.clone(), 0, 0, ExitCode::OK) - .unwrap(); - } - // with increasing nonce - for i in 0..5 { - h.apply_cross_msg(&mut rt, &from, &funder, value.clone(), i, i, ExitCode::OK) - .unwrap(); - } - - // trying to apply non-subsequent nonce. - h.apply_cross_msg( - &mut rt, - &from, - &funder, - value.clone(), - 10, - 0, - ExitCode::USR_ILLEGAL_STATE, - ) - .unwrap(); - // trying already applied nonce - h.apply_cross_msg( - &mut rt, - &from, - &funder, - value.clone(), - 0, - 0, - ExitCode::USR_ILLEGAL_STATE, - ) - .unwrap(); - - // TODO: Trying to release over circulating supply -} - #[test] fn test_set_membership() { let (h, mut rt) = setup_root(); diff --git a/gateway/tests/harness.rs b/gateway/tests/harness.rs index eba0755..fcedb20 100644 --- a/gateway/tests/harness.rs +++ b/gateway/tests/harness.rs @@ -1,6 +1,3 @@ -use anyhow::anyhow; -use cid::multihash::Code; -use cid::multihash::MultihashDigest; use cid::Cid; use fil_actors_runtime::builtin::HAMT_BIT_WIDTH; use fil_actors_runtime::deserialize_block; @@ -10,11 +7,10 @@ use fil_actors_runtime::test_utils::{ MockRuntime, ACCOUNT_ACTOR_CODE_ID, INIT_ACTOR_CODE_ID, MULTISIG_ACTOR_CODE_ID, SUBNET_ACTOR_CODE_ID, SYSTEM_ACTOR_CODE_ID, }; +use fil_actors_runtime::INIT_ACTOR_ADDR; use fil_actors_runtime::{ - make_map_with_root_and_bitwidth, ActorError, Map, BURNT_FUNDS_ACTOR_ADDR, SYSTEM_ACTOR_ADDR, + make_map_with_root_and_bitwidth, ActorError, BURNT_FUNDS_ACTOR_ADDR, SYSTEM_ACTOR_ADDR, }; -use fil_actors_runtime::{Array, INIT_ACTOR_ADDR}; -use fvm_ipld_blockstore::Blockstore; use fvm_ipld_encoding::ipld_block::IpldBlock; use fvm_ipld_encoding::RawBytes; use fvm_shared::address::Address; @@ -27,10 +23,9 @@ use fvm_shared::MethodNum; use fvm_shared::METHOD_SEND; use ipc_gateway::checkpoint::ChildCheck; use ipc_gateway::{ - ext, get_topdown_msg, is_bottomup, Actor, ApplyMsgParams, Checkpoint, ConstructorParams, - CrossMsg, CrossMsgMeta, CrossMsgParams, CrossMsgs, FundParams, IPCAddress, IPCMsgType, Method, - PropagateParams, State, StorableMsg, Subnet, SubnetID, CROSSMSG_AMT_BITWIDTH, CROSS_MSG_FEE, - DEFAULT_CHECKPOINT_PERIOD, MAX_NONCE, MIN_COLLATERAL_AMOUNT, + ext, get_topdown_msg, is_bottomup, Actor, Checkpoint, ConstructorParams, CrossMsg, + CrossMsgParams, FundParams, IPCAddress, Method, PropagateParams, State, StorableMsg, Subnet, + SubnetID, CROSS_MSG_FEE, DEFAULT_CHECKPOINT_PERIOD, MIN_COLLATERAL_AMOUNT, }; use ipc_gateway::{CronCheckpoint, SUBNET_ACTOR_REWARD_METHOD}; use ipc_sdk::ValidatorSet; @@ -102,22 +97,15 @@ impl Harness { self.construct(rt); let st: State = rt.get_state(); - let store = &rt.store; - - let empty_bottomup_array = Array::<(), _>::new_with_bit_width(store, CROSSMSG_AMT_BITWIDTH) - .flush() - .unwrap(); assert_eq!(st.network_name, self.net_name); assert_eq!(st.min_stake, TokenAmount::from_atto(MIN_COLLATERAL_AMOUNT)); assert_eq!(st.check_period, DEFAULT_CHECKPOINT_PERIOD); - assert_eq!(st.applied_bottomup_nonce, MAX_NONCE); - assert_eq!(st.bottomup_msg_meta.cid(), empty_bottomup_array); + assert_eq!(st.applied_bottomup_nonce, 0); assert_eq!(st.cron_period, *DEFAULT_CRON_PERIOD); assert_eq!(st.genesis_epoch, *DEFAULT_GENESIS_EPOCH); verify_empty_map(rt, st.subnets.cid()); verify_empty_map(rt, st.checkpoints.cid()); - verify_empty_map(rt, st.check_msg_registry.cid()); } pub fn register( @@ -334,7 +322,7 @@ impl Harness { .unwrap() .unwrap(); assert_eq!(&sub.circ_supply, expected_circ_sup); - assert_eq!(sub.nonce, expected_nonce); + assert_eq!(sub.topdown_nonce, expected_nonce); let from = IPCAddress::new(&self.net_name, &*TEST_BLS).unwrap(); let to = IPCAddress::new(&id, &TEST_BLS).unwrap(); assert_eq!(msg.from, from); @@ -352,8 +340,6 @@ impl Harness { code: ExitCode, value: TokenAmount, expected_nonce: u64, - prev_meta: &Cid, - expected_fee: TokenAmount, ) -> Result { rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, *releaser); rt.expect_validate_caller_type(SIG_TYPES.clone()); @@ -393,28 +379,14 @@ impl Harness { let to = IPCAddress::new(&parent, &TEST_BLS).unwrap(); rt.set_epoch(0); let ch = st.get_window_checkpoint(rt.store(), 0).unwrap(); - let chmeta = ch.cross_msgs(); - - let cross_reg = st.check_msg_registry.load(rt.store()).unwrap(); - let meta = get_cross_msgs(&cross_reg, &chmeta.msgs_cid.cid()) - .unwrap() - .unwrap(); - let msg = meta.msgs[expected_nonce as usize].clone(); - assert_eq!(meta.msgs.len(), (expected_nonce + 1) as usize); + let msg = ch.data.cross_msgs.cross_msgs.unwrap()[expected_nonce as usize].clone(); assert_eq!(msg.msg.from, from); assert_eq!(msg.msg.to, to); assert_eq!(msg.msg.nonce, expected_nonce); assert_eq!(msg.msg.value, value); - if prev_meta != &Cid::default() { - match get_cross_msgs(&cross_reg, &prev_meta).unwrap() { - Some(_) => panic!("previous meta should have been removed"), - None => {} - } - } - - Ok(chmeta.msgs_cid.cid()) + Ok(Cid::default()) } pub fn send_cross( @@ -498,15 +470,8 @@ impl Harness { let to = IPCAddress::new(&dest, &to).unwrap(); rt.set_epoch(0); let ch = st.get_window_checkpoint(rt.store(), 0).unwrap(); - let chmeta = ch.cross_msgs(); - let cross_reg = st.check_msg_registry.load(rt.store()).unwrap(); - let meta = get_cross_msgs(&cross_reg, &chmeta.unwrap().msgs_cid.cid()) - .unwrap() - .unwrap(); - let msg = meta.msgs[nonce as usize].clone(); - - assert_eq!(meta.msgs.len(), (nonce + 1) as usize); + let msg = ch.data.cross_msgs.cross_msgs.unwrap()[nonce as usize].clone(); assert_eq!(msg.msg.from, from); assert_eq!(msg.msg.to, to); assert_eq!(msg.msg.nonce, nonce); @@ -519,7 +484,7 @@ impl Harness { let crossmsgs = sub.top_down_msgs.load(rt.store()).unwrap(); let msg = get_topdown_msg(&crossmsgs, nonce - 1).unwrap().unwrap(); assert_eq!(&sub.circ_supply, expected_circ_sup); - assert_eq!(sub.nonce, nonce); + assert_eq!(sub.topdown_nonce, nonce); let from = IPCAddress::new(&self.net_name, &SYSTEM_ACTOR_ADDR).unwrap(); let to = IPCAddress::new(&dest, &to).unwrap(); assert_eq!(msg.from, from); @@ -531,39 +496,6 @@ impl Harness { Ok(()) } - pub fn apply_cross_execute_only( - &self, - rt: &mut MockRuntime, - balance: TokenAmount, - params: StorableMsg, - append_expected_send: Option>, - ) -> Result, ActorError> { - rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR); - rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR.clone()]); - rt.set_balance(balance); - - if let Some(f) = append_expected_send { - f(rt) - } - let cid_blk = rt.call::( - Method::ApplyMessage as MethodNum, - IpldBlock::serialize_cbor(&ApplyMsgParams { - cross_msg: CrossMsg { - msg: params.clone(), - wrapped: false, - }, - })?, - )?; - rt.verify(); - - let cid: RawBytes = deserialize_block(cid_blk).unwrap(); - if cid.is_empty() { - Ok(None) - } else { - Ok(Some(Cid::try_from(cid.to_vec().as_slice()).unwrap())) - } - } - pub fn propagate( &self, rt: &mut MockRuntime, @@ -590,127 +522,6 @@ impl Harness { Ok(()) } - pub fn apply_cross_msg( - &self, - rt: &mut MockRuntime, - from: &IPCAddress, - to: &IPCAddress, - value: TokenAmount, - msg_nonce: u64, - td_nonce: u64, - code: ExitCode, - ) -> Result<(), ActorError> { - rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR); - rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR.clone()]); - - rt.set_balance(value.clone()); - let params = StorableMsg { - to: to.clone(), - from: from.clone(), - method: METHOD_SEND, - value: value.clone(), - params: RawBytes::default(), - nonce: msg_nonce, - }; - - let st: State = rt.get_state(); - let sto = params.to.subnet().unwrap(); - let rto = to.raw_addr().unwrap(); - - // if expected code is not ok - if code != ExitCode::OK { - expect_abort( - code, - rt.call::( - Method::ApplyMessage as MethodNum, - IpldBlock::serialize_cbor(&ApplyMsgParams { - cross_msg: CrossMsg { - msg: params.clone(), - wrapped: false, - }, - }) - .unwrap(), - ), - ); - rt.verify(); - return Ok(()); - } - - if params.apply_type(&st.network_name).unwrap() == IPCMsgType::BottomUp { - if sto == st.network_name { - rt.expect_send( - rto, - METHOD_SEND, - None, - params.value.clone(), - None, - ExitCode::OK, - ); - } - - rt.call::( - Method::ApplyMessage as MethodNum, - IpldBlock::serialize_cbor(&ApplyMsgParams { - cross_msg: CrossMsg { - msg: params.clone(), - wrapped: false, - }, - }) - .unwrap(), - )?; - rt.verify(); - let st: State = rt.get_state(); - assert_eq!(st.applied_bottomup_nonce, msg_nonce); - } else { - if sto == st.network_name { - rt.expect_send( - rto, - METHOD_SEND, - None, - params.value.clone(), - None, - ExitCode::OK, - ); - } - let cid_blk = rt.call::( - Method::ApplyMessage as MethodNum, - IpldBlock::serialize_cbor(&ApplyMsgParams { - cross_msg: CrossMsg { - msg: params.clone(), - wrapped: false, - }, - }) - .unwrap(), - )?; - rt.verify(); - let st: State = rt.get_state(); - - if sto != st.network_name { - let sub = self - .get_subnet(rt, &sto.down(&self.net_name).unwrap()) - .unwrap(); - assert_eq!(sub.nonce, td_nonce); - let crossmsgs = sub.top_down_msgs.load(rt.store()).unwrap(); - let msg = get_topdown_msg(&crossmsgs, td_nonce).unwrap(); - assert_eq!(msg.is_none(), true); - - let cid: RawBytes = deserialize_block(cid_blk).unwrap(); - let cid_ref = cid.to_vec(); - let item = st - .load_from_postbox(rt.store(), Cid::try_from(cid_ref.as_slice()).unwrap()) - .unwrap(); - assert_eq!(item.owners, Some(vec![from.clone().raw_addr().unwrap()])); - let msg = item.cross_msg.msg; - assert_eq!(&msg.to, to); - assert_eq!(msg.nonce, msg_nonce); - assert_eq!(msg.value, value); - } else { - assert_eq!(st.applied_topdown_nonce, msg_nonce + 1); - } - } - Ok(()) - } - pub fn check_state(&self) { // TODO: https://github.com/filecoin-project/builtin-actors/issues/44 } @@ -781,15 +592,6 @@ pub fn has_cid<'a, T: TCidContent>(children: &'a Vec>, cid: &Cid) -> boo children.iter().any(|c| c.cid() == *cid) } -pub fn get_cross_msgs<'m, BS: Blockstore>( - registry: &'m Map, - cid: &Cid, -) -> anyhow::Result> { - registry - .get(&cid.to_bytes()) - .map_err(|e| anyhow!("error getting fross messages: {:?}", e)) -} - fn set_rt_value_with_cross_fee(rt: &mut MockRuntime, value: &TokenAmount) { rt.set_value(if value.clone() != TokenAmount::zero() { value.clone() + &*CROSS_MSG_FEE @@ -797,18 +599,3 @@ fn set_rt_value_with_cross_fee(rt: &mut MockRuntime, value: &TokenAmount) { value.clone() }); } - -pub fn set_msg_meta(ch: &mut Checkpoint, rand: Vec, value: TokenAmount, fee: TokenAmount) { - let mh_code = Code::Blake2b256; - let c = TCid::from(Cid::new_v1( - fvm_ipld_encoding::DAG_CBOR, - mh_code.digest(&rand), - )); - let meta = CrossMsgMeta { - msgs_cid: c, - nonce: 0, - value, - fee, - }; - ch.set_cross_msgs(meta); -} diff --git a/subnet-actor/src/lib.rs b/subnet-actor/src/lib.rs index 083b0a8..dabc8f9 100644 --- a/subnet-actor/src/lib.rs +++ b/subnet-actor/src/lib.rs @@ -33,6 +33,7 @@ pub enum Method { Leave = frc42_dispatch::method_hash!("Leave"), Kill = frc42_dispatch::method_hash!("Kill"), SubmitCheckpoint = frc42_dispatch::method_hash!("SubmitCheckpoint"), + SetValidatorNetAddr = frc42_dispatch::method_hash!("SetValidatorNetAddr"), Reward = frc42_dispatch::method_hash!("Reward"), } @@ -358,6 +359,37 @@ impl SubnetActor for Actor { } } +/// This impl includes methods that are not required by the subnet actor +/// trait. +impl Actor { + /// Sets a new net address to an existing validator + pub fn set_validator_net_addr( + rt: &mut impl Runtime, + params: JoinParams, + ) -> Result, ActorError> { + rt.validate_immediate_caller_type(CALLER_TYPES_SIGNABLE.iter())?; + let caller = rt.message().caller(); + + rt.transaction(|st: &mut State, _rt| { + // if the caller is a validator allow him to change his net addr + if let Some(index) = st + .validator_set + .validators() + .iter() + .position(|x| x.addr == caller) + { + if let Some(x) = st.validator_set.validators_mut().get_mut(index) { + x.net_addr = params.validator_net_addr; + } + } else { + return Err(actor_error!(forbidden, "caller is not a validator")); + } + Ok(()) + })?; + Ok(None) + } +} + impl ActorCode for Actor { type Methods = Method; @@ -368,5 +400,6 @@ impl ActorCode for Actor { Kill => kill, SubmitCheckpoint => submit_checkpoint, Reward => reward, + SetValidatorNetAddr => set_validator_net_addr, } } diff --git a/subnet-actor/tests/actor_test.rs b/subnet-actor/tests/actor_test.rs index 51403bd..c2c1843 100644 --- a/subnet-actor/tests/actor_test.rs +++ b/subnet-actor/tests/actor_test.rs @@ -115,6 +115,79 @@ mod test { ); } + #[test] + fn test_set_net_addr_works() { + let mut runtime = construct_runtime(); + + let caller = Address::new_id(10); + let validator = Address::new_id(100); + let params = JoinParams { + validator_net_addr: validator.to_string(), + }; + let gateway = Address::new_id(IPC_GATEWAY_ADDR); + + // join + let value = TokenAmount::from_atto(MIN_COLLATERAL_AMOUNT); + runtime.set_value(value.clone()); + runtime.set_balance(value.clone()); + runtime.set_caller(*ACCOUNT_ACTOR_CODE_ID, caller.clone()); + runtime.expect_validate_caller_type(SIG_TYPES.clone()); + runtime.expect_send( + gateway.clone(), + ipc_gateway::Method::Register as u64, + None, + TokenAmount::from_atto(MIN_COLLATERAL_AMOUNT), + None, + ExitCode::new(0), + ); + runtime + .call::( + Method::Join as u64, + IpldBlock::serialize_cbor(¶ms).unwrap(), + ) + .unwrap(); + + // modify net address + let new_addr = String::from("test_addr"); + let params = JoinParams { + validator_net_addr: new_addr.clone(), + }; + + runtime.set_caller(*ACCOUNT_ACTOR_CODE_ID, caller.clone()); + runtime.expect_validate_caller_type(SIG_TYPES.clone()); + runtime + .call::( + Method::SetValidatorNetAddr as u64, + IpldBlock::serialize_cbor(¶ms).unwrap(), + ) + .unwrap(); + + let st: State = runtime.get_state(); + + if let Some(val) = st + .validator_set + .validators() + .iter() + .find(|x| x.addr == caller) + { + assert_eq!(val.net_addr, new_addr); + } else { + panic!("validator address not set correctly") + } + + // user which is not a validator tries to change address + let caller = Address::new_id(11); + runtime.set_caller(*ACCOUNT_ACTOR_CODE_ID, caller.clone()); + runtime.expect_validate_caller_type(SIG_TYPES.clone()); + expect_abort( + ExitCode::USR_FORBIDDEN, + runtime.call::( + Method::SetValidatorNetAddr as u64, + IpldBlock::serialize_cbor(¶ms).unwrap(), + ), + ); + } + #[test] fn test_join_works() { let mut runtime = construct_runtime();