What you’ll be able to do
- Deliver a message from another chain to Metal.
- Handle authenticated incoming messages in a receiving contract.
Outbox on the source chain. A relayer proves
its stored commitment under a checkpoint accepted by Metal’s light client for
that chain. ForeignInbox verifies the evidence and calls the receiving
contract with the original payload.
Check the route and receiving contract
Obtain the route provider’s authenticated source chain ID, source RPC endpoint, sourceOutbox address, artifact and approved runtime bytecode hash, Metal
ForeignInbox address and artifact, and light-client configuration. You also
need the receiving contract’s ABI, address and authorized source sender.
Fund the relayer’s signing account on Metal and the source transaction’s signing
account on its chain for their respective fees.
Verify runtime bytecode against the supplied artifacts. Read the inbox’s
lightClient(), foreignOutbox(), foreignOutboxCodeHash() and
foreignChainId() and compare them to the route configuration. The code hash
pins the runtime bytecode of a direct, immutable Outbox. A new Outbox
version requires a new inbox. Preserve application records across route
replacement to prevent completed instructions from executing again. The source
RPC must support eth_getProof at a checkpoint that the configured light client
accepts.
Inside the receiving function, require both msg.sender == inbox and
IForeignInbox(inbox).foreignSender() == authorizedSourceSender before changing
application state. These checks authenticate the route and source application.
The inbox interface exposes
foreignSender() only during execution and returns zero otherwise. Anyone may
relay a valid message, while the receiving contract authorizes its source
sender.
Queue the source call
Encode the receiving contract’s function selector and arguments with its ABI. From the authorized source application, callOutbox.send
with Metal’s chain ID as uint64, the receiving contract address, and that
payload. A source contract must make this call when the Metal contract
authorizes that contract as source sender. Calling the outbox directly from an
externally owned account authenticates that account as the source sender.
Save the signed transaction and its hash before broadcast. Decode MessageSent
from the configured source outbox in its successful receipt. Match destination
chain ID, target, sender and payload, then save the emitted nonce,
source block number and block hash. Wait for source-chain finality before
proceeding. If the submission result is uncertain, look up its saved transaction
hash to resume the same message.
Select an accepted source checkpoint
Choose a finalized source block at or after the message’s block and confirmisAccepted(checkpointBlockHash) on the light client. Read its checkpoint
return value and retain the accepted block number and state root. The
light-client reference gives the exact
return fields and update signatures.
Metal block execution maintains Ethereum’s light-client checkpoints.
latestUpdateTimestamp() can advance without a newer execution checkpoint, so
use the checkpoint’s block number to decide whether it covers the message.
Tempo and Arc require a public certificate source and documented conversion to
their exact update arguments. Authenticate the provider’s pinned group key or
validator set independently of that certificate feed. Each light client uses its
source chain’s certificate format, as specified in the reference.
Request the account and storage proofs
Install the publicviem dependency with pnpm add viem. Using a source-chain
public client configured with the supplied endpoint, derive the storage key and
request its proof at the selected block number:
nonce is the saved message nonce, and sourceOutbox is the authenticated
outbox address. outboxCodeHash is the inbox’s foreignOutboxCodeHash() value,
already matched to the provider’s approved runtime bytecode artifact. The
outbox’s messages mapping is at storage slot zero. Select the storageProof
entry whose key equals storageKey, comparing keys as 256-bit integers if
their hex padding differs. Require a nonzero value and compare it to the message
commitment below:
accountProof
and the selected entry’s proof separately so the inbox can verify both.
If the endpoint provides proofs only at the latest block, collect the block and
proof, then read the block again. Retry if it changed, and obtain checkpoint
acceptance for the exact block hash and state root used by the proof. Keep the
original proofs until acceptance, since a later checkpoint requires new proofs.
For Ethereum, Metal must record that exact execution checkpoint. Use a
historical proof provider if the endpoint cannot supply proofs for an accepted
checkpoint.
Execute and record the result
ReadForeignInbox.consumed(nonce). If false, simulate and submit
execute with
checkpointBlockHash, nonce, sender, target, payload,
accountProof, and storageProof, in that order. The relayer submits this
Metal transaction from its signing account. The signing account’s balance in the
gas token pays the transaction fee. Save the signed bytes and transaction hash
before submission.
Require a successful receipt and MessageExecuted from the configured inbox
with the expected nonce, target and sender. Verify consumed(nonce) is
true and check the receiving contract’s application event or state change. Check
Metal finality and the receipt’s canonical block before
applying an irreversible downstream effect.
Key each delivery by Metal chain ID, inbox address and nonce, retaining the
source transaction and message fields. On an uncertain response, recover the
saved transaction hash and consumed state to find the existing execution. If
still pending, rebroadcast the same signed transaction. Retrying delivery keeps
the original message nonce, while a new send queues a separate instruction.
Recover and test failed delivery
A receiving contract’s revert rolls back the consumed flag and every change in
the dispatched call. Track retries per message because the inbox allows other
nonces to execute first. Define any ordering, expiry or cancellation rules in
the application contract.
Test the configured route with a disposable receiving contract. An altered
payload or proof must fail without consuming the nonce. Cause the receiving
contract to revert and verify that application state stays unchanged, then
resolve that condition and deliver the same message. After success, replay must
fail without repeating the effect. Save failed and successful attempts so a
restarted relayer can distinguish retryable and completed delivery.
Use a controlled proof fixture with a different account code hash to verify
rejection before nonce consumption even when the storage value matches.