warp

package
v0.13.0-rc.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 13, 2024 License: GPL-3.0, LGPL-3.0 Imports: 17 Imported by: 3

README

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:

  1. Call the Warp Precompile sendWarpMessage function with the arguments for the UnsignedMessage
  2. Warp Precompile emits an event / log containing the UnsignedMessage specified by the caller of sendWarpMessage
  3. Network accepts the block containing the UnsignedMessage in the log, so that validators are willing to sign the message
  4. An off-chain relayer queries the validators for their signatures of the message and aggregates the signatures to create a SignedMessage
  5. The off-chain relayer encodes the SignedMessage as the predicate in the AccessList of a transaction to deliver on blockchain B
  6. 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-Chain
  • SourceAddress - msg.sender encoded as a 32 byte value that calls sendWarpMessage
  • Payload - payload argument specified in the call to sendWarpMessage 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:

  1. The EVM execution does not need to verify the Warp Message at runtime (no signature verification or external calls to the P-Chain)
  2. 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:

  1. Every Subnet validator validates the C-Chain
  2. 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:

  1. Read the SourceChainID of the signed message (C-Chain)
  2. Look up the SubnetID that validates C-Chain: Primary Network
  3. 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
  4. 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.

Design Considerations

Re-Processing Historical Blocks

Avalanche Warp Messaging depends on the Avalanche P-Chain state at the P-Chain height specified by the ProposerVM block header.

Verifying a message requires looking up the validator set of the source subnet on the P-Chain. To support this, Avalanche Warp Messaging uses the ProposerVM header, which includes the P-Chain height it was issued at as the canonical point to lookup the source subnet's validator set.

This means verifying the Warp Message and therefore the state transition on a block depends on state that is external to the blockchain itself: the P-Chain.

The Avalanche P-Chain tracks only its current state and reverse diff layers (reversing the changes from past blocks) in order to re-calculate the validator set at a historical height. This means calculating a very old validator set that is used to verify a Warp Message in an old block may become prohibitively expensive.

Therefore, we need a heuristic to ensure that the network can correctly re-process old blocks (note: re-processing old blocks is a requirement to perform bootstrapping and is used in some VMs to serve or verify historical data).

As a result, we require that the block itself provides a deterministic hint which determines which Avalanche Warp Messages were considered valid/invalid during the block's execution. This ensures that we can always re-process blocks and use the hint to decide whether an Avalanche Warp Message should be treated as valid/invalid even after the P-Chain state that was used at the original execution time may no longer support fast lookups.

To provide that hint, we've explored two designs:

  1. Include a predicate in the transaction to ensure any referenced message is valid
  2. Append the results of checking whether a Warp Message is valid/invalid to the block data itself

The current implementation uses option (1).

The original reason for this was that the notion of predicates for precompiles was designed with Shared Memory in mind. In the case of shared memory, there is no canonical "P-Chain height" in the block which determines whether or not Avalanche Warp Messages are valid.

Instead, the VM interprets a shared memory import operation as valid as soon as the UTXO is available in shared memory. This means that if it were up to the block producer to staple the valid/invalid results of whether or not an attempted atomic operation should be treated as valid, a byzantine block producer could arbitrarily report that such atomic operations were invalid and cause a griefing attack to burn the gas of users that attempted to perform an import.

Therefore, a transaction specified predicate is required to implement the shared memory precompile to prevent such a griefing attack.

In contrast, Avalanche Warp Messages are validated within the context of an exact P-Chain height. Therefore, if a block producer attempted to lie about the validity of such a message, the network would interpret that block as invalid.

Guarantees Offered by Warp Precompile vs. Built on Top
Guarantees Offered by Warp Precompile

The Warp Precompile was designed with the intention of minimizing the trusted computing base for the VM as much as possible. Therefore, it makes several tradeoffs which encourage users to use protocols built ON TOP of the Warp Precompile itself as opposed to directly using the Warp Precompile.

The Warp Precompile itself provides ONLY the following ability:

  • Emit a verifiable message from (Address A, Blockchain A) to (Address B, Blockchain B) that can be verified by the destination chain
Explicitly Not Provided / Built on Top

The Warp Precompile itself does not provide any guarantees of:

  • Eventual message delivery (may require re-send on blockchain A and additional assumptions about off-chain relayers and chain progress)
  • Ordering of messages (requires ordering provided a layer above)
  • Replay protection (requires replay protection provided a layer above)

Documentation

Index

Constants

View Source
const (
	WarpDefaultQuorumNumerator uint64 = 67
	WarpQuorumNumeratorMinimum uint64 = 33
	WarpQuorumDenominator      uint64 = 100
)
View Source
const (
	GetVerifiedWarpMessageBaseCost uint64 = 2      // Base cost of entering getVerifiedWarpMessage
	GetBlockchainIDGasCost         uint64 = 2      // Based on GasQuickStep used in existing EVM instructions
	AddWarpMessageGasCost          uint64 = 20_000 // Cost of producing and serving a BLS Signature

	SendWarpMessageGasCost uint64 = contract.LogGas + 3*contract.LogTopicGas + AddWarpMessageGasCost + contract.WriteGasCostPerSlot
	// SendWarpMessageGasCostPerByte cost accounts for producing a signed message of a given size
	SendWarpMessageGasCostPerByte uint64 = contract.LogDataGas

	GasCostPerWarpSigner            uint64 = 500
	GasCostPerWarpMessageBytes      uint64 = 100
	GasCostPerSignatureVerification uint64 = 200_000
)
View Source
const ConfigKey = "warpConfig"

ConfigKey is the key used in json config files to specify this precompile config. must be unique across all precompiles.

Variables

View Source
var (
	// WarpRawABI contains the raw ABI of Warp contract.
	//go:embed contract.abi
	WarpRawABI string

	WarpABI = contract.ParseABI(WarpRawABI)

	WarpPrecompile = createWarpPrecompile()
)

Singleton StatefulPrecompiledContract and signatures.

View Source
var ContractAddress = common.HexToAddress("0x0200000000000000000000000000000000000005")

ContractAddress is the address of the warp precompile contract

View Source
var Module = modules.Module{
	ConfigKey:    ConfigKey,
	Address:      ContractAddress,
	Contract:     WarpPrecompile,
	Configurator: &configurator{},
}

Module is the precompile module. It is used to register the precompile contract.

Functions

func PackGetBlockchainID

func PackGetBlockchainID() ([]byte, error)

PackGetBlockchainID packs the include selector (first 4 func signature bytes). This function is mostly used for tests.

func PackGetBlockchainIDOutput

func PackGetBlockchainIDOutput(blockchainID common.Hash) ([]byte, error)

PackGetBlockchainIDOutput attempts to pack given blockchainID of type common.Hash to conform the ABI outputs.

func PackGetVerifiedWarpBlockHash

func PackGetVerifiedWarpBlockHash(index uint32) ([]byte, error)

PackGetVerifiedWarpBlockHash packs [index] of type uint32 into the appropriate arguments for getVerifiedWarpBlockHash. the packed bytes include selector (first 4 func signature bytes). This function is mostly used for tests.

func PackGetVerifiedWarpBlockHashOutput

func PackGetVerifiedWarpBlockHashOutput(outputStruct GetVerifiedWarpBlockHashOutput) ([]byte, error)

PackGetVerifiedWarpBlockHashOutput attempts to pack given [outputStruct] of type GetVerifiedWarpBlockHashOutput to conform the ABI outputs.

func PackGetVerifiedWarpMessage

func PackGetVerifiedWarpMessage(index uint32) ([]byte, error)

PackGetVerifiedWarpMessage packs [index] of type uint32 into the appropriate arguments for getVerifiedWarpMessage. the packed bytes include selector (first 4 func signature bytes). This function is mostly used for tests.

func PackGetVerifiedWarpMessageOutput

func PackGetVerifiedWarpMessageOutput(outputStruct GetVerifiedWarpMessageOutput) ([]byte, error)

PackGetVerifiedWarpMessageOutput attempts to pack given [outputStruct] of type GetVerifiedWarpMessageOutput to conform the ABI outputs.

func PackSendWarpMessage

func PackSendWarpMessage(payloadData []byte) ([]byte, error)

PackSendWarpMessage packs [inputStruct] of type []byte into the appropriate arguments for sendWarpMessage.

func PackSendWarpMessageEvent

func PackSendWarpMessageEvent(sourceAddress common.Address, unsignedMessageID common.Hash, unsignedMessageBytes []byte) ([]common.Hash, []byte, error)

PackSendWarpMessageEvent packs the given arguments into SendWarpMessage events including topics and data.

func PackSendWarpMessageOutput

func PackSendWarpMessageOutput(messageID common.Hash) ([]byte, error)

PackSendWarpMessageOutput attempts to pack given messageID of type common.Hash to conform the ABI outputs.

func UnpackGetVerifiedWarpBlockHashInput

func UnpackGetVerifiedWarpBlockHashInput(input []byte) (uint32, error)

UnpackGetVerifiedWarpBlockHashInput attempts to unpack [input] into the uint32 type argument assumes that [input] does not include selector (omits first 4 func signature bytes)

func UnpackGetVerifiedWarpMessageInput

func UnpackGetVerifiedWarpMessageInput(input []byte) (uint32, error)

UnpackGetVerifiedWarpMessageInput attempts to unpack [input] into the uint32 type argument assumes that [input] does not include selector (omits first 4 func signature bytes)

func UnpackSendWarpEventDataToMessage

func UnpackSendWarpEventDataToMessage(data []byte) (*warp.UnsignedMessage, error)

UnpackSendWarpEventDataToMessage attempts to unpack event [data] as warp.UnsignedMessage.

func UnpackSendWarpMessageInput

func UnpackSendWarpMessageInput(input []byte) ([]byte, error)

UnpackSendWarpMessageInput attempts to unpack [input] as []byte assumes that [input] does not include selector (omits first 4 func signature bytes)

func UnpackSendWarpMessageOutput

func UnpackSendWarpMessageOutput(output []byte) (common.Hash, error)

UnpackSendWarpMessageOutput attempts to unpack given [output] into the common.Hash type output assumes that [output] does not include selector (omits first 4 func signature bytes)

Types

type Config

type Config struct {
	precompileconfig.Upgrade
	QuorumNumerator uint64 `json:"quorumNumerator"`
}

Config implements the precompileconfig.Config interface and adds specific configuration for Warp.

func NewConfig

func NewConfig(blockTimestamp *uint64, quorumNumerator uint64) *Config

NewConfig returns a config for a network upgrade at [blockTimestamp] that enables Warp with the given quorum numerator.

func NewDefaultConfig

func NewDefaultConfig(blockTimestamp *uint64) *Config

NewDefaultConfig returns a config for a network upgrade at [blockTimestamp] that enables Warp with the default quorum numerator (0 denotes using the default).

func NewDisableConfig

func NewDisableConfig(blockTimestamp *uint64) *Config

NewDisableConfig returns config for a network upgrade at [blockTimestamp] that disables Warp.

func (*Config) Accept

func (c *Config) Accept(acceptCtx *precompileconfig.AcceptContext, blockHash common.Hash, blockNumber uint64, txHash common.Hash, logIndex int, topics []common.Hash, logData []byte) error

func (*Config) Equal

func (c *Config) Equal(s precompileconfig.Config) bool

Equal returns true if [s] is a *Config and it has been configured identical to [c].

func (*Config) Key

func (*Config) Key() string

Key returns the key for the Warp precompileconfig. This should be the same key as used in the precompile module.

func (*Config) PredicateGas

func (c *Config) PredicateGas(predicateBytes []byte) (uint64, error)

PredicateGas returns the amount of gas necessary to verify the predicate PredicateGas charges for: 1. Base cost of the message 2. Size of the message 3. Number of signers 4. TODO: Lookup of the validator set

If the payload of the warp message fails parsing, return a non-nil error invalidating the transaction.

func (*Config) Verify

func (c *Config) Verify(chainConfig precompileconfig.ChainConfig) error

Verify tries to verify Config and returns an error accordingly.

func (*Config) VerifyPredicate

func (c *Config) VerifyPredicate(predicateContext *precompileconfig.PredicateContext, predicateBytes []byte) error

VerifyPredicate returns whether the predicate described by [predicateBytes] passes verification.

type GetVerifiedWarpBlockHashOutput

type GetVerifiedWarpBlockHashOutput struct {
	WarpBlockHash WarpBlockHash
	Valid         bool
}

func UnpackGetVerifiedWarpBlockHashOutput

func UnpackGetVerifiedWarpBlockHashOutput(output []byte) (GetVerifiedWarpBlockHashOutput, error)

UnpackGetVerifiedWarpBlockHashOutput attempts to unpack [output] as GetVerifiedWarpBlockHashOutput assumes that [output] does not include selector (omits first 4 func signature bytes)

type GetVerifiedWarpMessageOutput

type GetVerifiedWarpMessageOutput struct {
	Message WarpMessage
	Valid   bool
}

func UnpackGetVerifiedWarpMessageOutput

func UnpackGetVerifiedWarpMessageOutput(output []byte) (GetVerifiedWarpMessageOutput, error)

UnpackGetVerifiedWarpMessageOutput attempts to unpack [output] as GetVerifiedWarpMessageOutput assumes that [output] does not include selector (omits first 4 func signature bytes)

type SendWarpMessageEventData

type SendWarpMessageEventData struct {
	Message []byte
}

type WarpBlockHash

type WarpBlockHash struct {
	SourceChainID common.Hash
	BlockHash     common.Hash
}

WarpBlockHash is an auto generated low-level Go binding around an user-defined struct.

type WarpMessage

type WarpMessage struct {
	SourceChainID       common.Hash
	OriginSenderAddress common.Address
	Payload             []byte
}

WarpMessage is an auto generated low-level Go binding around an user-defined struct.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL