Virtual Machines

Understand how Virtual Machines (VMs) define blockchain behavior in AvalancheGo, including the VM interface and built-in VMs.

A Virtual Machine (VM) defines the application-level logic of a blockchain. In AvalancheGo, VMs are decoupled from consensus, allowing developers to create custom blockchain behavior while reusing the battle-tested consensus layer.

What is a Virtual Machine?

Think of a VM as the "personality" of a blockchain:

AspectWhat the VM Defines
StateWhat data the blockchain stores
TransactionsValid operations and their effects
BlocksHow transactions are packaged
APIsHow users interact with the chain
ValidationRules for accepting blocks

VMs are reusable. Multiple blockchains can run the same VM, each with independent state. This is similar to how a class can have multiple instances in object-oriented programming.

VM Architecture

Core VM Interface

Every VM must implement the ChainVM interface for linear chain consensus:

snow/engine/snowman/block/vm.go
type ChainVM interface {
    common.VM

    // Block building
    BuildBlock(ctx context.Context) (snowman.Block, error)

    // Block retrieval
    GetBlock(ctx context.Context, blkID ids.ID) (snowman.Block, error)
    ParseBlock(ctx context.Context, blockBytes []byte) (snowman.Block, error)

    // Consensus integration
    SetPreference(ctx context.Context, blkID ids.ID) error
    LastAccepted(ctx context.Context) (ids.ID, error)

    // Optional: height-indexed access
    GetBlockIDAtHeight(ctx context.Context, height uint64) (ids.ID, error)
}

Base VM Interface

The foundation interface that all VMs implement (source):

snow/engine/common/vm.go
type VM interface {
    common.AppHandler          // AppRequest/AppResponse/AppGossip hooks
    health.Checker             // Exposed via /ext/health
    validators.Connector       // Called on peer connect/disconnect

    // Lifecycle
    Initialize(ctx context.Context, chainCtx *snow.Context,
        db database.Database, genesisBytes []byte,
        upgradeBytes []byte, configBytes []byte,
        fxs []*common.Fx, appSender common.AppSender) error
    SetState(ctx context.Context, state snow.State) error
    Shutdown(ctx context.Context) error

    // Info
    Version(ctx context.Context) (string, error)

    // APIs
    CreateHandlers(ctx context.Context) (map[string]http.Handler, error)
    NewHTTPHandler(ctx context.Context) (http.Handler, error)

    // Engine notifications (PendingTxs, etc.)
    WaitForEvent(ctx context.Context) (common.Message, error)
}

Block Interface

Blocks are the fundamental unit of consensus (source):

snow/consensus/snowman/block.go
type Block interface {
    // ID(), Accept(), Reject(), Status() come from snow.Decidable
    snow.Decidable

    // Identity
    Parent() ids.ID
    Height() uint64
    Timestamp() time.Time
    Bytes() []byte

    // Validation
    Verify(context.Context) error
}

Block Lifecycle

Block Status

type Status int

const (
    Unknown Status = iota
    Processing
    Rejected
    Accepted
)

Built-in Virtual Machines

Platform VM (P-Chain)

The Platform VM (vms/platformvm) manages the Avalanche network itself:

vms/platformvm/vm.go
type VM struct {
    // State management
    state        state.State
    atomicUTXOs  atomic.SharedMemory

    // Block building
    Builder      blockbuilder.Builder
    Network      network.Network

    // Validators
    Validators   validators.Manager
}

Responsibilities:

  • Validator Management: Add/remove validators, track stake
  • Subnet Creation: Create new validator sets
  • Chain Creation: Launch new blockchains
  • Staking: Manage delegation and rewards
  • Warp Messaging: Sign cross-chain messages

Key Transaction Types:

TransactionPurposeEra
AddValidatorTxAdd a Primary Network validatorApricot
AddDelegatorTxDelegate stake to a validatorApricot
CreateSubnetTxCreate a new subnetApricot
CreateChainTxLaunch a blockchain on a subnetApricot
ImportTx / ExportTxCross-chain asset transfersApricot
AddPermissionlessValidatorTxAdd validator to permissionless subnetBanff
AddPermissionlessDelegatorTxDelegate to permissionless validatorBanff
TransformSubnetTxConvert subnet to permissionlessBanff
TransferSubnetOwnershipTxTransfer subnet ownershipDurango
ConvertSubnetToL1TxConvert subnet to Avalanche L1Etna
RegisterL1ValidatorTxRegister validator on L1Etna
SetL1ValidatorWeightTxSet L1 validator weightEtna
IncreaseL1ValidatorBalanceTxAdd balance to L1 validatorEtna
DisableL1ValidatorTxDisable an L1 validatorEtna

The Etna upgrade introduced 5 new transaction types for managing Avalanche L1s. These enable converting subnets to sovereign L1s with their own validator management.

AVM (X-Chain)

The AVM (vms/avm) handles asset creation and transfers using the UTXO model:

The X-Chain was linearized in the Cortina upgrade (April 2023). It now uses Snowman consensus like the P-Chain and C-Chain, rather than the legacy Avalanche DAG consensus. The AVM implements the LinearizableVMWithEngine interface to support this transition.

vms/avm/vm.go
type VM struct {
    // Asset management
    fxs     []*Fx
    state   state.State

    // UTXO handling
    utxoSet atomic.SharedMemory
}

Features:

  • Multi-Asset Support: Native AVAX and custom assets
  • UTXO Model: Bitcoin-style transaction inputs/outputs
  • Snowman Consensus: Linear chain consensus (linearized from DAG in Cortina upgrade)
  • Feature Extensions (Fxs): Pluggable transaction types
    • secp256k1fx: Standard signatures
    • nftfx: Non-fungible tokens
    • propertyfx: Property ownership

Transaction Types:

TransactionPurpose
CreateAssetTxCreate a new asset
OperationTxMint/burn assets
BaseTxTransfer assets
ImportTxImport from other chains
ExportTxExport to other chains

Coreth (C-Chain)

Coreth is the EVM implementation for the C-Chain:

Coreth is grafted into the AvalancheGo repository at graft/coreth/. The standalone ava-labs/coreth repository has been archived and is now read-only. All active development occurs within the AvalancheGo monorepo.

Features:

  • Full Ethereum Virtual Machine compatibility
  • Supports Solidity smart contracts
  • Web3 JSON-RPC API (eth, personal, txpool, debug namespaces)
  • EIP-1559 dynamic fees
  • Atomic transactions with other Avalanche chains (via shared memory)
  • Support for Ethereum upgrades through Cancun

ProposerVM (Snowman++)

The ProposerVM (vms/proposervm) wraps other VMs to add Snowman++ proposer windows:

vms/proposervm/vm.go
type VM struct {
    // Wrapped VM
    ChainVM block.ChainVM

    // Proposer selection
    windower proposer.Windower

    // Block timing
    MinBlockDelay time.Duration
}

How it Works:

  1. Stake-weighted proposers are sampled for each height.
  2. Each proposer has a 5s slot (up to 6 slots) counted from the parent timestamp.
  3. Only the active proposer can build during its slot; after the final slot any validator may build.
  4. The wrapper enforces proposer signatures/timestamps before issuing to consensus.

Snowman++ is enabled on all Primary Network chains (P-Chain, C-Chain, X-Chain) to pace block production without sacrificing liveness.

Custom VMs

You can build custom VMs that run on Avalanche L1s. There are two approaches:

1. Native Go VM

Implement the ChainVM interface directly in Go:

type MyVM struct {
    db      database.Database
    state   *MyState
    builder *BlockBuilder
    pending <-chan struct{}
}

func (vm *MyVM) Initialize(ctx context.Context, ...) error {
    // Set up database and state
    vm.db = db
    vm.state = NewState(db)
    vm.pending = vm.builder.Subscribe() // emits when there are pending txs
    return nil
}

func (vm *MyVM) WaitForEvent(ctx context.Context) (common.Message, error) {
    select {
    case <-ctx.Done():
        return 0, ctx.Err()
    case <-vm.pending:
        return common.PendingTxs, nil
    }
}

func (vm *MyVM) BuildBlock(ctx context.Context) (snowman.Block, error) {
    // Collect pending transactions
    txs := vm.builder.GetPendingTxs()
    // Create new block
    return NewBlock(vm.state.LastAccepted(), txs), nil
}

2. RPC VM (Any Language)

Use the rpcchainvm interface to build VMs in any language:

Benefits:

  • Write VMs in Rust, TypeScript, etc.
  • Process isolation
  • Independent deployment

Protocol:

// gRPC service definition
service VM {
    rpc Initialize(InitializeRequest) returns (InitializeResponse);
    rpc BuildBlock(BuildBlockRequest) returns (BuildBlockResponse);
    rpc ParseBlock(ParseBlockRequest) returns (ParseBlockResponse);
    rpc GetBlock(GetBlockRequest) returns (GetBlockResponse);
    // ... more methods
}

VM Registration

VMs are registered with the node at startup:

node/node.go
func (n *Node) initVMs() error {
    // Built-in VMs
    n.VMManager.RegisterFactory(ctx, constants.PlatformVMID,
        &platformvm.Factory{})
    n.VMManager.RegisterFactory(ctx, constants.AVMID,
        &avm.Factory{})
    n.VMManager.RegisterFactory(ctx, constants.EVMID,
        &coreth.Factory{})

    // Plugin VMs are discovered from the plugins directory
    // (default: ~/.avalanchego/plugins/)
}

Plugin Discovery:

  1. VMs are placed in ~/.avalanchego/plugins/
  2. Filename is the VM ID (e.g., srEXiWaHuhNyGwPUi444Tu47ZEDwxTWrbQiuD7FmgSAQ6X7Dy)
  3. Node discovers and loads plugins at startup

VM Configuration

Each chain can have custom VM configuration:

~/.avalanchego/configs/chains/{chainID}/config.json
{
  "pruning-enabled": true,
  "state-sync-enabled": true,
  "eth-apis": ["eth", "eth-filter", "net", "web3"]
}

Chain-Specific Configs:

  • Stored in ~/.avalanchego/configs/chains/{chainID}/
  • config.json: VM configuration
  • upgrade.json: Upgrade coordination

Best Practices

State Management

// Use versioned database for atomic commits
func (vm *VM) Accept(ctx context.Context, blk *Block) error {
    batch := vm.db.NewBatch()
    defer batch.Reset()

    // Apply state changes
    for _, tx := range blk.Txs {
        if err := vm.state.Apply(batch, tx); err != nil {
            return err
        }
    }

    // Commit atomically
    return batch.Write()
}

Block Building

// Drive block production: return PendingTxs when mempool has work
func (vm *VM) WaitForEvent(ctx context.Context) (common.Message, error) {
    select {
    case <-ctx.Done():
        return 0, ctx.Err()
    case <-vm.pending:
        return common.PendingTxs, nil
    }
}

API Design

func (vm *VM) CreateHandlers(ctx context.Context) (map[string]http.Handler, error) {
    return map[string]http.Handler{
        "/rpc":    vm.newJSONRPCHandler(),
        "/ws":     vm.newWebSocketHandler(),
        "/health": vm.newHealthHandler(),
    }, nil
}

Next Steps

Is this guide helpful?