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:
vm.Runtime
(Client)vm.VM
(Server)
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:
Http
(Server)
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.
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 thesupported 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 formathost:port
(examplelocalhost: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.
- VM.Version
- Return: your VM's version.
- VM.CreateStaticHandler
- Return: an empty array - (Not absolutely required).
- VM.Shutdown
- You should gracefully stop your process.
- Return: Empty
Initialization Sequence
- VM.CreateStaticHandlers
- Return an empty array - (Not absolutely required).
- 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.
- VM.VerifyHeightIndex
- Return: a VerifyHeightIndexResponse with the code
ERROR_UNSPECIFIED
to indicate that no error has occurred.
- Return: a VerifyHeightIndexResponse with the code
- VM.CreateHandlers
- VM.StateSyncEnabled
- Return:
true
if you want to enable StateSync,false
otherwise.
- Return:
- VM.SetState If you had specified
true
in theStateSyncEnabled
result- Param: a SetStateRequest with the
StateSyncing
value - Set your blockchain's state to
StateSyncing
- Return: a SetStateResponse built from the genesis block.
- Param: a SetStateRequest with the
- VM.GetOngoingSyncStateSummary If you had specified
true
in theStateSyncEnabled
result- Return: a GetOngoingSyncStateSummaryResponse built from the genesis block.
- VM.SetState
- Param: a SetStateRequest with the
Bootstrapping
value - Set your blockchain's state to
Bootstrapping
- Return: a SetStateResponse built from the genesis block.
- Param: a SetStateRequest with the
- VM.SetPreference
- Param:
SetPreferenceRequest
containing the preferred block ID - Return: Empty
- Param:
- VM.SetState
- Param: a SetStateRequest with the
NormalOp
value - Set your blockchain's state to
NormalOp
- Return: a SetStateResponse built from the genesis block.
- Param: a SetStateRequest with the
- 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
- VM.Health
- Param: Empty
- Return: a HealthResponse with an empty
details
property.
- 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.
Supposing that AvalancheGo accepts block 0x123...
. The following RPC methods are called on all nodes:
- VM.BlockAccept: You must accept this block as your last final block.
- Param: The block's ID (
0x123...
) - Return: Empty
- Param: The block's ID (
- VM.BlockReject: You must mark this block as rejected.
- Param: The block's ID (
0x321...
) - Return: Empty
- Param: The block's ID (
- VM.BlockReject: You must mark this block as rejected.
- Param: The block's ID (
0x456...
) - Return: Empty
- Param: The block's ID (
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