What you’ll be able to do
- Send a message from Metal to a contract on another chain.
- Deliver the message and execute its destination call.
Prepare a configured route
Obtain the route provider’s authenticated configuration. You need the destination chain ID and RPC endpoint, itsMetalLightClient and MetalInbox addresses and public
artifacts, the receiving contract’s address and ABI, the authorized source
sender, and the destination’s finality policy. Fund the signing accounts that
submit the source and destination transactions for each chain’s transaction
fees.
Verify runtime bytecode against those artifacts and read the inbox’s
lightClient() and metalChainId(). Check the light client’s Metal chain ID,
genesis hash, verifier version, and validator configuration against the route
provider’s authenticated values. The destination must support EIP-2537 and
transient storage. Read the
light-client interfaces
for these checks. MetalLightClient accepts certificates only for epoch 0,
so check the source network’s current certificate epoch before sending.
The receiving contract must require both msg.sender == inbox and
IMetalInbox(inbox).metalSender() == authorizedSourceSender inside its
dispatched function. Its application state or event must identify the intended
action. metalSender() is zero outside execution.
Record the message on Metal
Use the Metal outbox address in protocol contracts and the outbox ABI. Encode the receiving contract’s function call with its ABI, then callsend(destinationChainId, target, payload). The destination chain ID is a
uint64. target is the receiving contract, and payload contains its exact
function selector and arguments.
For a viem application, install the public dependency with pnpm add viem.
Encode the action using the receiving contract’s published ABI:
receiverAbi, receiverFunction, and receiverArguments describe the
intended call. Submit data to the outbox from the authorized source sender. If
the receiving contract authorizes a source contract, that contract must call the
outbox. A direct call from an externally owned account authenticates that
account. When an application payment and message must succeed together, perform
both in the same source transaction.
Save the signed transaction and its hash before broadcast. Decode MessageSent
from the exact outbox address in the successful receipt and match its
destinationChainId, target, sender and payload to the intended
message. Save the emitted nonce, receipt block number and block hash, then check
canonical Metal finality. Use the nonce from the
event, since concurrent sends can change a simulation’s predicted return value.
Select an accepted checkpoint
ReadlatestBlockHash() and then checkpoint(hash) on the destination light
client. An accepted checkpoint covers the message when its Metal block number is
at least the message’s source block number. If no accepted checkpoint covers it,
read Metal’s finalized head with consensus_status and fetch
metal_getFinalityCertificate for a covering finalized block using the
certificate procedure.
Require a non-null certificate, epoch zero, and
keccak256(headerRlp) == blockHash. Submit update using the response’s
headerRlp, epoch, view, parentView, signersBitmap, and
signatureEipUncompressed, in that order. Both block height and view must
advance the light client’s latest values. Save the signed destination
transaction before broadcast, check successful execution and
isAccepted(blockHash), then apply destination finality. One checkpoint can
cover many messages.
A null certificate can reflect delayed persistence or pruning. Retry for a
recent finalized block or another endpoint. If another relayer advances the
light client first, check whether the checkpoint hash was accepted. Otherwise,
select a later accepted checkpoint and build its proof. Preserve the chosen hash
and returned interopMessageRoot with the delivery record.
Build the message proof
Retrieve everyMessageSent event from the Metal outbox from nonce zero through
the checkpoint block, including messages for other destinations. Paginate block
ranges to respect RPC limits. Verify the canonical block hashes, sort by nonce,
and reject gaps or duplicates. The leaf count is the number of all messages at
that block. Obtain missing event history from an indexer or RPC endpoint before
attempting delivery.
Each leaf uses ABI encoding of the full message:
nonce is that commitment.
Missing leaves are emptyLeaf. At each level, hash the left and right child in
that order. The parent of two empty subtrees is pair(empty, empty). For a
sparse implementation, store occupied nodes and derive missing siblings from the
hashes of empty subtrees at each level.
For the requested nonce, record its sibling at each of the 32 levels, starting
at the leaf level. Divide the index by two after each level. To verify a path,
hash pair(node, sibling) for an even index and pair(sibling, node) for an
odd index. The message root binds the tree root and leaf count and must equal
the checkpoint’s interopMessageRoot:
treeRoot is the depth-32 root and leafCount is a bigint. Require
nonce < leafCount. Changing the checkpoint requires a matching leaf count and
path, even when the source message is unchanged. An old accepted checkpoint and
its original proof remain usable while the nonce is unconsumed.
Execute and verify the destination effect
CheckMetalInbox.consumed(nonce). If false, simulate and submit
execute with
checkpointBlockHash, nonce, sender, target, payload, leafCount,
and the 32-element path, in that order. The relayer submits this
destination transaction from a signing account funded for that chain’s
transaction fees. Save its signed bytes and hash before broadcast.
Require a successful receipt and the inbox’s MessageExecuted with the expected
nonce, target and sender. Check consumed(nonce) == true, then read
the receiving contract’s event or state to verify the intended application
effect. Confirm the receipt’s canonical block and destination finality before
marking the delivery complete.
Key the delivery record by destination chain ID, inbox address and nonce. Retain
the source chain ID and outbox address with the original message fields,
checkpoint and transaction attempts. If a response is lost, look up the saved
hash and consumed state before rebroadcasting the same signed transaction.
Retrying delivery keeps the original message nonce, while a new send records a
separate instruction.
Recover and test rejection
A receiving contract’s revert rolls back the application call and consumed flag.
Track retries per message because the inbox allows other nonces to execute
first. Define any ordering, expiry, cancellation and acknowledgement rules in
the application protocol.
Test the route with a disposable receiving contract. Alter the payload and a
proof sibling and confirm rejection leaves state unchanged. Cause the receiving
contract to revert, confirm
consumed stays false, remove that condition, and
deliver the same message. After success, replay must fail without repeating its
effect. Record the final destination state and transaction hashes to test
recovery after a relayer restart.