Skip to content
This repository has been archived by the owner on Jan 11, 2024. It is now read-only.

Commit

Permalink
FM-190: Check relayed bottom-up checkpoints (#198)
Browse files Browse the repository at this point in the history
* FM-190: Create an IpcMessageInterpreter

* FM-190: Remove IpcMessageInterpreter

* FM-190: Add VerifiableMessage and SyntheticMessage
  • Loading branch information
aakoshh authored Aug 23, 2023
1 parent 285e644 commit f7de439
Show file tree
Hide file tree
Showing 11 changed files with 165 additions and 40 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 2 additions & 4 deletions fendermint/app/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
},
};

Expand Down
2 changes: 2 additions & 0 deletions fendermint/vm/actor_interface/src/ipc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
3 changes: 2 additions & 1 deletion fendermint/vm/interpreter/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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"

Expand Down
95 changes: 71 additions & 24 deletions fendermint/vm/interpreter/src/chain.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -71,7 +77,7 @@ where
#[async_trait]
impl<I> ExecInterpreter for ChainMessageInterpreter<I>
where
I: ExecInterpreter<Message = SignedMessage, DeliverOutput = SignedMessageApplyRet>,
I: ExecInterpreter<Message = VerifiableMessage, DeliverOutput = SignedMessageApplyRet>,
{
type State = I::State;
type Message = ChainMessage;
Expand All @@ -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")
}
}
}
Expand All @@ -111,7 +118,7 @@ where
#[async_trait]
impl<I> CheckInterpreter for ChainMessageInterpreter<I>
where
I: CheckInterpreter<Message = SignedMessage, Output = SignedMessageCheckRet>,
I: CheckInterpreter<Message = VerifiableMessage, Output = SignedMessageCheckRet>,
{
type State = I::State;
type Message = ChainMessage;
Expand All @@ -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)))
}
}
}
}
}
Expand Down Expand Up @@ -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<CertifiedMessage<BottomUpCheckpoint>>,
) -> anyhow::Result<SyntheticMessage> {
// 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)
}
77 changes: 72 additions & 5 deletions fendermint/vm/interpreter/src/signed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -17,6 +20,67 @@ pub struct InvalidSignature(pub String);
pub type SignedMessageApplyRet = Result<FvmApplyRet, InvalidSignature>;
pub type SignedMessageCheckRet = Result<FvmCheckRet, InvalidSignature>;

/// 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<T: Serialize>(
message: FvmMessage,
orig: &T,
signature: Signature,
) -> Result<Self, IpldError> {
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)]
Expand All @@ -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;
Expand All @@ -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)))
}
}
Expand All @@ -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(
Expand All @@ -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)))
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
a163497063a16f426f74746f6d55705265736f6c7665a2676d657373616765a3676d657373616765a2676d657373616765a4697375626e65745f6964821b072a749d4e007b3d8256040a0c499bb8f71bfa8a7dc89b21be0050fe01dff50556040a03e75800cbbea43532cc0f8bf76a2f51c23b7d0a666865696768741a550feaaf756e6578745f76616c696461746f725f7365745f69641b01b536ec9cea9bd472626f74746f6d5f75705f6d65737361676573d82a5823001220a73b8d52b312e2e993235783dea2635a70f158f91e5cf1290e1798176ace66416b6365727469666963617465a16a7369676e61747572657381a26976616c696461746f724b00a78594e6f295fcc6f201697369676e61747572654501f9910bec6772656c61796572583103074f77fe3354fbdbc4d846007201386134cca1e7907d85010238c6fb67eceb687ad3017587aa0d9db170c0620089752f6873657175656e63651bf0f091093de904c5697369676e61747572654702bbcafcff3d42
a163497063a16f426f74746f6d55705265736f6c7665a2676d657373616765a6676d657373616765a2676d657373616765a4697375626e65745f6964821b7c71defc6a386e78834b00f5e3aee78080a0bf9f0156040a761b9e23622b966af8ef2d2d57dcb7856e5e54504a0088d9d5c1a3abc3d351666865696768741ae3e9fc60756e6578745f76616c696461746f725f7365745f69641b11fc1e77ae60cb5072626f74746f6d5f75705f6d65737361676573d82a5823001220469cd40705ee3724f1e8e598b1f50226efa85a5e902e1e1a3a02bceb1205b0656b6365727469666963617465a16a7369676e61747572657383a26976616c696461746f72550224c06e79929caadd2f1f96ff55f701a146058596697369676e617475726542021fa26976616c696461746f725502005c2e87b0e761927a2ab10f0001dcc782ff5fff697369676e6174757265460163b0f80048a26976616c696461746f725502de2d802ebfcc1d7701c5dcdc00018d140f936068697369676e61747572654601d96f8c01b96772656c61796572583103430fcc5b7c76bb455d9a000baccd63f6f4ff99e401f86200211f46783d001cf77ffe8d2c5fe3ff42091a71af1aa15ad26873657175656e63651bd6fb372fc464a1be696761735f6c696d69741b5bf160e4530c90af6b6761735f6665655f636170587d00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6c31e54ec6bd7d22c46631c1c2f505d00a79229be88bd39d73e5d150b1b8cfaa3ad78d19ea1281c36b6761735f7072656d69756d583100323429ee0653953f35328d8494d19c67967d867390896a46927e881ba2d75b89d436fbd61724c8b013ad3c251aa06d9f697369676e61747572654901005fe047ff3b3b4d
Original file line number Diff line number Diff line change
@@ -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] } }))
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] } }))
13 changes: 11 additions & 2 deletions fendermint/vm/message/src/ipc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -39,6 +41,10 @@ pub struct RelayedMessage<T> {
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.
Expand Down Expand Up @@ -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};

Expand Down Expand Up @@ -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,
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion fendermint/vm/message/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T: Serialize>(value: &T) -> Result<Cid, IpldError> {
pub fn cid<T: Serialize>(value: &T) -> Result<Cid, IpldError> {
let bz = to_vec(value)?;
let digest = multihash::Code::Blake2b256.digest(&bz);
let cid = Cid::new_v1(DAG_CBOR, digest);
Expand Down
2 changes: 1 addition & 1 deletion fendermint/vm/message/src/signed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

Expand Down

0 comments on commit f7de439

Please sign in to comment.