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

FM-190: Check relayed bottom-up checkpoints #198

Merged
merged 3 commits into from
Aug 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to double-check, this is the original signature of the message, right? Should we maybe call it WrappedMessage instead of SyntheticMessage? (weak opinion weakly held, but synthetic seemed like "artificial" to me :) )

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is an artificial message, that's exactly what I was trying to convey, that it's just a transient technical message to piggy-back on the signature checks and FVM message execution. The construction of the FVM message from the original is synthesising an artificial message that nobody sent.

It's indeed wrapping something but that wouldn't say why; so does the other one after all.

You are correct that signature is the signature over the original message, in particular the CID of the original message.

We could get rid of this if we used FVM messages, in which case instead of synthesising a message in the ChainMessageInterpreter from an IPC specific one, we would parse an FVM message into an IPC message to look for CIDs to resolve.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is fine for now, and now I understand the rationale behind the name. Thanks!

}

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());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought the chainID was a prefix instead of a suffix of the message cid. Out of a curiosity, is this Fendermint-specific or Filecoin also does this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no idea what Filecoin does, I did this in #115

Ethereum seems to play with the signature itself, which seemed so complicated I didn't think we need to replicate it in Fendermint. The ethers library takes care of it for us, but for regular messages I do the suffix. Is prefix safer?


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.
adlrocha marked this conversation as resolved.
Show resolved Hide resolved
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