logoDocumentation

Simple VM in Any Language

Learn how to implement a simple virtual machine in any language.

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 sequence.

  1. VM.Version
    • Return: your VM's version.
  2. VM.CreateStaticHandler
    • Return: an empty array - (Not absolutely required).
  3. VM.Shutdown
    • You should gracefully stop your process.
    • Return: Empty

Initialization Sequence

  1. VM.CreateStaticHandlers
    • Return an empty array - (Not absolutely required).
  2. VM.Initialize
    • Param: an InitializeRequest.
    • You must use this data to initialize your VM.
    • You should add the genesis block to your blockchain and set it as the last accepted block.
    • Return: an InitializeResponse containing data about the genesis extracted from the genesis_bytes that was sent in the request.
  3. VM.VerifyHeightIndex
  4. VM.CreateHandlers
    • To serve json-RPC endpoint, /ext/bc/subnetId/rpc exposed by AvalancheGo
    • See json-RPC for more detail
    • Create a Http server and get its url.
    • Return: a CreateHandlersResponse containing a single item with the server's url. (or an empty array if not implementing the json-RPC endpoint)
  5. VM.StateSyncEnabled
    • Return: true if you want to enable StateSync, false otherwise.
  6. VM.SetState If you had specified true in the StateSyncEnabled result
    • Param: a SetStateRequest with the StateSyncing value
    • Set your blockchain's state to StateSyncing
    • Return: a SetStateResponse built from the genesis block.
  7. VM.GetOngoingSyncStateSummary If you had specified true in the StateSyncEnabled result
  8. VM.SetState
    • Param: a SetStateRequest with the Bootstrapping value
    • Set your blockchain's state to Bootstrapping
    • Return: a SetStateResponse built from the genesis block.
  9. VM.SetPreference
    • Param: SetPreferenceRequest containing the preferred block ID
    • Return: Empty
  10. VM.SetState
  11. VM.Connected (for every other node validating this Subnet in the network)
    • Param: a ConnectedRequest with the NodeID and the version of AvalancheGo.
    • Return: Empty
  12. VM.Health
    • Param: Empty
    • Return: a HealthResponse with an empty details property.
  13. VM.ParseBlock
    • Param: A byte array containing a Block (the genesis block in this case)
    • Return: a ParseBlockResponse built from the last accepted block.

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. Let's say NodeX has received a new transaction on it's json-RPC endpoint.

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:

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:

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

JSON-RPC

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

  • 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.

Last updated on

On this page

Edit on Github