Create a Virtual Machine (VM)

Introduction

One of the core features of Avalanche is the ability to create new, custom blockchains, which are defined by Virtual Machines (VMs)
In this tutorial, we’ll create a very simple VM. The blockchain defined by the VM is a timestamp server. Each block in the blockchain contains the timestamp when it was created along with a 32-byte piece of data (payload). Each block’s timestamp is after its parent’s timestamp.
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).
A blockchain can run as a separate process from AvalancheGo and can communicate with AvalancheGo over gRPC. This is enabled by rpcchainvm, a special VM that uses go-plugin and wraps another VM implementation. The C-Chain, for example, runs the Coreth VM in this fashion.
Before we get to the implementation of a VM, we’ll look at the interface that a VM must implement to be compatible with AvalancheGo's consensus engine. We’ll show and explain all the code in snippets. If you want to see all the code in one place, see this repository.
Note: IDs of Blockchains, Subnets, Transactions and Addresses can be different for each run/network. It means that some inputs, endpoints etc. in the tutorial can be different when you try.

Interfaces

block.ChainVM

To reach consensus on linear blockchains (as opposed to DAG blockchains), Avalanche uses the Snowman consensus engine. In order to be compatible with Snowman, a VM must implement the block.ChainVM interface, which we include below from its declaration.
The interface is big, but don’t worry, we’ll explain each method and see an implementation example, and it isn't important that you understand every detail right away.
1
package block
2
3
import (
4
"github.com/ava-labs/avalanchego/ids"
5
"github.com/ava-labs/avalanchego/snow/consensus/snowman"
6
"github.com/ava-labs/avalanchego/snow/engine/common"
7
)
8
9
// ChainVM defines the required functionality of a Snowman VM.
10
//
11
// A Snowman VM is responsible for defining the representation of state,
12
// the representation of operations on that state, the application of operations
13
// on that state, and the creation of the operations. Consensus will decide on
14
// if the operation is executed and the order operations are executed in.
15
//
16
// For example, suppose we have a VM that tracks an increasing number that
17
// is agreed upon by the network.
18
// The state is a single number.
19
// The operation is setting the number to a new, larger value.
20
// Applying the operation will save to the database the new value.
21
// The VM can attempt to issue a new number, of larger value, at any time.
22
// Consensus will ensure the network agrees on the number at every block height.
23
type ChainVM interface {
24
common.VM
25
26
// Attempt to create a new block from data contained in the VM.
27
//
28
// If the VM doesn't want to issue a new block, an error should be
29
// returned.
30
BuildBlock() (snowman.Block, error)
31
32
// Attempt to create a block from a stream of bytes.
33
//
34
// The block should be represented by the full byte array, without extra
35
// bytes.
36
ParseBlock([]byte) (snowman.Block, error)
37
38
// Attempt to load a block.
39
//
40
// If the block does not exist, then an error should be returned.
41
GetBlock(ids.ID) (snowman.Block, error)
42
43
// Notify the VM of the currently preferred block.
44
//
45
// This should always be a block that has no children known to consensus.
46
SetPreference(ids.ID) error
47
48
// LastAccepted returns the ID of the last accepted block.
49
//
50
// If no blocks have been accepted by consensus yet, it is assumed there is
51
// a definitionally accepted block, the Genesis block, that will be
52
// returned.
53
LastAccepted() (ids.ID, error)
54
}
Copied!

common.VM

common.VM is a type that every VM, whether a DAG or linear chain, must implement.
1
// VM describes the interface that all consensus VMs must implement
2
type VM interface {
3
// Returns nil if the VM is healthy.
4
// Periodically called and reported via the node's Health API.
5
health.Checkable
6
7
// Connector represents a handler that is called on connection connect/disconnect
8
validators.Connector
9
10
// Initialize this VM.
11
// [ctx]: Metadata about this VM.
12
// [ctx.networkID]: The ID of the network this VM's chain is running on.
13
// [ctx.chainID]: The unique ID of the chain this VM is running on.
14
// [ctx.Log]: Used to log messages
15
// [ctx.NodeID]: The unique staker ID of this node.
16
// [ctx.Lock]: A Read/Write lock shared by this VM and the consensus
17
// engine that manages this VM. The write lock is held
18
// whenever code in the consensus engine calls the VM.
19
// [dbManager]: The manager of the database this VM will persist data to.
20
// [genesisBytes]: The byte-encoding of the genesis information of this
21
// VM. The VM uses it to initialize its state. For
22
// example, if this VM were an account-based payments
23
// system, `genesisBytes` would probably contain a genesis
24
// transaction that gives coins to some accounts, and this
25
// transaction would be in the genesis block.
26
// [toEngine]: The channel used to send messages to the consensus engine.
27
// [fxs]: Feature extensions that attach to this VM.
28
Initialize(
29
ctx *snow.Context,
30
dbManager manager.Manager,
31
genesisBytes []byte,
32
upgradeBytes []byte,
33
configBytes []byte,
34
toEngine chan<- Message,
35
fxs []*Fx,
36
) error
37
38
// Bootstrapping is called when the node is starting to bootstrap this chain.
39
Bootstrapping() error
40
41
// Bootstrapped is called when the node is done bootstrapping this chain.
42
Bootstrapped() error
43
44
// Shutdown is called when the node is shutting down.
45
Shutdown() error
46
47
// Version returns the version of the VM this node is running.
48
Version() (string, error)
49
50
// Creates the HTTP handlers for custom VM network calls.
51
//
52
// This exposes handlers that the outside world can use to communicate with
53
// a static reference to the VM. Each handler has the path:
54
// [Address of node]/ext/VM/[VM ID]/[extension]
55
//
56
// Returns a mapping from [extension]s to HTTP handlers.
57
//
58
// Each extension can specify how locking is managed for convenience.
59
//
60
// For example, it might make sense to have an extension for creating
61
// genesis bytes this VM can interpret.
62
CreateStaticHandlers() (map[string]*HTTPHandler, error)
63
64
// Creates the HTTP handlers for custom chain network calls.
65
//
66
// This exposes handlers that the outside world can use to communicate with
67
// the chain. Each handler has the path:
68
// [Address of node]/ext/bc/[chain ID]/[extension]
69
//
70
// Returns a mapping from [extension]s to HTTP handlers.
71
//
72
// Each extension can specify how locking is managed for convenience.
73
//
74
// For example, if this VM implements an account-based payments system,
75
// it have an extension called `accounts`, where clients could get
76
// information about their accounts.
77
CreateHandlers() (map[string]*HTTPHandler, error)
78
}
Copied!

snowman.Block

You may have noticed the snowman.Block type referenced in the block.ChainVM interface. It describes the methods that a block must implement to be a block in a linear (Snowman) chain.
Let’s look at this interface and its methods, which we copy from here.
1
// Block is a possible decision that dictates the next canonical block.
2
//
3
// Blocks are guaranteed to be Verified, Accepted, and Rejected in topological
4
// order. Specifically, if Verify is called, then the parent has already been
5
// verified. If Accept is called, then the parent has already been accepted. If
6
// Reject is called, the parent has already been accepted or rejected.
7
//
8
// If the status of the block is Unknown, ID is assumed to be able to be called.
9
// If the status of the block is Accepted or Rejected; Parent, Verify, Accept,
10
// and Reject will never be called.
11
type Block interface {
12
choices.Decidable
13
14
// Parent returns the ID of this block's parent.
15
Parent() ids.ID
16
17
// Verify that the state transition this block would make if accepted is
18
// valid. If the state transition is invalid, a non-nil error should be
19
// returned.
20
//
21
// It is guaranteed that the Parent has been successfully verified.
22
Verify() error
23
24
// Bytes returns the binary representation of this block.
25
//
26
// This is used for sending blocks to peers. The bytes should be able to be
27
// parsed into the same block on another node.
28
Bytes() []byte
29
30
// Height returns the height of this block in the chain.
31
Height() uint64
32
}
Copied!

choices.Decidable

This interface is the superset of every decidable object, such as transactions, blocks and vertices.
1
// Decidable represents element that can be decided.
2
//
3
// Decidable objects are typically thought of as either transactions, blocks, or
4
// vertices.
5
type Decidable interface {
6
// ID returns a unique ID for this element.
7
//
8
// Typically, this is implemented by using a cryptographic hash of a
9
// binary representation of this element. An element should return the same
10
// IDs upon repeated calls.
11
ID() ids.ID
12
13
// Accept this element.
14
//
15
// This element will be accepted by every correct node in the network.
16
Accept() error
17
18
// Reject this element.
19
//
20
// This element will not be accepted by any correct node in the network.
21
Reject() error
22
23
// Status returns this element's current status.
24
//
25
// If Accept has been called on an element with this ID, Accepted should be
26
// returned. Similarly, if Reject has been called on an element with this
27
// ID, Rejected should be returned. If the contents of this element are
28
// unknown, then Unknown should be returned. Otherwise, Processing should be
29
// returned.
30
Status() Status
31
}
Copied!

Helper Libraries

We’ve created some types that your VM implementation can embed (embedding is like Go’s version of inheritance) in order to handle boilerplate code.
In our example, we use both of the library types below, and we encourage you to use them too.

core.SnowmanVM

This helper type is a struct that implements many of the block.ChainVM methods. Its implementation can be found here.

Methods

Some block.ChainVM methods implemented by core.SnowmanVM are:
    ParseBlock
    GetBlock
    SetPreference
    LastAccepted
    Shutdown
    Bootstrapping
    Bootstrapped
    Initialize
If your VM implementation embeds a core.SnowmanVM, you need not implement any of these methods because they are already implemented by core.SnowmanVM. You may, if you want, override these inherited methods.

Fields

This type contains several fields that you’ll want to include in your VM implementation. Among them:
    DB: the blockchain’s database
    Ctx: the blockchain’s runtime context
    preferred: ID of the preferred block, which new blocks will be built on
    LastAcceptedID: ID of the most recently accepted block
    ToEngine: channel used to send messages to the consensus engine powering this blockchain
    State: used to persist data such as blocks

core.Block

This helper type implements many methods of the snowman.Block interface.

Methods

Some implemented snowman.Block interface methods are:
    ID
    Parent
    Accept
    Reject
    Status
    Height
The blocks in your VM implementation will probably override Accept and Reject so that those methods cause application-specific state changes.

Fields

core.Block has a field VM, which is a reference to a core.SnowmanVM. This means that a core.Block has access to all of the fields and methods of that type.

rpcchainvm

rpcchainvm is a special VM that wraps a block.ChainVM and allows the wrapped blockchain to run in its own process separate from AvalancheGo. rpcchainvm has two important parts: a server and a client. The server runs the underlying block.ChainVM in its own process and allows the underlying VM's methods to be called via gRPC. The client runs as part of AvalancheGo and makes gRPC calls to the corresponding server in order to update or query the state of the blockchain.
To make things more concrete: suppose that AvalancheGo wants to retrieve a block from a chain run in this fashion. AvalancheGo calls the client's GetBlock method, which makes a gRPC call to the server, which is running in a separate process. The server calls the underlying VM's GetBlock method and serves the response to the client, which in turn gives the response to AvalancheGo.
As another example, let's look at the server's BuildBlock method:
1
func (vm *VMServer) BuildBlock(_ context.Context, _ *vmproto.BuildBlockRequest) (*vmproto.BuildBlockResponse, error) {
2
blk, err := vm.vm.BuildBlock()
3
if err != nil {
4
return nil, err
5
}
6
blkID := blk.ID()
7
parentID := blk.Parent()
8
return &vmproto.BuildBlockResponse{
9
Id: blkID[:],
10
ParentID: parentID[:],
11
Bytes: blk.Bytes(),
12
Height: blk.Height(),
13
}, nil
14
}
Copied!
It calls vm.vm.BuildBlock(), where vm.vm is the underlying VM implementation, and returns a new block.

Timestamp Server 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.

Block

First, let’s look at our block implementation.
The type declaration is:
1
// Block is a block on the chain.
2
// Each block contains:
3
// 1) A piece of data (the block's payload)
4
// 2) The (unix) timestamp when the block was created
5
type Block struct {
6
*core.Block `serialize:"true"`
7
Data [dataLen]byte `serialize:"true"`
8
Timestamp int64 `serialize:"true"`
9
}
Copied!
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 saves it in the database.
1
// Verify returns nil iff this block is valid.
2
// To be valid, it must be that:
3
// b.parent.Timestamp < b.Timestamp <= [local time] + 1 hour
4
func (b *Block) Verify() error {
5
// Check to see if this block has already been verified by calling Verify on the
6
// embedded *core.Block.
7
// If there is an error while checking, return an error.
8
// If the core.Block says the block is accepted, return accepted.
9
if accepted, err := b.Block.Verify(); err != nil || accepted {
10
return err
11
}
12
13
// Get [b]'s parent
14
parentID := b.Parent()
15
parentIntf, err := b.VM.GetBlock(parentID)
16
if err != nil {
17
return errDatabaseGet
18
}
19
parent, ok := parentIntf.(*Block)
20
if !ok {
21
return errBlockType
22
}
23
24
// Ensure [b]'s timestamp is after its parent's timestamp.
25
if b.Timestamp < time.Unix(parent.Timestamp, 0).Unix() {
26
return errTimestampTooEarly
27
}
28
29
// Ensure [b]'s timestamp is not more than an hour
30
// ahead of this node's time
31
if b.Timestamp >= time.Now().Add(time.Hour).Unix() {
32
return errTimestampTooLate
33
}
34
35
// Our block inherits VM from *core.Block.
36
// It holds the database we read/write, b.VM.DB
37
// We persist this block to that database using VM's SaveBlock method.
38
if err := b.VM.SaveBlock(b.VM.DB, b); err != nil {
39
return errDatabaseSave
40
}
41
42
// Then we flush the database's contents
43
return b.VM.DB.Commit()
44
}
Copied!
That’s all the code for our block implementation! All of the other methods of snowman.Block, which our Block must implement, are inherited from *core.Block.

Virtual Machine

Now, let’s look at our timestamp VM implementation, which implements the block.ChainVM interface.
The declaration is:
1
// This Virtual Machine defines a blockchain that acts as a timestamp server
2
// Each block contains data (a payload) and the timestamp when it was created
3
4
const (
5
dataLen = 32
6
codecVersion = 0
7
)
8
9
type VM struct {
10
core.SnowmanVM
11
12
// codec serializes and de-serializes structs to/from bytes
13
codec codec.Manager
14
15
// Proposed data that haven't been put into a block and proposed yet
16
mempool [][dataLen]byte
17
}
Copied!

Initialize

This method is called when a new instance of VM is initialized. Genesis block is created under this method.
1
// Initialize this vm
2
// [ctx] is this vm's context
3
// [dbManager] is the manager of this vm's database
4
// [toEngine] is used to notify the consensus engine that new blocks are
5
// ready to be added to consensus
6
// The data in the genesis block is [genesisData]
7
func (vm *VM) Initialize(
8
ctx *snow.Context,
9
dbManager manager.Manager,
10
genesisData []byte,
11
upgradeData []byte,
12
configData []byte,
13
toEngine chan<- common.Message,
14
_ []*common.Fx,
15
) error {
16
version, err := vm.Version()
17
if err != nil {
18
log.Error("error initializing Timestamp VM: %v", err)
19
return err
20
}
21
log.Info("Initializing Timestamp VM", "Version", version)
22
if err := vm.SnowmanVM.Initialize(ctx, dbManager.Current().Database, vm.ParseBlock, toEngine); err != nil {
23
log.Error("error initializing SnowmanVM: %v", err)
24
return err
25
}
26
c := linearcodec.NewDefault()
27
manager := codec.NewDefaultManager()
28
if err := manager.RegisterCodec(codecVersion, c); err != nil {
29
return err
30
}
31
vm.codec = manager
32
33
// If database is empty, create it using the provided genesis data
34
if !vm.DBInitialized() {
35
if len(genesisData) > dataLen {
36
return errBadGenesisBytes
37
}
38
39
// genesisData is a byte slice but each block contains an byte array
40
// Take the first [dataLen] bytes from genesisData and put them in an array
41
var genesisDataArr [dataLen]byte
42
copy(genesisDataArr[:], genesisData)
43
44
// Create the genesis block
45
// Timestamp of genesis block is 0. It has no parent.
46
genesisBlock, err := vm.NewBlock(ids.Empty, 0, genesisDataArr, time.Unix(0, 0))
47
if err != nil {
48
log.Error("error while creating genesis block: %v", err)
49
return err
50
}
51
52
// Saves the genesis block to DB
53
if err := vm.SaveBlock(vm.DB, genesisBlock); err != nil {
54
log.Error("error while saving genesis block: %v", err)
55
return err
56
}
57
58
// Accept the genesis block
59
// Sets [vm.lastAccepted] and [vm.preferred]
60
if err := genesisBlock.Accept(); err != nil {
61
return fmt.Errorf("error accepting genesis block: %w", err)
62
}
63
64
// Sets DB status to initialized.
65
if err := vm.SetDBInitialized(); err != nil {
66
return fmt.Errorf("error while setting db to initialized: %w", err)
67
}
68
69
// Flush VM's database to underlying db
70
if err := vm.DB.Commit(); err != nil {
71
log.Error("error while committing db: %v", err)
72
return err
73
}
74
}
75
return nil
76
}
Copied!

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.
1
// proposeBlock appends [data] to [p.mempool].
2
// Then it notifies the consensus engine
3
// that a new block is ready to be added to consensus
4
// (namely, a block with data [data])
5
func (vm *VM) proposeBlock(data [dataLen]byte) {
6
vm.mempool = append(vm.mempool, data)
7
vm.NotifyBlockReady()
8
}
Copied!

ParseBlock

Parse a block from its byte representation.
1
// ParseBlock parses [bytes] to a snowman.Block
2
// This function is used by the vm's state to unmarshal blocks saved in state
3
// and by the consensus layer when it receives the byte representation of a block
4
// from another node
5
func (vm *VM) ParseBlock(bytes []byte) (snowman.Block, error) {
6
// A new empty block
7
block := &Block{}
8
9
// Unmarshal the byte repr. of the block into our empty block
10
_, err := vm.codec.Unmarshal(bytes, block)
11
if err != nil {
12
return nil, err
13
}
14
15
// Initialize the block
16
// (Block inherits Initialize from its embedded *core.Block)
17
block.Initialize(bytes, &vm.SnowmanVM)
18
19
// Return the block
20
return block, nil
21
}
Copied!

CreateHandlers

Registed handlers defined in Service. See below for more on APIs.
1
// CreateHandlers returns a map where:
2
// Keys: The path extension for this blockchain's API (empty in this case)
3
// Values: The handler for the API
4
// In this case, our blockchain has only one API, which we name timestamp,
5
// and it has no path extension, so the API endpoint:
6
// [Node IP]/ext/bc/[this blockchain's ID]
7
// See API section in documentation for more information
8
func (vm *VM) CreateHandlers() map[string]*common.HTTPHandler {
9
// Create the API handler (we'll see the declaration of Service further on)
10
handler := vm.NewHandler("timestamp", &Service{vm})
11
return map[string]*common.HTTPHandler{
12
"": handler,
13
}
14
}
Copied!

CreateStaticHandlers

Registers static handlers defined in StaticService. See below for more on static APIs.
1
// CreateStaticHandlers returns a map where:
2
// Keys: The path extension for this VM's static API
3
// Values: The handler for that static API
4
// We return nil because this VM has no static API
5
// CreateStaticHandlers implements the common.StaticVM interface
6
func (vm *VM) CreateStaticHandlers() (map[string]*common.HTTPHandler, error) {
7
newServer := rpc.NewServer()
8
codec := cjson.NewCodec()
9
newServer.RegisterCodec(codec, "application/json")
10
newServer.RegisterCodec(codec, "application/json;charset=UTF-8")
11
12
// name this service "timestamp"
13
staticService := CreateStaticService()
14
return map[string]*common.HTTPHandler{
15
"": {LockOptions: common.WriteLock, Handler: newServer},
16
}, newServer.RegisterService(staticService, "timestampvm")
17
}
Copied!

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 analagous to static methods in computer programming. AvalancheGo uses Gorilla’s RPC library to implement HTTP APIs.
StaticService implements the static API for our VM.
1
// StaticService defines the static API for the timestamp vm
2
type StaticService struct{}
Copied!

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.
1
// EncodeArgs are arguments for Encode
2
type EncodeArgs struct {
3
Data string `json:"data"`
4
Encoding formatting.Encoding `json:"encoding"`
5
}
6
7
// EncodeReply is the reply from Encoder
8
type EncodeReply struct {
9
Bytes string `json:"bytes"`
10
Encoding formatting.Encoding `json:"encoding"`
11
}
12
13
// Encoder returns the encoded data
14
func (ss *StaticService) Encode(_ *http.Request, args *EncodeArgs, reply *EncodeReply) error {
15
bytes, err := formatting.Encode(args.Encoding, []byte(args.Data))
16
if err != nil {
17
return fmt.Errorf("couldn't encode data as string: %s", err)
18
}
19
reply.Bytes = bytes
20
reply.Encoding = args.Encoding
21
return nil
22
}
Copied!

Decode

This API method is the inverse of Encode.
1
// DecoderArgs are arguments for Decode
2
type DecoderArgs struct {
3
Bytes string `json:"bytes"`
4
Encoding formatting.Encoding `json:"encoding"`
5
}
6
7
// DecoderReply is the reply from Decoder
8
type DecoderReply struct {
9
Data string `json:"data"`
10
Encoding formatting.Encoding `json:"encoding"`
11
}
12
13
// Decoder returns the Decoded data
14
func (ss *StaticService) Decode(_ *http.Request, args *DecoderArgs, reply *DecoderReply) error {
15
bytes, err := formatting.Decode(args.Encoding, args.Bytes)
16
if err != nil {
17
return fmt.Errorf("couldn't Decode data as string: %s", err)
18
}
19
reply.Data = string(bytes)
20
reply.Encoding = args.Encoding
21
return nil
22
}
Copied!

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:
1
// Service is the API service for this VM
2
type Service struct{ vm *VM }
Copied!
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. For more information, see Interacting with the New Blockchain.

timestampvm.getBlock

Get a block by its ID. If no ID is provided, get the latest block.
Signature
1
timestampvm.getBlock({id: string}) ->
2
{
3
id: string,
4
data: string,
5
timestamp: int,
6
parentID: string
7
}
Copied!
    id is the ID of the block being retrieved. If omitted from arguments, gets the latest block
    data is the base 58 (with checksum) representation of the block’s 32 byte payload
    timestamp is the Unix timestamp when this block was created
    parentID is the block’s parent
Example Call
1
curl -X POST --data '{
2
"jsonrpc": "2.0",
3
"method": "timestampvm.getBlock",
4
"params":{
5
"id":"xqQV1jDnCXDxhfnNT7tDBcXeoH2jC3Hh7Pyv4GXE1z1hfup5K"
6
},
7
"id": 1
8
}' -H 'content-type:application/json;' 127.0.0.1:9650/ext/bc/sw813hGSWH8pdU9uzaYy9fCtYFfY7AjDd2c9rm64SbApnvjmk
Copied!
Example Response
1
{
2
"jsonrpc": "2.0",
3
"result": {
4
"timestamp": "1581717416",
5
"data": "11111111111111111111111111111111LpoYY",
6
"id": "xqQV1jDnCXDxhfnNT7tDBcXeoH2jC3Hh7Pyv4GXE1z1hfup5K",
7
"parentID": "22XLgiM5dfCwTY9iZnVk8ZPuPe3aSrdVr5Dfrbxd3ejpJd7oef"
8
},
9
"id": 1
10
}
Copied!
Implementation
1
// APIBlock is the API representation of a block
2
type APIBlock struct {
3
Timestamp int64 `json:"timestamp"` // Timestamp of most recent block
4
Data string `json:"data"` // Data in the most recent block. Base 58 repr. of 5 bytes.
5
ID string `json:"id"` // String repr. of ID of the most recent block
6
ParentID string `json:"parentID"` // String repr. of ID of the most recent block's parent
7
}
8
9
// GetBlockArgs are the arguments to GetBlock
10
type GetBlockArgs struct {
11
// ID of the block we're getting.
12
// If left blank, gets the latest block
13
ID string
14
}
15
16
// GetBlockReply is the reply from GetBlock
17
type GetBlockReply struct {
18
APIBlock
19
}
20
21
// GetBlock gets the block whose ID is [args.ID]
22
// If [args.ID] is empty, get the latest block
23
func (s *Service) GetBlock(_ *http.Request, args *GetBlockArgs, reply *GetBlockReply) error {
24
// If an ID is given, parse its string representation to an ids.ID
25
// If no ID is given, ID becomes the ID of last accepted block
26
var id ids.ID
27
var err error
28
if args.ID == "" {
29
id, err = s.vm.LastAccepted()
30
if err != nil {
31
return fmt.Errorf("problem finding the last accepted ID: %s", err)
32
}
33
} else {
34
id, err = ids.FromString(args.ID)
35
if err != nil {
36
return errors.New("problem parsing ID")
37
}
38
}
39
40
// Get the block from the database
41
blockInterface, err := s.vm.GetBlock(id)
42
if err != nil {
43
return errNoSuchBlock
44
}
45
46
block, ok := blockInterface.(*Block)
47
if