Skip to main content

How to Build a Simple VM From Scratch

This is a language-agnostic high-level documentation explaining the basics of how to get started at implementing your own virtual machine from scratch.

Avalanche virtual machines are grpc servers implementing Avalanche's Proto interfaces. This means that it can be done in any language that has a grpc implementation.

Minimal Implementation

To get the process started, at the minimum, you will to implement the following interfaces :

To build a blockchain taking advantage of AvalancheGo's consensus to build blocks, you will need to implement :

To have a json-RPC endpoint, /ext/bc/subnetId/rpc exposed by AvalancheGo, you will need to implement :

You can and should use a tool like buf to generate the (Client/Server) code from the interfaces as stated in the Avalanche module's page.

note

There are server and client interfaces to implement. AvalancheGo calls the server interfaces exposed by your VM and your VM calls the client interfaces exposed by AvalancheGo.

Starting Process

Your VM is started by AvalancheGo launching your binary. Your binary is started as a sub-process of AvalancheGo. While launching your binary, AvalancheGo passes an environment variable AVALANCHE_VM_RUNTIME_ENGINE_ADDR containing an url. We must use this url to initialize a vm.Runtime client.

Your VM, after having started a grpc server implementing the VM interface must call the vm.Runtime.InitializeRequest with the following parameters.

  • protocolVersion : It must match the supported plugin version of the AvalancheGo release you are using. It is always part of the release notes.

  • addr : It is your grpc server's address. It must be in the following format host:port (example localhost:12345)

VM Initialization

The service methods are described in the same order as they are called. You will need to implement these methods in your server.

Pre-Initialization Sequence

AvalancheGo starts/stops your process multiple times before launching the real initialization

Initialization Sequence

At this point, your VM is fully started and initialized.

Building Blocks

Transaction Gossiping Sequence

When your VM receives transactions (for example using the json-RPC endpoints), it can gossip them to the other nodes by using the AppSender service.

Supposing we have a 3 nodes network with nodeX, nodeY, nodeZ

NodeX has received a new transaction on it's json-RPC endpoint : on nodeX

  • AppSender.SendAppGossip (client)
    • You must serialize your transaction data into a byte array and call the SendAppGossip to propagate the transaction.

AvalancheGo then propagates this to the other nodes.

on nodeY and nodeZ

  • VM.AppGossip
    • Param : A byte array containing your transaction data, and the NodeID of the node which sent the gossip message.
    • You must deserialize the transaction and store it for the next block.
    • Return : Empty

Block Building Sequence

Whenever your VM is ready to build a new block, it will initiate the block building process by using the Messenger service. Supposing that nodeY wants to build the block. you probably will implement some kind of background worker checking every second if there are any pending transactions :

on nodeY

  • clientMessenger.Notify
    • You must issue a notify request to AvalancheGo by calling the method with the MESSAGE_BUILD_BLOCK value.

on nodeY

  • VM.BuildBlock
    • Param : Empty
    • You must build a block with your pending transactions. Serialize it to a byte array.
    • Store this block in memory as a "pending blocks"
    • Return : a BuildBlockResponse from the newly built block and it's associated data (id, parent_id, height, timestamp).
  • VM.BlockVerify
    • Param : The byte array containing the block data
    • Return : the block's timestamp
  • VM.SetPreference
    • Param : The block's ID
    • You must mark this block as the next preferred block.
    • Return : Empty

on nodeX and nodeZ

  • VM.ParseBlock
    • Param : A byte array containing a the newly built block's data
    • Store this block in memory as a "pending blocks"
    • Return : a ParseBlockResponse built from the last accepted block.
  • VM.BlockVerify
    • Param : The byte array containing the block data
    • Return : the block's timestamp
  • VM.SetPreference
    • Param : The block's ID
    • You must mark this block as the next preferred block.
    • Return : Empty

on all nodes

  • VM.BlockAccept
    • Param : The block's ID
    • You must accept this block as your last final block.
    • Return : Empty

Managing Conflicts

Conflicts happen when two or more nodes propose the next block at the same time. AvalancheGo takes care of this and decides which block should be considered final, and which blocks should be rejected using Snowman consensus. On the VM side, all there is to do is implement the VM.BlockAccept and VM.BlockReject methods.

nodeX proposes block 0x123..., nodeY proposes block 0x321... and nodeZ proposes block 0x456

There are three conflicting blocks (different hashes), and if we look at our VM's log files, we can see that AvalancheGo uses Snowman to decide which block must be accepted.

...
... snowman/voter.go:58 filtering poll results ...
... snowman/voter.go:65 finishing poll ...
... snowman/voter.go:87 Snowman engine can't quiesce
...
... snowman/voter.go:58 filtering poll results ...
... snowman/voter.go:65 finishing poll ...
... snowman/topological.go:600 accepting block
...

Supposing that AvalancheGo accepts block 0x123.... The following RPC methods are called on all nodes :

  • VM.BlockAccept
    • Param : The block's ID (0x123...)
    • You must accept this block as your last final block.
    • Return : Empty
  • VM.BlockReject
    • Param : The block's ID (0x321...)
    • You must mark this block as rejected.
    • Return : Empty
  • VM.BlockReject
    • Param : The block's ID (0x456...)
    • You must mark this block as rejected.
    • Return : Empty

json-RPC

To enable your json-RPC endpoint, you must implement the HandleSimple method of the Http interface.

  • Http.HandleSimple
    • Param : a HandleSimpleHTTPRequest containing the original request's method, url, headers, and body.

    • Analyze, deserialize and handle the request

      for example, if the request represents a transaction, we must deserialize it, check the signature, store it and gossip it to the other nodes using the messenger client)

    • Return the HandleSimpleHTTPResponse response that will be sent back to the original sender.

This server is registered with AvalancheGo during the initialization process when the VM.CreateHandlers method is called. You must simply respond with the server's url in the CreateHandlersResponse result.