logoAcademy

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.

  1. The function GetState takes an address and a Hash (the storage key) as inputs and returns the Hash stored at that slot.
  2. 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
}

On this page