Select Page

How to Create a Virtual Machine on Avalanche

avalanche-virtual-machine

The Avalanche VM is an important component of the Avalanche platform, and it is a key reason why the platform is well-suited for building and deploying decentralized applications. One of the key features of Avalanche is its virtual machine (VM), which is responsible for executing the smart contracts that power dApps on the platform. The Avalanche VM is a highly efficient and optimized implementation of the Ethereum Virtual Machine (EVM), the runtime environment that executes smart contracts on the Ethereum blockchain.

One of the main benefits of the Avalanche VM is its high performance and scalability. It is designed to handle a large number of transactions per second (TPS), making it well-suited for applications that require fast and efficient execution of smart contracts. Additionally, the Avalanche VM is designed to be fully compatible with the EVM, which means that it can execute any smart contract written in Solidity, the programming language used to write smart contracts on Ethereum. Another key feature of the Avalanche VM is its security. It is designed to be resistant to common vulnerabilities and exploits, and it includes a number of security measures to protect against potential attacks. For example, it has a built-in garbage collector to prevent memory leaks and several measures to prevent code injection and other code-level attacks.

This article will explain the basics of Avalanche protocol, the importance of a virtual machine on Avalanche and how to create a virtual machine on Avalanche.

About the Avalanche blockchain

Avalanche is a decentralized protocol for building and deploying blockchain networks. It is designed to be fast, secure, and scalable and aims to solve many problems plaguing existing blockchains, such as slow transaction speeds and high fees. Avalanche is a blockchain that promises rapid confirmation times and scaling capabilities through its Avalanche Consensus Protocol. It can process approx. 4,500 transactions per second (TPS). Avalanche’s native token, AVAX, is the 10th largest, and had a market capitalization of $33 billion in March 2022.

Avalanche was launched in September 2020, and has since grown to be one of the most popular blockchains. According to statistics it has more than $11 billion in total value. This makes it the fourth largest DeFi-supporting blockchain after Terra Smart Chain and Binance Smart Chain. Avalanche’s DeFi ecosystem is thriving and includes some protocols from Ethereum, such as the lending protocol Aave and the decentralized exchange protocol SushiSwap. However, Avalanche doesn’t only support DeFi. Ava Labs financially supports Metaverse Investments in this network. The idea is that a fast, cheap network could easily support blockchain-based games as well as virtual worlds.

The key features of Avalanche

  • Fast transaction processing: The Avalanche consensus mechanism allows for faster transaction processing than traditional proof-of-work (PoW) or proof-of-stake (PoS) consensus mechanisms, resulting in faster confirmation times and lower fees.
  • High scalability: The Avalanche protocol is designed to scale horizontally, meaning that it can easily handle an increase in the number of nodes in the network. This allows for faster transaction processing as the network grows.
  • Customizable: The modular design of the Avalanche protocol enables developers to build and deploy customized blockchain networks for a variety of use cases.
  • Secure: The Avalanche consensus mechanism is designed to be resistant to certain types of attacks, such as the “nothing at stake” attack, which can plague PoS systems.
  • Interoperability: The Avalanche protocol is designed to be interoperable with other blockchain networks, allowing for easy integration and communication between different networks.

How does Avalanche work?

Avalanche’s platform may be complex, but its three main aspects stand out from other blockchain projects. These are its consensus mechanism, its incorporation of subnetworks and its use of multiple built-in blockchains.

Avalanche consensus

A protocol allowing nodes to agree is necessary for a blockchain to verify transactions and keep them secure. This protocol is referred to as consensus. With regard to cryptocurrencies, the discussion has centered around proof-of-work (PoW) vs. proof-of-stake (PoS) as the most popular methods to reach this agreement. Avalanche utilizes a new consensus mechanism that is based on the PoS foundation. In this mechanism, when a user initiates a transaction, it is then received by a validator, which samples a small number of validators and checks for agreement. To reach a consensus, the validators “gossip” with one another repeatedly during this sampling process.

This is how one validator sends a message to another validator, which samples more validators. The process continues until all parties reach an agreement. A single transaction can turn into an Avalanche, just as a single snowflake could become a snowball. Depending on the amount of time a node has staked its tokens, validators reward scale accordingly, which is called Proof of Uptime. If the node acts according to the software’s rules, it is known as Proof of Correctness.

Subnetworks

Avalanche users have the ability to launch their own chains, which can be operated using their own rules. This system is similar to other blockchain scaling solutions like Polkadot’s parachains or Ethereum 2.0’s shards. Subnetworks, or subnets, are groups of nodes that participate in validating a set of blockchains to reach a consensus on these chains. Subnet validators must also validate Avalanche’s Primary Network.

Built-in blockchains

Avalanche uses three different blockchains to overcome the limitations of the blockchain trilemma. Each chain can hold digital assets that can be used to perform different functions within the ecosystem.

  • The Exchange Chain (X-Chain) is where assets can be created and traded. This includes Avalanche’s native token, AVAX.
  • The Contract Chain (C-Chain) allows you to create and execute smart contracts. Avalanche smart contracts, which are based on Ethereum Virtual Machine, can benefit from cross-chain interoperability because they are cross-chain compatible.
  • The Platform Chain (P-Chain) coordinates validators and allows the creation and management of subnets.

Benefits that the Avalanche protocol offers

    • Anyone can create dApps on Avalanche

Avalanche allows anyone to create custom-made applications. This is a result of the revolutionary Avalanche consensus. It’s very similar to Cosmos and Polkadot but has higher transaction throughput and finality. Furthermore, it can scale to millions of validators and impact its network. Avalanche is also the first platform for smart contracts to confirm transactions in less than one second.

    • Uses a solid consensus method

Avalanche is distinguished by its combination of the Nakamoto consensus’s strengths, scalability, decentralization. Avalanche Consensus, which is also known as Classical consensus, offers all the benefits that Classical consensus has – speed, finality, and energy efficiency. These are the key features of Avalanche Consensus:

      • High throughput of 4500 TPS
      • Upto 51% resilient to attacks
      • Highly decentralized
      • Scalable
      • Robust
    • Ethereum compatibility

Avalanche is a smart contract chain that is 100% compatible with the Ethereum Virtual Machine. This allows anyone to deploy smart contracts in Ethereum-related languages such as Solidity on Avalanche. You can also develop dApps using Ethereum. Avalanche allows you to do the same but with much faster transactions and very low fees.

Other benefits include the following:

  • Energy efficient: Because the avalanche protocol does not rely on mining, it is much more energy efficient than PoW.
  • Resilient to attack: The avalanche protocol is resistant to many types of attacks, including double-spend attacks and Sybil attacks, because it requires a large number of nodes to reach a consensus on a new block.
  • Low fees: Because the avalanche protocol does not require miners to solve complex computational problems, fees for transactions on the blockchain are typically much lower than on other blockchain systems.
  • Decentralized: The avalanche protocol is highly decentralized, as it does not rely on a small number of miners or stakers to validate transactions. This makes it less vulnerable to centralization and censorship.

What is a virtual machine?

In the context of blockchain, a virtual machine is a software program that executes the instructions of a smart contract. Smart contracts are self-executing contracts with the terms of the agreement between buyer and seller being directly written into lines of code. The code and the agreements contained therein are stored and replicated on a blockchain network. As part of smart contract execution, the virtual machine is responsible for executing codes of smart contracts, which can include things like sending and receiving transactions, modifying data stored on the blockchain, and interacting with other smart contracts. The virtual machine ensures that the instructions of the smart contract are carried out as intended, providing a secure and reliable way to automate complex processes and agreements on the blockchain.

There are several virtual machine implementations in various blockchain platforms, each with its own set of features and capabilities. Some examples include the Ethereum Virtual Machine (EVM), the Hyperledger Fabric Virtual Machine (HLF VM), and the Avalanche Virtual Machine (BSVVM).

Role of Avalanche virtual machine

Two components are essential to a blockchain: the consensus engine and the virtual machine (VM). The VM describes each application’s behavior and how blocks are built and parsed to create the blockchain. VMs run on top of the Avalanche Consensus Engine, allowing all nodes to agree on the status of the blockchain. Here is a quick example to show how VMs interact and support the consensus.

  • A node wants to update the state of the blockchain.
  • The node’s VM will inform the consensus engine that the state needs to be updated.
  • The consensus engine will request the block from the VM.
  • The consensus engine will verify the returned block using verify () VM implementation.
  • The consensus engine will allow the network to agree on the acceptance or rejection of the newly verified block.
  • Every network node will show the same preference for a specific block if it is virtuous and well-behaved.
  • The consensus results will determine whether the engine accepts or rejects the block.
  • The implementation of the VM will determine what happens to a block that is rejected or accepted.

AvalancheGo is the consensus engine for all blockchains on the Avalanche network. The VM interface is used to build, parse, store and verify blocks for the consensus engine. Developers can build their applications quickly using virtual machines. This allows them to avoid having to worry about Avalanche’s consensus layer, which manages how nodes decide whether to accept or reject a block. Binaries are used to supply VMs to an AvalancheGo node. These binaries should be named the VMID that was assigned to the VM. A VMID is a 32 -byte hash encoded in CB58, which is used to build a VM.

Empower your business with the speed, security and scalability of Avalanche blockchain.

Develop your next feature-rich dApps with LeewayHertz.

How to create a virtual machine on Avalanche?

This article will show you how to create a simple Avalanche virtual machine called TimestampVM. Each block of the TimestampVM’s blockchain contains a 32-byte payload and an increasing timestamp at the block creation time. This server can be used to prove that a block of data existed at the time it was created.

Step 1: Prerequisites

1. Interfaces that every VM must implement

  • block.ChainVM– To reach a consensus on linear blockchains, Avalanche uses the Snowman consensus engine. To be compatible with Snowman, a VM must implement the block.ChainVM interface.
type ChainVM interface {
common.VM
Getter
Parser

BuildBlock() (snowman.Block, error)
LastAccepted() (ids.ID, error)
}

// Getter defines the functionality for fetching a block by its ID.
type Getter interface {
// Attempt to load a block.
//
// If the block does not exist, an error should be returned.
//
GetBlock(ids.ID) (snowman.Block, error)
}

// Parser defines the functionality for fetching a block by its bytes.
type Parser interface {
// Attempt to create a block from a stream of bytes.
//
// The block should be represented by the full byte array, without extra
// bytes.
ParseBlock([]byte) (snowman.Block, error)
}
  • common.VM – This is a type that every VM must implement.
type VM interface {
// Contains handlers for VM-to-VM specific messages
AppHandler

// Returns nil if the VM is healthy.
// Periodically called and reported via the node's Health API.
health.Checkable

// Connector represents a handler that is called on connection connect/disconnect
validators.Connector

// Initialize this VM.
// [ctx]: Metadata about this VM.
// [ctx.networkID]: The ID of the network this VM's chain is running on.
// [ctx.chainID]: The unique ID of the chain this VM is running on.
// [ctx.Log]: Used to log messages
// [ctx.NodeID]: The unique staker ID of this node.
// [ctx.Lock]: A Read/Write lock shared by this VM and the consensus
// engine that manages this VM. The write lock is held
// whenever code in the consensus engine calls the VM.
// [dbManager]: The manager of the database this VM will persist data to.
// [genesisBytes]: The byte-encoding of the genesis information of this
// VM. The VM uses it to initialize its state. For
// example, if this VM were an account-based payments
// system, `genesisBytes` would probably contain a genesis
// transaction that gives coins to some accounts, and this
// transaction would be in the genesis block.
// [toEngine]: The channel used to send messages to the consensus engine.
// [fxs]: Feature extensions that attach to this VM.
Initialize(
ctx *snow.Context,
dbManager manager.Manager,
genesisBytes []byte,
upgradeBytes []byte,
configBytes []byte,
toEngine chan<- Message,
fxs []*Fx,
appSender AppSender,
) error

// Bootstrapping is called when the node is starting to bootstrap this chain.
Bootstrapping() error

// Bootstrapped is called when the node is done bootstrapping this chain.
Bootstrapped() error

// Shutdown is called when the node is shutting down.
Shutdown() error

// Version returns the version of the VM this node is running.
Version() (string, error)

// Creates the HTTP handlers for custom VM network calls.
//
// This exposes handlers that the outside world can use to communicate with
// a static reference to the VM. Each handler has the path:
// [Address of node]/ext/VM/[VM ID]/[extension]
//
// Returns a mapping from [extension]s to HTTP handlers.
//
// Each extension can specify how locking is managed for convenience.
//
// For example, it might make sense to have an extension for creating
// genesis bytes this VM can interpret.
CreateStaticHandlers() (map[string]*HTTPHandler, error)

// Creates the HTTP handlers for custom chain network calls.
//
// This exposes handlers that the outside world can use to communicate with
// the chain. Each handler has the path:
// [Address of node]/ext/bc/[chain ID]/[extension]
//
// Returns a mapping from [extension]s to HTTP handlers.
//
// Each extension can specify how locking is managed for convenience.
//
// For example, if this VM implements an account-based payments system,
// it have an extension called `accounts`, where clients could get
// information about their accounts.
CreateHandlers() (map[string]*HTTPHandler, error)
}
  • snowman.Block – The snowman.Block interface defines the functionality a block must implement to be a block in a linear Snowman chain.
type Block interface {
choices.Decidable

// Parent returns the ID of this block's parent.
Parent() ids.ID

// Verify that the state transition this block would make if accepted is
// valid. If the state transition is invalid, a non-nil error should be
// returned.
//
// It is guaranteed that the Parent has been successfully verified.
Verify() error

// Bytes returns the binary representation of this block.
//
// This is used for sending blocks to peers. The bytes should be able to be
// parsed into the same block on another node.
Bytes() []byte

// Height returns the height of this block in the chain.
Height() uint64
}
  • choices.Decidable – This interface is a superset of every decidable object, such as transactions, blocks, and vertices.
type Decidable interface {
// ID returns a unique ID for this element.
//
// Typically, this is implemented by using a cryptographic hash of a
// binary representation of this element. An element should return the same
// IDs upon repeated calls.
ID() ids.ID

// Accept this element.
//
// This element will be accepted by every correct node in the network.
Accept() error

// Reject this element.
//
// This element will not be accepted by any correct node in the network.
Reject() error

// Status returns this element's current status.
//
// If Accept has been called on an element with this ID, Accepted should be
// returned. Similarly, if Reject has been called on an element with this
// ID, Rejected should be returned. If the contents of this element are
// unknown, then Unknown should be returned. Otherwise, Processing should be
// returned.
Status() Status
}

2. Download the timestampvm code from GitHub.

Step 2: Writing TimestampVM

The following classes are used to write the TimestampVM.The detailed code of the classes is available in downloadable from GitHub, as mentioned in the previous step. We will describe the functionality of each of the classes.

codec.go – required to encode/decode the block into byte representation.

const(
// CodecVersion is the current default codec version
CodecVersion =0
)

// Codecs do serialization and deserialization
var(
Codec codec.Manager
)

funcinit(){
// Create default codec and manager
c := linearcodec.NewDefault()
Codec = codec.NewDefaultManager()

// Register codec to manager with CodecVersion
if err := Codec.RegisterCodec(CodecVersion, c); err !=nil{
panic(err)
}
}

state.go – The State interface defines the database layer and connections. Each VM should define its own database methods. State embeds the BlockState which defines block-related state operations.

var(
// These are prefixes for db keys.
// It's important to set different prefixes for each separate database objects.
singletonStatePrefix =[]byte("singleton")
blockStatePrefix =[]byte("block")

_ State =&state{}
)

// State is a wrapper around avax.SingleTonState and BlockState
// State also exposes a few methods needed for managing database commits and close.
type State interface{
// SingletonState is defined in avalanchego,
// it is used to understand if db is initialized already.
avax.SingletonState
BlockState

Commit()error
Close()error
}

type state struct{
avax.SingletonState
BlockState

baseDB *versiondb.Database
}

funcNewState(db database.Database, vm *VM) State {
// create a new baseDB
baseDB := versiondb.New(db)

// create a prefixed "blockDB" from baseDB
blockDB := prefixdb.New(blockStatePrefix, baseDB)
// create a prefixed "singletonDB" from baseDB
singletonDB := prefixdb.New(singletonStatePrefix, baseDB)

// return state with created sub state components
return&state{
BlockState:NewBlockState(blockDB, vm),
SingletonState: avax.NewSingletonState(singletonDB),
baseDB: baseDB,
}
}

// Commit commits pending operations to baseDB
func(s *state)Commit()error{
return s.baseDB.Commit()
}

// Close closes the underlying base database
func(s *state)Close()error{
return s.baseDB.Close()
}

block_state.go – This interface and its implementation provide storage functions to VM to store and retrieve blocks.

const(
lastAcceptedByte byte=iota
)

const(
// maximum block capacity of the cache
blockCacheSize =8192
)

// persists lastAccepted block IDs with this key
var lastAcceptedKey =[]byte{lastAcceptedByte}

var_ BlockState =&blockState{}

// BlockState defines methods to manage state with Blocks and LastAcceptedIDs.
type BlockState interface{
GetBlock(blkID ids.ID)(*Block,error)
PutBlock(blk *Block)error

GetLastAccepted()(ids.ID,error)
SetLastAccepted(ids.ID)error
}

// blockState implements BlocksState interface with database and cache.
type blockState struct{
// cache to store blocks
blkCache cache.Cacher
// block database
blockDB database.Database
lastAccepted ids.ID

// vm reference
vm *VM
}

// blkWrapper wraps the actual blk bytes and status to persist them together
type blkWrapper struct{
Blk []byte`serialize:"true"`
Status choices.Status `serialize:"true"`
}

// NewBlockState returns BlockState with a new cache and given db
funcNewBlockState(db database.Database, vm *VM) BlockState {
return&blockState{
blkCache:&cache.LRU{Size: blockCacheSize},
blockDB: db,
vm: vm,
}
}

// GetBlock gets Block from either cache or database
func(s *blockState)GetBlock(blkID ids.ID)(*Block,error){
// Check if cache has this blkID
if blkIntf, cached := s.blkCache.Get(blkID); cached {
// there is a key but value is nil, so return an error
if blkIntf ==nil{
returnnil, database.ErrNotFound
}
// We found it return the block in cache
return blkIntf.(*Block),nil
}

// get block bytes from db with the blkID key
wrappedBytes, err := s.blockDB.Get(blkID[:])
if err !=nil{
// we could not find it in the db, let's cache this blkID with nil value
// so next time we try to fetch the same key we can return error
// without hitting the database
if err == database.ErrNotFound {
s.blkCache.Put(blkID,nil)
}
// could not find the block, return error
returnnil, err
}

// first decode/unmarshal the block wrapper so we can have status and block bytes
blkw := blkWrapper{}
if_, err := Codec.Unmarshal(wrappedBytes,&blkw); err !=nil{
returnnil, err
}

// now decode/unmarshal the actual block bytes to block
blk :=&Block{}
if_, err := Codec.Unmarshal(blkw.Blk, blk); err !=nil{
returnnil, err
}

// initialize block with block bytes, status and vm
blk.Initialize(blkw.Blk, blkw.Status, s.vm)

// put block into cache
s.blkCache.Put(blkID, blk)

return blk,nil
}

// PutBlock puts block into both database and cache
func(s *blockState)PutBlock(blk *Block)error{
// create block wrapper with block bytes and status
blkw := blkWrapper{
Blk: blk.Bytes(),
Status: blk.Status(),
}

// encode block wrapper to its byte representation
wrappedBytes, err := Codec.Marshal(CodecVersion,&blkw)
if err !=nil{
return err
}

blkID := blk.ID()
// put actual block to cache, so we can directly fetch it from cache
s.blkCache.Put(blkID, blk)

// put wrapped block bytes into database
return s.blockDB.Put(blkID[:], wrappedBytes)
}

// DeleteBlock deletes block from both cache and database
func(s *blockState)DeleteBlock(blkID ids.ID)error{
s.blkCache.Put(blkID,nil)
return s.blockDB.Delete(blkID[:])
}

// GetLastAccepted returns last accepted block ID
func(s *blockState)GetLastAccepted()(ids.ID,error){
// check if we already have lastAccepted ID in state memory
if s.lastAccepted != ids.Empty {
return s.lastAccepted,nil
}

// get lastAccepted bytes from database with the fixed lastAcceptedKey
lastAcceptedBytes, err := s.blockDB.Get(lastAcceptedKey)
if err !=nil{
return ids.ID{}, err
}
// parse bytes to ID
lastAccepted, err := ids.ToID(lastAcceptedBytes)
if err !=nil{
return ids.ID{}, err
}
// put lastAccepted ID into memory
s.lastAccepted = lastAccepted
return lastAccepted,nil
}

// SetLastAccepted persists lastAccepted ID into both cache and database
func(s *blockState)SetLastAccepted(lastAccepted ids.ID)error{
// if the ID in memory and the given memory are same don't do anything
if s.lastAccepted == lastAccepted {
returnnil
}
// put lastAccepted ID to memory
s.lastAccepted = lastAccepted
// persist lastAccepted ID to database with fixed lastAcceptedKey
return s.blockDB.Put(lastAcceptedKey, lastAccepted[:])
}

block.go – It is used for block implementation. 

There are three important methods here –

  • 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 as shown above.
func (b *Block) Verify() error {
// Get [b]'s parent
parentID := b.Parent()
parent, err := b.vm.getBlock(parentID)
if err != nil {
return errDatabaseGet
}
}
  • Accept – Accept is called by consensus to indicate this block is accepted.
func (b *Block) Accept() error {
b.SetStatus(choices.Accepted) // Change state of this block
blkID := b.ID()

// Persist data
if err := b.vm.state.PutBlock(b); err != nil {
return err
}

// Set last accepted ID to this block ID
if err := b.vm.state.SetLastAccepted(blkID); err != nil {
return err
}

// Delete this block from verified blocks as it's accepted
delete(b.vm.verifiedBlocks, b.ID())

// Commit changes to database
return b.vm.state.Commit()
}
  • Reject – This is called by the consensus to indicate this block is rejected.
func (b *Block) Reject() error {
b.SetStatus(choices.Rejected) // Change state of this block
if err := b.vm.state.PutBlock(b); err != nil {
return err
}
// Delete this block from verified blocks as it's rejected
delete(b.vm.verifiedBlocks, b.ID())
// Commit changes to database
return b.vm.state.Commit()
}

The following methods are required by the snowman.Block interface

// ID returns the ID of this block
func (b *Block) ID() ids.ID { return b.id }

// ParentID returns [b]'s parent's ID
func (b *Block) Parent() ids.ID { return b.PrntID }

// Height returns this block's height. The genesis block has height 0.
func (b *Block) Height() uint64 { return b.Hght }

// Timestamp returns this block's time. The genesis block has time 0.
func (b *Block) Timestamp() time.Time { return time.Unix(b.Tmstmp, 0) }

// Status returns the status of this block
func (b *Block) Status() choices.Status { return b.status }

// Bytes returns the byte repr. of this block
func (b *Block) Bytes() []byte { return b.bytes }

Step 3: Implementation of TimestampVM

Let’s now look at how timestamp VM implements block.ChainVM interface. The complete implementation is done in the vm.go class.

Here we have described the most important functions of the vm.go class.
To initialize the VM, the class calls Initialize function. 

func (vm *VM) Initialize(
ctx *snow.Context,
dbManager manager.Manager,
genesisData []byte,
upgradeData []byte,
configData []byte,
toEngine chan<- common.Message,
_ []*common.Fx,
_ common.AppSender,
) error {
version, err := vm.Version()
if err != nil {
log.Error("error initializing Timestamp VM: %v", err)
return err
}
log.Info("Initializing Timestamp VM", "Version", version)

vm.dbManager = dbManager
vm.ctx = ctx
vm.toEngine = toEngine
vm.verifiedBlocks = make(map[ids.ID]*Block)

// Create new state
vm.state = NewState(vm.dbManager.Current().Database, vm)

// Initialize genesis
if err := vm.initGenesis(genesisData); err != nil {
return err
}

// Get last accepted
lastAccepted, err := vm.state.GetLastAccepted()
if err != nil {
return err
}

ctx.Log.Info("initializing last accepted block as %s", lastAccepted)

// Build off the most recently accepted block
return vm.SetPreference(lastAccepted)
}

This class is also responsible for initializing the genesis block through its initGenesis helper method

func (vm *VM) initGenesis(genesisData []byte) error {
stateInitialized, err := vm.state.IsInitialized()
if err != nil {
return err
}

// if state is already initialized, skip init genesis.
if stateInitialized {
return nil
}

if len(genesisData) > dataLen {
return errBadGenesisBytes
}

// genesisData is a byte slice but each block contains an byte array
// Take the first [dataLen] bytes from genesisData and put them in an array
var genesisDataArr [dataLen]byte
copy(genesisDataArr[:], genesisData)

// Create the genesis block
// Timestamp of genesis block is 0. It has no parent.
genesisBlock, err := vm.NewBlock(ids.Empty, 0, genesisDataArr, time.Unix(0, 0))
if err != nil {
log.Error("error while creating genesis block: %v", err)
return err
}

// Put genesis block to state
if err := vm.state.PutBlock(genesisBlock); err != nil {
log.Error("error while saving genesis block: %v", err)
return err
}

// Accept the genesis block
// Sets [vm.lastAccepted] and [vm.preferred]
if err := genesisBlock.Accept(); err != nil {
return fmt.Errorf("error accepting genesis block: %w", err)
}

// Mark this vm's state as initialized, so we can skip initGenesis in further restarts
if err := vm.state.SetInitialized(); err != nil {
return fmt.Errorf("error while setting db to initialized: %w", err)
}

// Flush VM's database to underlying db
return vm.state.Commit()
}

The class builds a new block and returns it through its BuildBlock method as requested by the consensus engine.

func (vm *VM) BuildBlock() (snowman.Block, error) {
if len(vm.mempool) == 0 { // There is no block to be built
return nil, errNoPendingBlocks
}

// Get the value to put in the new block
value := vm.mempool[0]
vm.mempool = vm.mempool[1:]

// Notify consensus engine that there are more pending data for blocks
// (if that is the case) when done building this block
if len(vm.mempool) > 0 {
defer vm.NotifyBlockReady()
}

// Gets Preferred Block
preferredBlock, err := vm.getBlock(vm.preferred)
if err != nil {
return nil, fmt.Errorf("couldn't get preferred block: %w", err)
}
preferredHeight := preferredBlock.Height()

// Build the block with preferred height
newBlock, err := vm.NewBlock(vm.preferred, preferredHeight+1, value, time.Now())
if err != nil {
return nil, fmt.Errorf("couldn't build block: %w", err)
}

// Verifies block
if err := newBlock.Verify(); err != nil {
return nil, err
}
return newBlock, nil
}

To send messages to the consensus engine, the class uses one of its helper methods, called NotifyBlockReady.

func (vm *VM) NotifyBlockReady() {
select {
case vm.toEngine <- common.PendingTxs:
default:
vm.ctx.Log.Debug("dropping message to consensus engine")
}
}

The block ID is ascertained with the GetBlock method.

func (vm *VM) GetBlock(blkID ids.ID) (snowman.Block, error) { return vm.getBlock(blkID) }

func (vm *VM) getBlock(blkID ids.ID) (*Block, error) {
// If block is in memory, return it.
if blk, exists := vm.verifiedBlocks[blkID]; exists {
return blk, nil
}

return vm.state.GetBlock(blkID)
}

The proposeBlock 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

func (vm *VM) proposeBlock(data [dataLen]byte) {
vm.mempool = append(vm.mempool, data)
vm.NotifyBlockReady()
}
  • The NewBlock method creates a new block
func (vm *VM) NewBlock(parentID ids.ID, height uint64, data [dataLen]byte, timestamp time.Time) (*Block, error) {

block := &Block{

PrntID: parentID,

Hght: height,

Tmstmp: timestamp.Unix(),

Dt: data,

}

// Get the byte representation of the block

blockBytes, err := Codec.Marshal(CodecVersion, block)

if err != nil {

return nil, err

}

// Initialize the block by providing it with its byte representation

// and a reference to this VM

block.Initialize(blockBytes, choices.Processing, vm)

return block, nil

}

Step 4: Factory creation

factory.go – VMs should implement the Factory interface. New method in the interface returns a new VM instance.

var_ vms.Factory =&Factory{}

// Factory ...
type Factory struct{}

// New ...
func (f *Factory) New(*snow.Context) (interface{}, error) { return &VM{}, nil }

Step 5: Static API creation

static_service.go – Creates 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. 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.

For the detailed implementation of static_service.go refer to the static_service.go code.

Step 6: API creation

service.go – Creates non-static API

A VM may also have a non-static HTTP API, which allows clients to query and update the blockchain’s 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 a unique ID.

Step 7: Defining the main package

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.

func main() {
log.Root().SetHandler(log.LvlFilterHandler(log.LvlDebug, log.StreamHandler(os.Stderr, log.TerminalFormat())))
plugin.Serve(&plugin.ServeConfig{
HandshakeConfig: rpcchainvm.Handshake,
Plugins: map[string]plugin.Plugin{
"vm": rpcchainvm.New(&timestampvm.VM{}),
},

// A non-nil value here enables gRPC serving for this plugin...
GRPCServer: plugin.DefaultGRPCServer,
})
}

Now AvalancheGo’s rpcchainvm can connect to this plugin and calls its methods.

Step 8: Binary execution

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 and its name can be provided to the build script via arguments. For example:

./scripts/build.sh ../avalanchego/build/plugins timestampvm

Your VM is now ready.

Endnote

VMs provide a way to isolate the execution of code from the underlying hardware and operating system, which can be useful for a number of reasons. One reason to use VMs on Avalanche is to enable the execution of untrusted code in a controlled environment. By running code in a VM, you can ensure that it cannot access sensitive resources or harm the system in any way, even if the code contains malicious intent. This can be particularly useful for running smart contracts or other code that is executed on the platform. Another reason to use VMs on Avalanche is to enable the execution of code in different environments or configurations. Creating a VM allows you to specify the operating system, runtime environment, and other settings to provide the right code execution environment. This can be useful for testing and debugging purposes or running code requiring specific dependencies or configurations.

Overall, using VMs on Avalanche can help improve the platform’s security, scalability, and flexibility and facilitate a wide range of applications and use cases.

Unlock the full potential of the decentralized world with Avalanche VMs. Contact LeewayHertz’s team of experts to create and run a virtual machine on Avalanche.

Author’s Bio

 

Akash Takyar

Akash Takyar LinkedIn
CEO LeewayHertz
Akash Takyar is the founder and CEO at LeewayHertz. The experience of building over 100+ platforms for startups and enterprises allows Akash to rapidly architect and design solutions that are scalable and beautiful.
Akash's ability to build enterprise-grade technology solutions has attracted over 30 Fortune 500 companies, including Siemens, 3M, P&G and Hershey’s.
Akash is an early adopter of new technology, a passionate technology enthusiast, and an investor in AI and IoT startups.

Start a conversation by filling the form

Once you let us know your requirement, our technical expert will schedule a call and discuss your idea in detail post sign of an NDA.
All information will be kept confidential.

Insights

Follow Us