What are Stateful Precompiles?
Learn about Stateful Precompiles in Avalanche L1 EVM.
When building the MD5 and Calculator precompiles, we emphasized their behavior. We focused on building precompiles that developers could call in Solidity to perform some algorithm and then simply return a result.
However, one aspect that we have yet to explore is the statefulness of precompiles. Simply put, precompiles can store data which is persistent. To understand how this is possible, recall the interface that our precompile needed to implement:
// StatefulPrecompiledContract is the interface for executing a precompiled contract
type StatefulPrecompiledContract interface {
// Run executes the precompiled contract.
Run(accessibleState AccessibleState,
caller common.Address,
addr common.Address,
input []byte,
suppliedGas uint64,
readOnly bool)
(ret []byte, remainingGas uint64, err error)
}
We also examined all parameters except for the AccessibleState
parameter. As the name suggests, this parameter lets us access the blockchain's state. Looking at the interface of AccessibleState
, we have the following:
// AccessibleState defines the interface exposed to stateful precompile contracts
type AccessibleState interface {
GetStateDB() StateDB
GetBlockContext() BlockContext
GetSnowContext() *snow.Context
}
Looking closer, we see that AccessibleState
gives us access to StateDB, which is used to store the state of the EVM. However, as we will see throughout this section, AccessibleState
also gives us access to other useful parameters, such as BlockContext
and snow.Context
.
StateDB
The parameter we will use the most when it comes to stateful precompiles, StateDB, is a key-value mapping that maps:
- Key: a tuple consisting of an address and the storage key of the type Hash
- Value: any data encoded in a Hash, also called a word in the EVM
A Hash in go-ethereum is a 32-byte array. In this context, we do not refer to hashing in the cryptographic sense. Rather, "hashing a value" means encoding it to a hash, a 32-byte array usually represented in hexadecimal digits in Ethereum.
const (
// HashLength is the expected length of the hash
HashLength = 32
)
// Hash represents the 32 byte of arbitrary data.
type Hash [HashLength]byte
// Example of a data encoded in a Hash:
// 0x00000000000000000000000000000000000000000048656c6c6f20576f726c64
Below is the interface of StateDB:
// StateDB is the interface for accessing EVM state
type StateDB interface {
GetState(common.Address, common.Hash) common.Hash
SetState(common.Address, common.Hash, common.Hash)
SetNonce(common.Address, uint64)
GetNonce(common.Address) uint64
GetBalance(common.Address) *big.Int
AddBalance(common.Address, *big.Int)
CreateAccount(common.Address)
Exist(common.Address) bool
AddLog(addr common.Address, topics []common.Hash, data []byte, blockNumber uint64)
GetPredicateStorageSlots(address common.Address) ([]byte, bool)
Suicide(common.Address) bool
Finalise(deleteEmptyObjects bool)
Snapshot() int
RevertToSnapshot(int)
}
As you can see in the interface of the StateDB, the two functions for writing to and reading from the EVM state all work with the Hash type.
- The function GetState takes an address and a Hash (the storage key) as inputs and returns the Hash stored at that slot.
- The function SetState takes an address, a Hash (the storage key), and another Hash (data to be stored) as inputs.
We can also see that the StateDB interface allows us to read and write account balances of the native token (via GetBalance/SetBalance), check whether an account exists (via Exist), and some other methods.
BlockContext
BlockContext
provides info about the current block. In particular, we can get the current block number and the current block timestamp. Below is the interface of BlockContext
:
// BlockContext defines an interface that provides information to a stateful precompile about the current block.
// The BlockContext may be provided during both precompile activation and execution.
type BlockContext interface {
Number() *big.Int
Timestamp() *big.Int
}
An example of how we could leverage BlockContext
in precompiles: building a voting precompile that only accepts votes within a certain time frame.
snow.Context
snow.Context
gives us info regarding the environment of the precompile. In particular, snow.Context
tells us about the state of the network the precompile is hosted on. Below is the interface of snow.Context
:
// Context is information about the current execution.
// [NetworkID] is the ID of the network this context exists within.
// [ChainID] is the ID of the chain this context exists within.
// [NodeID] is the ID of this node
type Context struct {
NetworkID uint32
Avalanche L1ID ids.ID
ChainID ids.ID
NodeID ids.NodeID
PublicKey *bls.PublicKey
XChainID ids.ID
CChainID ids.ID
AVAXAssetID ids.ID
Log logging.Logger
Lock sync.RWMutex
Keystore keystore.BlockchainKeystore
SharedMemory atomic.SharedMemory
BCLookup ids.AliaserReader
Metrics metrics.OptionalGatherer
WarpSigner warp.Signer
// snowman++ attributes
ValidatorState validators.State // interface for P-Chain validators
// Chain-specific directory where arbitrary data can be written
ChainDataDir string
}
Last updated on 3/7/2025