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 :
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
- 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
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.
- You must serialize your transaction data into a byte array and call the
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
- client
Messenger.Notify
- You must issue a notify request to AvalancheGo by calling the method with the
MESSAGE_BUILD_BLOCK
value.
- You must issue a notify request to AvalancheGo by calling the method with the
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
- Param : The block's ID (
- VM.BlockReject
- Param : The block's ID (
0x321...
) - You must mark this block as rejected.
- Return : Empty
- Param : The block's ID (
- VM.BlockReject
- Param : The block's ID (
0x456...
) - You must mark this block as rejected.
- 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.
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.
Was this page helpful?