Integrating Avalanche Warp Messaging into the EVM
Avalanche Warp Messaging offers a basic primitive to enable Cross-Subnet communication on the Avalanche Network.
It is intended to allow communication between arbitrary Custom Virtual Machines (including, but not limited to Subnet-EVM and Coreth).
How does Avalanche Warp Messaging Work?β
Avalanche Warp Messaging uses BLS Multi-Signatures with Public-Key Aggregation where every Avalanche validator registers a public key alongside its NodeID on the Avalanche P-Chain.
Every node tracking a Subnet has read access to the Avalanche P-Chain. This provides weighted sets of BLS Public Keys that correspond to the validator sets of each Subnet on the Avalanche Network. Avalanche Warp Messaging provides a basic primitive for signing and verifying messages between Subnets: the receiving network can verify whether an aggregation of signatures from a set of source Subnet validators represents a threshold of stake large enough for the receiving network to process the message.
For more details on Avalanche Warp Messaging, see the AvalancheGo Warp README.
Flow of Sending / Receiving a Warp Message within the EVMβ
The Avalanche Warp Precompile enables this flow to send a message from blockchain A to blockchain B:
- Call the Warp Precompile
sendWarpMessage
function with the arguments for theUnsignedMessage
- Warp Precompile emits an event / log containing the
UnsignedMessage
specified by the caller ofsendWarpMessage
- Network accepts the block containing the
UnsignedMessage
in the log, so that validators are willing to sign the message - An off-chain relayer queries the validators for their signatures of the message and aggregates the signatures to create a
SignedMessage
- The off-chain relayer encodes the
SignedMessage
as the predicate in the AccessList of a transaction to deliver on blockchain B - The transaction is delivered on blockchain B, the signature is verified prior to executing the block, and the message is accessible via the Warp Precompile's
getVerifiedWarpMessage
during the execution of that transaction
Warp Precompileβ
The Warp Precompile is broken down into three functions defined in the Solidity interface file here.
sendWarpMessageβ
sendWarpMessage
is used to send a verifiable message. Calling this function results in sending a message with the following contents:
SourceChainID
- blockchainID of the sourceChain on the Avalanche P-ChainSourceAddress
-msg.sender
encoded as a 32 byte value that callssendWarpMessage
Payload
-payload
argument specified in the call tosendWarpMessage
emitted as the unindexed data of the resulting log
Calling this function will issue a SendWarpMessage
event from the Warp Precompile. Since the EVM limits the number of topics to 4 including the EventID, this message includes only the topics that would be expected to help filter messages emitted from the Warp Precompile the most.
Specifically, the payload
is not emitted as a topic because each topic must be encoded as a hash. Therefore, we opt to take advantage of each possible topic to maximize the possible filtering for emitted Warp Messages.
Additionally, the SourceChainID
is excluded because anyone parsing the chain can be expected to already know the blockchainID. Therefore, the SendWarpMessage
event includes the indexable attributes:
sender
- The
messageID
of the unsigned message (sha256 of the unsigned message)
The actual message
is the entire Avalanche Warp Unsigned Message including an AddressedCall. The unsigned message is emitted as the unindexed data in the log.
getVerifiedMessageβ
getVerifiedMessage
is used to read the contents of the delivered Avalanche Warp Message into the expected format.
It returns the message if present and a boolean indicating if a message is present.
To use this function, the transaction must include the signed Avalanche Warp Message encoded in the predicate of the transaction. Prior to executing a block, the VM iterates through transactions and pre-verifies all predicates. If a transaction's predicate is invalid, then it is considered invalid to include in the block and dropped.
This leads to the following advantages:
- The EVM execution does not need to verify the Warp Message at runtime (no signature verification or external calls to the P-Chain)
- The EVM can deterministically re-execute and re-verify blocks assuming the predicate was verified by the network (eg., in bootstrapping)
This pre-verification is performed using the ProposerVM Block header during block verification and block building.
getBlockchainIDβ
getBlockchainID
returns the blockchainID of the blockchain that the VM is running on.
This is different from the conventional Ethereum ChainID registered to ChainList.
The blockchainID
in Avalanche refers to the txID that created the blockchain on the Avalanche P-Chain (docs).
Predicate Encodingβ
Avalanche Warp Messages are encoded as a signed Avalanche Warp Message where the UnsignedMessage's payload includes an AddressedPayload.
Since the predicate is encoded into the Transaction Access List, it is packed into 32 byte hashes intended to declare storage slots that should be pre-warmed into the cache prior to transaction execution.
Therefore, we use the Predicate Utils package to encode the actual byte slice of size N into the access list.
Performance Optimization: C-Chain to Subnetβ
To support C-Chain to Subnet communication, or more generally Primary Network to Subnet communication, we special case the C-Chain for two reasons:
- Every Subnet validator validates the C-Chain
- The Primary Network has the largest possible number of validators
Since the Primary Network has the largest possible number of validators for any Subnet on Avalanche, it would also be the most expensive Subnet to receive and verify Avalanche Warp Messages from as it reaching a threshold of stake on the primary network would require many signatures. Luckily, we can do something much smarter.
When a Subnet receives a message from a blockchain on the Primary Network, we use the validator set of the receiving Subnet instead of the entire network when validating the message. This means that the C-Chain sending a message can be the exact same as Subnet to Subnet communication.
However, when Subnet B receives a message from the C-Chain, it changes the semantics to the following:
- Read the SourceChainID of the signed message (C-Chain)
- Look up the SubnetID that validates C-Chain: Primary Network
- Look up the validator set of Subnet B (instead of the Primary Network) and the registered BLS Public Keys of Subnet B at the P-Chain height specified by the ProposerVM header
- Continue Warp Message verification using the validator set of Subnet B instead of the Primary Network
This means that C-Chain to Subnet communication only requires a threshold of stake on the receiving subnet to sign the message instead of a threshold of stake for the entire Primary Network.
This assumes that the security of Subnet B already depends on the validators of Subnet B to behave virtuously. Therefore, requiring a threshold of stake from the receiving Subnet's validator set instead of the whole Primary Network does not meaningfully change security of the receiving Subnet.
Note: this special case is ONLY applied during Warp Message verification. The message sent by the Primary Network will still contain the Avalanche C-Chain's blockchainID as the sourceChainID and signatures will be served by querying the C-Chain directly.