Integration with EVM
Avalanche Warp Messaging provides a basic primitive for signing and verifying messages between Avalanche L1s.
Warp (SAE C-Chain)
This package handles the parsing, verification, and storage of Avalanche Warp Messages for the C-Chain implementation of Streaming Asynchronous Execution (SAE).
A warp message is a payload attested by the validator set of a source chain: each validator signs the message with the BLS key it registered on the P-Chain, and the individual signatures are aggregated into a single multi-signature. Because every validator's BLS public key and stake weight are readable from the P-Chain, any chain can verify that a stake-weight quorum of the source chain endorsed a message — without additional trust assumptions or messaging infrastructure. (Newer documentation also calls this Avalanche Interchain Messaging, ICM.)
This package provides the pieces the VM needs to participate in that protocol: FromReceipts and Storage capture and persist the messages a block emits, Verifier decides what this node is willing to sign, and VerifyBlock checks the signed messages that transactions deliver. The rest of the lifecycle — the precompile that creates messages, the p2p protocol that collects signatures, the predicate machinery that carries signed messages in transactions — lives outside this package. This README shows how the pieces fit together.
Message lifecycle
flowchart TD
subgraph SC["Sender chain"]
user["Contract / EOA"]:::ext
pre["Warp precompile"]:::evm
rcpt["Block receipts"]:::evm
fr["FromReceipts"]:::pkg
store[("Storage")]:::pkg
end
subgraph VAL["Each sender-chain validator"]
h["acp118.Handler"]:::plat
v["Verifier"]:::pkg
sgn["BLS signer"]:::plat
end
rel["Relayer"]:::ext
subgraph RC["Receiver chain"]
tx["Tx with signed message<br/>in its access list"]:::ext
vb["VerifyBlock"]:::pkg
vp["Precompile<br/>VerifyPredicate"]:::evm
hdr["Predicate results<br/>in block header"]:::evm
gvm["getVerifiedWarpMessage"]:::evm
end
user -->|"① sendWarpMessage"| pre
pre -->|"emits log with the<br/>unsigned message"| rcpt
rcpt -->|"② after the block executes"| fr
fr -->|"Add"| store
rel -->|"③ SignatureRequest<br/>(p2p, per validator)"| h
h -->|"Verify"| v
v -->|"Get"| store
h -->|"on success: Sign"| sgn
sgn -->|"BLS signature"| rel
rel -->|"④ aggregate to a stake-weight<br/>quorum: signed message"| tx
tx -->|"block verification"| vb
vb -->|"per predicate"| vp
vb -->|"results"| hdr
hdr -->|"read during execution"| gvm
classDef pkg fill:#1d4ed8,stroke:#93c5fd,color:#fff
classDef evm fill:#b45309,stroke:#fde68a,color:#fff
classDef plat fill:#6d28d9,stroke:#c4b5fd,color:#fff
classDef ext fill:#4b5563,stroke:#d1d5db,color:#fff| Style | Meaning |
|---|---|
| Blue | This package |
| Orange | EVM side: warp precompile and predicate handling |
| Purple | AvalancheGo platform: ACP-118 handler, BLS signer |
| Gray | External actors |
- Send — a contract calls
sendWarpMessageon the warp precompile, which packs the payload into an unsigned warp message and emits it as a log. - Persist — after the block executes (under SAE, execution happens after acceptance), the VM collects every message in the block with
FromReceiptsand persists them withStorage.Add. - Sign — a relayer asks each validator for its signature over p2p (ACP-118). The handler consults
Verifier.Verify, which checksStorage; on success the node responds with its BLS signature, and the relayer aggregates responses until a stake-weight quorum is reached. - Receive — anyone submits a transaction on the receiver chain carrying the signed message in its access list.
VerifyBlockchecks the multi-signature during block verification and the results are recorded in the block header, so execution can serve the message to contracts viagetVerifiedWarpMessage.
Every validator of the sender chain runs the full sender-chain stack, which is why the Verifier in stage ③ reads the same Storage its node populated in stage ②. Sender and receiver chains need not run identical software, but they share this logical flow — and a single instance of this VM plays both ends of the diagram, sending some messages and receiving others.
Conceptual model
What makes a message signable
Three message types can be signed:
- Precompile messages —
AddressedCallpayloads the Warp contract emitted viasendWarpMessage. - Block-hash attestations —
Hashpayloads attesting that this chain accepted a given block. - Off-chain messages — messages the operator lists in the chain config for the node to sign, even though they were never emitted on chain.
The Verifier doesn't track these kinds explicitly; it validates a message by checking two sources in order:
flowchart TD
in["UnsignedMessage<br/>from acp118.Handler"]:::plat
g{"Storage.Get"}:::pkg
p{"payload.Parse"}:::pkg
t{"payload type?"}:::pkg
a{"Backend.IsAccepted"}:::pkg
ok(["sign the message"]):::ok
e1["StorageErrCode"]:::err
e2["ParseErrCode"]:::err
e3["UnknownMessageErrCode"]:::err
e4["NotAcceptedErrCode"]:::err
in --> g
g -->|"found: precompile-emitted<br/>or off-chain message"| ok
g -->|"error other than ErrNotFound"| e1
g -->|"ErrNotFound"| p
p -->|"invalid"| e2
p -->|"valid"| t
t -->|"Hash"| a
t -->|"other, incl. AddressedCall<br/>missing from storage"| e3
a -->|"accepted"| ok
a -->|"not accepted"| e4
classDef pkg fill:#1d4ed8,stroke:#93c5fd,color:#fff
classDef plat fill:#6d28d9,stroke:#c4b5fd,color:#fff
classDef ok fill:#15803d,stroke:#86efac,color:#fff
classDef err fill:#b91c1c,stroke:#fca5a5,color:#fffGreen means the node signs; red is the error code returned to the requester.
The two Storage-backed kinds are indistinguishable to the Verifier: precompile and off-chain messages both live in Storage, so a single Storage.Get confirms either one. Only when that lookup misses does the Verifier fall back to the block-hash path — parsing the payload and asking the Backend whether the named block is accepted. Anything else — a corrupt payload, a non-Hash payload, or an AddressedCall absent from Storage — is refused with an error code that tells the requester why.
Inbound messages are predicates
A signed message rides into the receiver chain inside a transaction's access list — a predicate (see the predicate encoding). Verifying it means checking the multi-signature against the source chain's validator set, which changes over time; the proposervm block context supplies the P-Chain height that pins it.
flowchart TD
tx["Transaction<br/>access list"]:::ext
fal["predicate.FromAccessList"]:::evm
bctx["block.Context<br/>(P-Chain height)"]:::plat
bits["set.Bits of<br/>failed indices"]:::pkg
res["BlockResults"]:::pkg
hdr["Block header"]:::evm
gvm["getVerifiedWarpMessage"]:::evm
subgraph VB["VerifyBlock"]
vp["Precompile VerifyPredicate<br/>(one goroutine per predicate)"]:::evm
end
tx --> fal
fal -->|"predicates keyed by<br/>precompile address"| vp
bctx -->|"pins the source chain's<br/>validator set"| vp
vp --> bits --> res
res -->|"encoded into the header"| hdr
hdr -->|"read during execution"| gvm
classDef pkg fill:#1d4ed8,stroke:#93c5fd,color:#fff
classDef evm fill:#b45309,stroke:#fde68a,color:#fff
classDef plat fill:#6d28d9,stroke:#c4b5fd,color:#fff
classDef ext fill:#4b5563,stroke:#d1d5db,color:#fffThe results — which predicates failed, per transaction — are encoded into the block header, so nodes that process the block later read the recorded outcome instead of re-verifying signatures. Execution, including historical re-execution, only reads the recorded bits when a contract calls getVerifiedWarpMessage.
Easy to misunderstand
- There are two unrelated "verify"s:
Verifier.Verifydecides whether this node should sign an outbound message;VerifyBlockchecks the multi-signature on inbound messages. They share no code path. Verifierdoesn't checkNetworkIDorSourceChainID— the ACP-118 contract assigns those checks to the BLS signer.Storageholds only unsigned messages; signatures are never persisted.- A message becomes signable only once the block that emitted it has been executed — under SAE, strictly after acceptance.
Maintenance notes
- Database compatibility.
Storage's on-disk layout MUST NOT change: breaking it silently orphans every previously persisted message. - Off-chain messages are memory-only on purpose. They are operator-supplied configuration, not chain data, so the persisted set stays exactly what the precompile emitted — and an off-chain message is removed by simply not supplying it on the next construction.
- Predicate results are encoded in the header so warp messages are never verified during bootstrapping. Verification needs the source chain's validator set, which isn't guaranteed to be available then — bootstrapping nodes must be able to rely on the recorded results instead of re-checking signatures.
- VM wiring. Harvested messages are persisted in
(*hooks).AfterExecutingBlock, the predicate results are encoded into the header in(*builder).BuildBlock(both in../hooks.go), and the ACP-118 handler is registered with aBackendimplementation at VM initialization. Persisting inAfterExecutingBlockguarantees that only messages from accepted blocks are signable, because SAE executes blocks only after acceptance.
References
Integration points:
../hooks.go-AfterExecutingBlock,BuildBlock
Message format:
vms/platformvm/warp- message structure andBitSetSignaturevms/platformvm/warp/payload-AddressedCallandHashpayloads
Signing protocol:
network/p2p/acp118- signature request handler and aggregator- ACP-118 - protocol specification
EVM side:
- coreth warp precompile -
sendWarpMessage,getVerifiedWarpMessage, predicate verification vms/evm/predicate- access-list predicate encoding and results
Is this guide helpful?