Simple Golang VM
In this tutorial, we will learn how to build a virtual machine by referencing the TimestampVM.
In this tutorial, we'll create a very simple VM called the TimestampVM. Each block in the TimestampVM's blockchain contains a strictly increasing timestamp when the block was created and a 32-byte payload of data.
Such a server is useful because it can be used to prove a piece of data existed at the time the block was created. Suppose you have a book manuscript, and you want to be able to prove in the future that the manuscript exists today. You can add a block to the blockchain where the block's payload is a hash of your manuscript. In the future, you can prove that the manuscript existed today by showing that the block has the hash of your manuscript in its payload (this follows from the fact that finding the pre-image of a hash is impossible).
TimestampVM Implementation
Now we know the interface our VM must implement and the libraries we can use to build a VM.
Let's write our VM, which implements block.ChainVM
and whose blocks implement snowman.Block
. You can also follow the code in the TimestampVM repository.
Codec
Codec
is required to encode/decode the block into byte representation. TimestampVM uses the default codec and manager.
State
The State
interface defines the database layer and connections. Each VM should define their own database methods. State
embeds the BlockState
which defines block-related state operations.
Block State
This interface and implementation provides storage functions to VM to store and retrieve blocks.
Block
Let's look at our block implementation. The type declaration is:
The serialize:"true"
tag indicates that the field should be included in the byte representation of the block used when persisting the block or sending it to other nodes.
Verify
This method verifies that a block is valid and stores it in the memory. It is important to store the verified block in the memory and return them in the vm.GetBlock
method.
Accept
Accept
is called by the consensus to indicate this block is accepted.
Reject
Reject
is called by the consensus to indicate this block is rejected.
Block Field Methods
These methods are required by the snowman.Block
interface.
Helper Functions
These methods are convenience methods for blocks, they're not a part of the block interface.
Virtual Machine
Now, let's look at our timestamp VM implementation, which implements the block.ChainVM
interface. The declaration is:
Initialize
This method is called when a new instance of VM is initialized. Genesis block is created under this method.
initGenesis
initGenesis
is a helper method which initializes the genesis block from given bytes and puts into the state.
CreateHandlers
Registered handlers defined in Service
. See below for more on APIs.
CreateStaticHandlers
Registers static handlers defined in StaticService
. See below for more on static APIs.
BuildBock
BuildBlock
builds a new block and returns it. This is mainly requested by the consensus engine.
NotifyBlockReady
NotifyBlockReady
is a helper method that can send messages to the consensus engine through toEngine
channel.
GetBlock
GetBlock
returns the block with the given block ID.
proposeBlock
This method adds a piece of data to the mempool and notifies the consensus layer of the blockchain that a new block is ready to be built and voted on. This is called by API method ProposeBlock
, which we'll see later.
ParseBlock
Parse a block from its byte representation.
NewBlock
NewBlock
creates a new block with given block parameters.
SetPreference
SetPreference
implements the block.ChainVM
. It sets the preferred block ID.
Other Functions
These functions needs to be implemented for block.ChainVM
. Most of them are just blank functions returning nil
.
Factory
VMs should implement the Factory
interface. New
method in the interface returns a new VM instance.
Static API
A VM may have a static API, which allows clients to call methods that do not query or update the state of a particular blockchain, but rather apply to the VM as a whole. This is analogous to static methods in computer programming. AvalancheGo uses Gorilla's RPC library to implement HTTP APIs. StaticService
implements the static API for our VM.
Encode
For each API method, there is:
- A struct that defines the method's arguments
- A struct that defines the method's return values
- A method that implements the API method, and is parameterized on the above 2 structs
This API method encodes a string to its byte representation using a given encoding scheme. It can be used to encode data that is then put in a block and proposed as the next block for this chain.
Decode
This API method is the inverse of Encode
.
API
A VM may also have a non-static HTTP API, which allows clients to query and update the blockchain's state. Service
's declaration is:
Note that this struct has a reference to the VM, so it can query and update state.
This VM's API has two methods. One allows a client to get a block by its ID. The other allows a client to propose the next block of this blockchain. The blockchain ID in the endpoint changes, since every blockchain has an unique ID.
timestampvm.getBlock
Get a block by its ID. If no ID is provided, get the latest block.
getBlock
Signature
id
is the ID of the block being retrieved. If omitted from arguments, gets the latest blockdata
is the base 58 (with checksum) representation of the block's 32 byte payloadtimestamp
is the Unix timestamp when this block was createdparentID
is the block's parent
getBlock
Example Call
getBlock
Example Response
getBlock
Implementation
timestampvm.proposeBlock
Propose the next block on this blockchain.
proposeBlock
Signature
data
is the base 58 (with checksum) representation of the proposed block's 32 byte payload.
proposeBlock
Example Call
proposeBlock
Example Response
proposeBlock
Implementation
Plugin
In order to make this VM compatible with go-plugin
, we need to define a main
package and method, which serves our VM over gRPC so that AvalancheGo can call its methods. main.go
's contents are:
Now AvalancheGo's rpcchainvm
can connect to this plugin and calls its methods.
Executable Binary
This VM has a build script that builds an executable of this VM (when invoked, it runs the main
method from above.)
The path to the executable, as well as its name, can be provided to the build script via arguments. For example:
If no argument is given, the path defaults to a binary named with default VM ID: $GOPATH/src/github.com/ava-labs/avalanchego/build/plugins/tGas3T58KzdjLHhBDMnH2TvrddhqTji5iZAMZ3RXs2NLpSnhH
This name tGas3T58KzdjLHhBDMnH2TvrddhqTji5iZAMZ3RXs2NLpSnhH
is the CB58 encoded 32 byte identifier for the VM. For the timestampvm, this is the string "timestamp" zero-extended in a 32 byte array and encoded in CB58.
VM Aliases
Each VM has a predefined, static ID. For instance, the default ID of the TimestampVM is: tGas3T58KzdjLHhBDMnH2TvrddhqTji5iZAMZ3RXs2NLpSnhH
.
It's possible to give an alias for these IDs. For example, we can alias TimestampVM
by creating a JSON file at ~/.avalanchego/configs/vms/aliases.json
with:
The name of the VM binary is also its static ID and should not be changed manually. Changing the name of the VM binary will result in AvalancheGo failing to start the VM. To reference a VM by another name, define a VM alias as described below.
Installing a VM
AvalancheGo searches for and registers plugins under the plugins
directory.
To install the virtual machine onto your node, you need to move the built virtual machine binary under this directory. Virtual machine executable names must be either a full virtual machine ID (encoded in CB58), or a VM alias.
Copy the binary into the plugins directory.
Node Is Not Running
If your node isn't running yet, you can install all virtual machines under your plugin
directory by starting the node.
Node Is Already Running
Load the binary with the loadVMs
API.
Confirm the response of loadVMs
contains the newly installed virtual machine tGas3T58KzdjLHhBDMnH2TvrddhqTji5iZAMZ3RXs2NLpSnhH
. You'll see this virtual machine as well as any others that weren't already installed previously in the response.
Now, this VM's static API can be accessed at endpoints /ext/vm/timestampvm
and /ext/vm/timestamp
. For more details about VM configs, see here.
In this tutorial, we used the VM's ID as the executable name to simplify the process. However, AvalancheGo would also accept timestampvm
or timestamp
since those are registered aliases in previous step.
Wrapping Up
That's it! That's the entire implementation of a VM which defines a blockchain-based timestamp server.
In this tutorial, we learned:
- The
block.ChainVM
interface, which all VMs that define a linear chain must implement - The
snowman.Block
interface, which all blocks that are part of a linear chain must implement - The
rpcchainvm
type, which allows blockchains to run in their own processes. - An actual implementation of
block.ChainVM
andsnowman.Block
.