diff --git a/Cargo.lock b/Cargo.lock index fda7ebf2..e239994d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2429,6 +2429,7 @@ dependencies = [ "serde", "serde_json", "tempfile", + "thiserror", "tokio", "tokio-stream", "tokio-util 0.7.8", diff --git a/fendermint/app/src/app.rs b/fendermint/app/src/app.rs index 072ca268..601fa99c 100644 --- a/fendermint/app/src/app.rs +++ b/fendermint/app/src/app.rs @@ -485,10 +485,8 @@ where Err(e) => invalid_check_tx(AppError::InvalidEncoding, e.description), Ok(result) => match result { Err(IllegalMessage) => invalid_check_tx(AppError::IllegalMessage, "".to_owned()), - Ok(result) => match result { - Err(InvalidSignature(d)) => invalid_check_tx(AppError::InvalidSignature, d), - Ok(ret) => to_check_tx(ret), - }, + Ok(Err(InvalidSignature(d))) => invalid_check_tx(AppError::InvalidSignature, d), + Ok(Ok(ret)) => to_check_tx(ret), }, }; diff --git a/fendermint/vm/actor_interface/src/ipc.rs b/fendermint/vm/actor_interface/src/ipc.rs index 7893460e..c210d142 100644 --- a/fendermint/vm/actor_interface/src/ipc.rs +++ b/fendermint/vm/actor_interface/src/ipc.rs @@ -78,6 +78,8 @@ pub mod gateway { use crate::eam::{self, EthAddress}; + pub const METHOD_INVOKE_CONTRACT: u64 = crate::evm::Method::InvokeContract as u64; + // Constructor parameters aren't generated as part of the Rust bindings. /// Container type `ConstructorParameters`. diff --git a/fendermint/vm/interpreter/Cargo.toml b/fendermint/vm/interpreter/Cargo.toml index 6406a17b..3cb4212a 100644 --- a/fendermint/vm/interpreter/Cargo.toml +++ b/fendermint/vm/interpreter/Cargo.toml @@ -24,6 +24,7 @@ num-traits = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } tracing = { workspace = true } +thiserror = { workspace = true } cid = { workspace = true } fvm = { workspace = true } @@ -42,7 +43,7 @@ pin-project = { workspace = true } [dev-dependencies] quickcheck = { workspace = true } -fvm = { workspace = true, features= ["arb", "testing"] } +fvm = { workspace = true, features = ["arb", "testing"] } fendermint_vm_genesis = { path = "../genesis", features = ["arb"] } tempfile = "3.7.0" diff --git a/fendermint/vm/interpreter/src/chain.rs b/fendermint/vm/interpreter/src/chain.rs index 0269b915..83bb6597 100644 --- a/fendermint/vm/interpreter/src/chain.rs +++ b/fendermint/vm/interpreter/src/chain.rs @@ -1,16 +1,22 @@ -use anyhow::anyhow; // Copyright 2022-2023 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT -use async_trait::async_trait; - -use fendermint_vm_message::{chain::ChainMessage, ipc::IpcMessage, signed::SignedMessage}; - use crate::{ - signed::{SignedMessageApplyRet, SignedMessageCheckRet}, + fvm::FvmMessage, + signed::{SignedMessageApplyRet, SignedMessageCheckRet, SyntheticMessage, VerifiableMessage}, CheckInterpreter, ExecInterpreter, GenesisInterpreter, ProposalInterpreter, QueryInterpreter, }; +use anyhow::Context; +use async_trait::async_trait; +use fendermint_vm_actor_interface::ipc; +use fendermint_vm_message::{ + chain::ChainMessage, + ipc::{BottomUpCheckpoint, CertifiedMessage, IpcMessage, SignedRelayedMessage}, +}; +use fvm_ipld_encoding::RawBytes; +use fvm_shared::econ::TokenAmount; +use num_traits::Zero; -/// A message a user is not supposed to send. +/// A user sent a transaction which they are not allowed to do. pub struct IllegalMessage; // For now this is the only option, later we can expand. @@ -71,7 +77,7 @@ where #[async_trait] impl ExecInterpreter for ChainMessageInterpreter where - I: ExecInterpreter, + I: ExecInterpreter, { type State = I::State; type Message = ChainMessage; @@ -86,15 +92,16 @@ where ) -> anyhow::Result<(Self::State, Self::DeliverOutput)> { match msg { ChainMessage::Signed(msg) => { - let (state, ret) = self.inner.deliver(state, msg).await?; + let (state, ret) = self + .inner + .deliver(state, VerifiableMessage::Signed(msg)) + .await?; Ok((state, ChainMessageApplyRet::Signed(ret))) } ChainMessage::Ipc(_) => { // This only happens if a validator is malicious or we have made a programming error. // I expect for now that we don't run with untrusted validators, so it's okay to quit. - Err(anyhow!( - "The handling of IPC messages is not yet implemented." - )) + todo!("#191: implement execution handling for IPC") } } } @@ -111,7 +118,7 @@ where #[async_trait] impl CheckInterpreter for ChainMessageInterpreter where - I: CheckInterpreter, + I: CheckInterpreter, { type State = I::State; type Message = ChainMessage; @@ -125,20 +132,30 @@ where ) -> anyhow::Result<(Self::State, Self::Output)> { match msg { ChainMessage::Signed(msg) => { - let (state, ret) = self.inner.check(state, msg, is_recheck).await?; + let (state, ret) = self + .inner + .check(state, VerifiableMessage::Signed(msg), is_recheck) + .await?; Ok((state, Ok(ret))) } - ChainMessage::Ipc(IpcMessage::BottomUpResolve(_msg)) => { - // TODO: Check the relayer signature and nonce. Don't have to check the quorum certificate, if it's invalid, make the relayer pay. - // For `ChainMessage::Signed` this is currently sperad out over the `SignedMessageInterpreter` and the `FvmMessageInterpreter`, - // so think about a way to reuse. For now returning illegal as a placeholder. - Ok((state, Err(IllegalMessage))) - } - ChainMessage::Ipc(IpcMessage::TopDown) - | ChainMessage::Ipc(IpcMessage::BottomUpExec(_)) => { - // Users cannot send some of these messages, only validators can propose them in blocks. - Ok((state, Err(IllegalMessage))) + ChainMessage::Ipc(msg) => { + match msg { + IpcMessage::BottomUpResolve(msg) => { + let msg = relayed_bottom_up_ckpt_to_fvm(&msg) + .context("failed to syntesize FVM message")?; + let (state, ret) = self + .inner + .check(state, VerifiableMessage::Synthetic(msg), is_recheck) + .await?; + + Ok((state, Ok(ret))) + } + IpcMessage::TopDown | IpcMessage::BottomUpExec(_) => { + // Users cannot send these messages, only validators can propose them in blocks. + Ok((state, Err(IllegalMessage))) + } + } } } } @@ -179,3 +196,33 @@ where self.inner.init(state, genesis).await } } + +/// Convert a signed relayed bottom-up checkpoint to a syntetic message we can send to the FVM. +/// +/// By mapping to an FVM message we invoke the right contract to validate the checkpoint, +/// and automatically charge the relayer gas for the execution of the check, but not the +/// execution of the cross-messages, which aren't part of the payload. +fn relayed_bottom_up_ckpt_to_fvm( + relayed: &SignedRelayedMessage>, +) -> anyhow::Result { + // TODO #192: Convert the checkpoint to what the actor expects. + let params = RawBytes::default(); + + let msg = FvmMessage { + version: 0, + from: relayed.message.relayer, + to: ipc::GATEWAY_ACTOR_ADDR, + sequence: relayed.message.sequence, + value: TokenAmount::zero(), + method_num: ipc::gateway::METHOD_INVOKE_CONTRACT, + params, + gas_limit: relayed.message.gas_limit, + gas_fee_cap: relayed.message.gas_fee_cap.clone(), + gas_premium: relayed.message.gas_premium.clone(), + }; + + let msg = SyntheticMessage::new(msg, &relayed.message, relayed.signature.clone()) + .context("failed to create syntetic message")?; + + Ok(msg) +} diff --git a/fendermint/vm/interpreter/src/signed.rs b/fendermint/vm/interpreter/src/signed.rs index aa69978d..8b24996b 100644 --- a/fendermint/vm/interpreter/src/signed.rs +++ b/fendermint/vm/interpreter/src/signed.rs @@ -4,7 +4,10 @@ use anyhow::anyhow; use async_trait::async_trait; use fendermint_vm_core::chainid::HasChainID; -use fendermint_vm_message::signed::{SignedMessage, SignedMessageError}; +use fendermint_vm_message::signed::{chain_id_bytes, SignedMessage, SignedMessageError}; +use fvm_ipld_encoding::Error as IpldError; +use fvm_shared::{chainid::ChainID, crypto::signature::Signature}; +use serde::Serialize; use crate::{ fvm::{FvmApplyRet, FvmCheckRet, FvmMessage}, @@ -17,6 +20,67 @@ pub struct InvalidSignature(pub String); pub type SignedMessageApplyRet = Result; pub type SignedMessageCheckRet = Result; +/// Different kinds of signed messages. +/// +/// This technical construct was introduced so we can have a simple linear interpreter stack +/// where everything flows through all layers, which means to pass something to the FVM we +/// have to go through the signature check. +pub enum VerifiableMessage { + /// A normal message sent by a user. + Signed(SignedMessage), + /// Something we constructed to pass on to the FVM. + Synthetic(SyntheticMessage), +} + +impl VerifiableMessage { + pub fn verify(&self, chain_id: &ChainID) -> Result<(), SignedMessageError> { + match self { + Self::Signed(m) => m.verify(chain_id), + Self::Synthetic(m) => m.verify(chain_id), + } + } + + pub fn into_message(self) -> FvmMessage { + match self { + Self::Signed(m) => m.into_message(), + Self::Synthetic(m) => m.message, + } + } +} + +pub struct SyntheticMessage { + /// The artifical message. + message: FvmMessage, + /// The CID of the original message (assuming here that that's what was signed). + orig_cid: cid::Cid, + /// The signature over the original CID. + signature: Signature, +} + +impl SyntheticMessage { + pub fn new( + message: FvmMessage, + orig: &T, + signature: Signature, + ) -> Result { + let orig_cid = fendermint_vm_message::cid(orig)?; + Ok(Self { + message, + orig_cid, + signature, + }) + } + + pub fn verify(&self, chain_id: &ChainID) -> Result<(), SignedMessageError> { + let mut data = self.orig_cid.to_bytes(); + data.extend(chain_id_bytes(chain_id).iter()); + + self.signature + .verify(&data, &self.message.from) + .map_err(SignedMessageError::InvalidSignature) + } +} + /// Interpreter working on signed messages, validating their signature before sending /// the unsigned parts on for execution. #[derive(Clone)] @@ -37,7 +101,7 @@ where S: HasChainID + Send + 'static, { type State = I::State; - type Message = SignedMessage; + type Message = VerifiableMessage; type BeginOutput = I::BeginOutput; type DeliverOutput = SignedMessageApplyRet; type EndOutput = I::EndOutput; @@ -61,7 +125,7 @@ where Ok((state, Err(InvalidSignature(s)))) } Ok(()) => { - let (state, ret) = self.inner.deliver(state, msg.message).await?; + let (state, ret) = self.inner.deliver(state, msg.into_message()).await?; Ok((state, Ok(ret))) } } @@ -83,7 +147,7 @@ where S: HasChainID + Send + 'static, { type State = I::State; - type Message = SignedMessage; + type Message = VerifiableMessage; type Output = SignedMessageCheckRet; async fn check( @@ -109,7 +173,10 @@ where Ok((state, Err(InvalidSignature(s)))) } Ok(()) => { - let (state, ret) = self.inner.check(state, msg.message, is_recheck).await?; + let (state, ret) = self + .inner + .check(state, msg.into_message(), is_recheck) + .await?; Ok((state, Ok(ret))) } } diff --git a/fendermint/vm/message/golden/chain/ipc_bottom_up_resolve.cbor b/fendermint/vm/message/golden/chain/ipc_bottom_up_resolve.cbor index b4bdc396..9fcf2b1b 100644 --- a/fendermint/vm/message/golden/chain/ipc_bottom_up_resolve.cbor +++ b/fendermint/vm/message/golden/chain/ipc_bottom_up_resolve.cbor @@ -1 +1 @@ -a163497063a16f426f74746f6d55705265736f6c7665a2676d657373616765a3676d657373616765a2676d657373616765a4697375626e65745f6964821b072a749d4e007b3d8256040a0c499bb8f71bfa8a7dc89b21be0050fe01dff50556040a03e75800cbbea43532cc0f8bf76a2f51c23b7d0a666865696768741a550feaaf756e6578745f76616c696461746f725f7365745f69641b01b536ec9cea9bd472626f74746f6d5f75705f6d65737361676573d82a5823001220a73b8d52b312e2e993235783dea2635a70f158f91e5cf1290e1798176ace66416b6365727469666963617465a16a7369676e61747572657381a26976616c696461746f724b00a78594e6f295fcc6f201697369676e61747572654501f9910bec6772656c61796572583103074f77fe3354fbdbc4d846007201386134cca1e7907d85010238c6fb67eceb687ad3017587aa0d9db170c0620089752f6873657175656e63651bf0f091093de904c5697369676e61747572654702bbcafcff3d42 \ No newline at end of file +a163497063a16f426f74746f6d55705265736f6c7665a2676d657373616765a6676d657373616765a2676d657373616765a4697375626e65745f6964821b7c71defc6a386e78834b00f5e3aee78080a0bf9f0156040a761b9e23622b966af8ef2d2d57dcb7856e5e54504a0088d9d5c1a3abc3d351666865696768741ae3e9fc60756e6578745f76616c696461746f725f7365745f69641b11fc1e77ae60cb5072626f74746f6d5f75705f6d65737361676573d82a5823001220469cd40705ee3724f1e8e598b1f50226efa85a5e902e1e1a3a02bceb1205b0656b6365727469666963617465a16a7369676e61747572657383a26976616c696461746f72550224c06e79929caadd2f1f96ff55f701a146058596697369676e617475726542021fa26976616c696461746f725502005c2e87b0e761927a2ab10f0001dcc782ff5fff697369676e6174757265460163b0f80048a26976616c696461746f725502de2d802ebfcc1d7701c5dcdc00018d140f936068697369676e61747572654601d96f8c01b96772656c61796572583103430fcc5b7c76bb455d9a000baccd63f6f4ff99e401f86200211f46783d001cf77ffe8d2c5fe3ff42091a71af1aa15ad26873657175656e63651bd6fb372fc464a1be696761735f6c696d69741b5bf160e4530c90af6b6761735f6665655f636170587d00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6c31e54ec6bd7d22c46631c1c2f505d00a79229be88bd39d73e5d150b1b8cfaa3ad78d19ea1281c36b6761735f7072656d69756d583100323429ee0653953f35328d8494d19c67967d867390896a46927e881ba2d75b89d436fbd61724c8b013ad3c251aa06d9f697369676e61747572654901005fe047ff3b3b4d \ No newline at end of file diff --git a/fendermint/vm/message/golden/chain/ipc_bottom_up_resolve.txt b/fendermint/vm/message/golden/chain/ipc_bottom_up_resolve.txt index bdf3e601..08b1158f 100644 --- a/fendermint/vm/message/golden/chain/ipc_bottom_up_resolve.txt +++ b/fendermint/vm/message/golden/chain/ipc_bottom_up_resolve.txt @@ -1 +1 @@ -Ipc(BottomUpResolve(SignedRelayedMessage { message: RelayedMessage { message: CertifiedMessage { message: BottomUpCheckpoint { subnet_id: SubnetID { root: 516353326254684989, children: [Address { payload: Delegated(DelegatedAddress { namespace: 10, length: 20, buffer: [12, 73, 155, 184, 247, 27, 250, 138, 125, 200, 155, 33, 190, 0, 80, 254, 1, 223, 245, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] }) }, Address { payload: Delegated(DelegatedAddress { namespace: 10, length: 20, buffer: [3, 231, 88, 0, 203, 190, 164, 53, 50, 204, 15, 139, 247, 106, 47, 81, 194, 59, 125, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] }) }] }, height: 1427106479, next_validator_set_id: 123064954695359444, bottom_up_messages: Cid(QmZbTTpS83pTiqyaUNTWBZotqDcin9p8j434qRVNQX2Z5r) }, certificate: MultiSig { signatures: [ValidatorSignature { validator: Address { payload: ID(17477890364055814823) }, signature: Signature { sig_type: Secp256k1, bytes: [249, 145, 11, 236] } }] } }, relayer: Address { payload: BLS([7, 79, 119, 254, 51, 84, 251, 219, 196, 216, 70, 0, 114, 1, 56, 97, 52, 204, 161, 231, 144, 125, 133, 1, 2, 56, 198, 251, 103, 236, 235, 104, 122, 211, 1, 117, 135, 170, 13, 157, 177, 112, 192, 98, 0, 137, 117, 47]) }, sequence: 17361536032392676549 }, signature: Signature { sig_type: BLS, bytes: [187, 202, 252, 255, 61, 66] } })) \ No newline at end of file +Ipc(BottomUpResolve(SignedRelayedMessage { message: RelayedMessage { message: CertifiedMessage { message: BottomUpCheckpoint { subnet_id: SubnetID { root: 8967193508766576248, children: [Address { payload: ID(11492764036801212917) }, Address { payload: Delegated(DelegatedAddress { namespace: 10, length: 20, buffer: [118, 27, 158, 35, 98, 43, 150, 106, 248, 239, 45, 45, 87, 220, 183, 133, 110, 94, 84, 80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] }) }, Address { payload: ID(5883686119324085384) }] }, height: 3823762528, next_validator_set_id: 1295944292151380816, bottom_up_messages: Cid(QmT6HwwYSVw1NxZHDFRBVpgXHoP1BZsp8egrUGJLKvBKBz) }, certificate: MultiSig { signatures: [ValidatorSignature { validator: Address { payload: Actor([36, 192, 110, 121, 146, 156, 170, 221, 47, 31, 150, 255, 85, 247, 1, 161, 70, 5, 133, 150]) }, signature: Signature { sig_type: BLS, bytes: [31] } }, ValidatorSignature { validator: Address { payload: Actor([0, 92, 46, 135, 176, 231, 97, 146, 122, 42, 177, 15, 0, 1, 220, 199, 130, 255, 95, 255]) }, signature: Signature { sig_type: Secp256k1, bytes: [99, 176, 248, 0, 72] } }, ValidatorSignature { validator: Address { payload: Actor([222, 45, 128, 46, 191, 204, 29, 119, 1, 197, 220, 220, 0, 1, 141, 20, 15, 147, 96, 104]) }, signature: Signature { sig_type: Secp256k1, bytes: [217, 111, 140, 1, 185] } }] } }, relayer: Address { payload: BLS([67, 15, 204, 91, 124, 118, 187, 69, 93, 154, 0, 11, 172, 205, 99, 246, 244, 255, 153, 228, 1, 248, 98, 0, 33, 31, 70, 120, 61, 0, 28, 247, 127, 254, 141, 44, 95, 227, 255, 66, 9, 26, 113, 175, 26, 161, 90, 210]) }, sequence: 15491036021568872894, gas_limit: 6625183060600852655, gas_fee_cap: TokenAmount(41855804968213567224547853478906320725054875457247406540771499545716837934567817284890561672488119458109166910841919797858872862722356017328064756151166307827869405370407152286801072676024887272960758522802096518222997167129660169469158860048690764605453004790204472697535710106459.730900083939639747), gas_premium: TokenAmount(7727066607978473919987999013363901971034861126626277477615296751585842935993863628627762990636417.915060831698054559) }, signature: Signature { sig_type: Secp256k1, bytes: [0, 95, 224, 71, 255, 59, 59, 77] } })) \ No newline at end of file diff --git a/fendermint/vm/message/src/ipc.rs b/fendermint/vm/message/src/ipc.rs index a7acaa66..1138ecc1 100644 --- a/fendermint/vm/message/src/ipc.rs +++ b/fendermint/vm/message/src/ipc.rs @@ -2,7 +2,9 @@ // SPDX-License-Identifier: Apache-2.0, MIT use cid::Cid; -use fvm_shared::{address::Address, clock::ChainEpoch, crypto::signature::Signature}; +use fvm_shared::{ + address::Address, clock::ChainEpoch, crypto::signature::Signature, econ::TokenAmount, +}; use ipc_sdk::subnet_id::SubnetID; use serde::{Deserialize, Serialize}; @@ -39,6 +41,10 @@ pub struct RelayedMessage { pub relayer: Address, /// The nonce of the relayer in the current subnet. pub sequence: u64, + /// The gas the relayer is willing to spend on the verification of the relayed message. + pub gas_limit: u64, + pub gas_fee_cap: TokenAmount, + pub gas_premium: TokenAmount, } /// Relayed messages are signed by the relayer, so we can rightfully charge them message inclusion costs. @@ -90,7 +96,7 @@ pub struct BottomUpCheckpoint { #[cfg(feature = "arb")] mod arb { - use fendermint_testing::arb::{ArbAddress, ArbCid, ArbSubnetID}; + use fendermint_testing::arb::{ArbAddress, ArbCid, ArbSubnetID, ArbTokenAmount}; use fvm_shared::crypto::signature::Signature; use quickcheck::{Arbitrary, Gen}; @@ -124,6 +130,9 @@ mod arb { message: T::arbitrary(g), relayer: ArbAddress::arbitrary(g).0, sequence: u64::arbitrary(g), + gas_limit: u64::arbitrary(g), + gas_fee_cap: ArbTokenAmount::arbitrary(g).0, + gas_premium: ArbTokenAmount::arbitrary(g).0, } } } diff --git a/fendermint/vm/message/src/lib.rs b/fendermint/vm/message/src/lib.rs index d5970f66..93278d9b 100644 --- a/fendermint/vm/message/src/lib.rs +++ b/fendermint/vm/message/src/lib.rs @@ -13,7 +13,7 @@ pub mod signed; /// Calculate the CID using Blake2b256 digest and DAG_CBOR. /// /// This used to be part of the `Cbor` trait, which is deprecated. -fn cid(value: &T) -> Result { +pub fn cid(value: &T) -> Result { let bz = to_vec(value)?; let digest = multihash::Code::Blake2b256.digest(&bz); let cid = Cid::new_v1(DAG_CBOR, digest); diff --git a/fendermint/vm/message/src/signed.rs b/fendermint/vm/message/src/signed.rs index 6c414568..dff00732 100644 --- a/fendermint/vm/message/src/signed.rs +++ b/fendermint/vm/message/src/signed.rs @@ -202,7 +202,7 @@ fn sign_secp256k1(sk: &libsecp256k1::SecretKey, hash: &[u8; 32]) -> [u8; SECP_SI } /// Turn a [`ChainID`] into bytes. Uses big-endian encoding. -fn chain_id_bytes(chain_id: &ChainID) -> [u8; 8] { +pub fn chain_id_bytes(chain_id: &ChainID) -> [u8; 8] { u64::from(*chain_id).to_be_bytes() }