vms

package
v0.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 26, 2024 License: BSD-3-Clause Imports: 10 Imported by: 2

README

Snowman VMs

Recap of Odyssey Subnets

The Odyssey Network is composed of multiple validator sets and blockchains. A validator set defines a group of validators and their specified weights in the consensus process. A subnet is a validator set working together to achieve consensus on a set of blockchains. Every blockchain is validated by one subnet, and one subnet can validate many blockchains.

There is a special subnet inherent to the Odyssey Network called the Primary Network. The Primary Network is validated by every node on the Odyssey network. All subnets' validator sets are required to be a subset of the Primary Network's validator set. That is, if a validator belongs to a subnet then it also belongs to the Primary Network. The Primary Network validates three blockchains that are inherent to the Odyssey Network: the O-Chain, D-Chain, and A-Chain.

For each blockchain, consensus is driven by the consensus engine. For each subnet, the O-Chain, or Omega Chain, defines the validator set and the set of blockchains that are validated by the subnet.

A blockchain consists of two components: a consensus engine and a Virtual Machine (VM). The consensus engine samples validators, handles the responses, and pushes the results of the completed polls into the consensus code to decide which containers to Accept/Reject. The VM encodes the application logic for the blockchain. The VM defines the contents of a block, the rules for determining whether a block is valid, the APIs exposed to users, the state transition that occurs if a given block is accepted, and so on.

The consensus engine is general and agnostic to the application semantics of the blockchain. There are two consensus engine implementations in OdysseyGo: Snowman and Odyssey. Snowman provides a consensus engine for linear chains and Odyssey provides a consensus engine for DAGs. These consensus engine implementations can be re-used for multiple different blockchains in the Odyssey ecosystem, and each blockchain actually runs its own independent instance of consensus.

To launch a blockchain on Odyssey, you just need to write a VM that defines your application; the consensus part is handled by the existing consensus engine implementations.

This document will go into the details of implementing a ChainVM to run on the Snowman consensus engine. To implement a VM for snowman, we just need to implement the ChainVM interface defined here.

VMs are reusable. Arbitrarily many blockchains can run the same VM. Each blockchain has its own state. In this way, a VM is to a blockchain what a class is to an instance of a class in an object-oriented programming language.

Snowman VM From the Perspective of the Consensus Engine

To the consensus engine, the Snowman VM is a black box that handles all block building, parsing, and storage and provides a simple block interface for the consensus engine to call as it decides blocks.

Snowman VM Block Handling

The Snowman VM needs to implement the following functions used by the consensus engine during the consensus process.

Build Block

Build block allows the VM to propose a new block to be added to consensus.

The VM can send messages to the consensus engine through a toEngine channel that is passed in when the VM is initialized. This channel allows the VM to send the consensus engine a message when it is ready to build a block. For example, if the VM receives some transactions via gossip or from an API, then it will signal that it is ready to build a block by sending a PendingTxs message to the consensus engine. The PendingTxs message signals to the consensus engine that it should call BuildBlock() so that the block can be added to consensus.

The major caveat to this is the Snowman VMs are wrapped with Snowman++. Snowman++ provides congestion control by using a soft leader, where a leader is designated as the proposer that should create a block at a given time. Snowman++ gracefully falls back to increase the number of validators that are allowed to propose a block to handle the case that the leader does not propose a block in a timely manner.

Since a VM may be ready to build a block before its turn to propose a block according to Snowman++, the proposer VM will buffer PendingTxs messages until the ProposerVM agrees that it is time to build a block as well. This means that the consensus engine is not guaranteed to receive the PendingTxs message and call BuildBlock() in a timely manner.

When the consensus engine does call BuildBlock, the VM should build a block on top of the currently preferred block. This increases the likelihood that the block will be accepted since if the VM builds on top of a block that is not preferred, then the consensus engine is already leaning towards accepting something else, such that the newly created block will be more likely to get rejected.

Parse Block

Parse block provides the consensus engine the ability to parse a byte array into the block interface.

ParseBlock(bytes []byte) attempts to parse a byte array into a block, so that it can return the block interface to the consensus engine. ParseBlock can perform syntactic verification to ensure that a block is well formed. For example, if a certain field of a block is invalid such that the block can be immediately determined to not be valid, ParseBlock can immediately return an error so that the consensus engine does not need to do the extra work of verifying it.

It is required for all historical blocks to be parsable.

GetBlock

GetBlock fetches blocks that are already known to the VM. If the block has been verified, the VM is required to return that uniquified block when requested until Accept or Reject are called. After a block has been decided, it is no longer necessary to return a uniquified block, and if the block has been rejected the VM is no longer required to store that block or return it at all.

Set Preference

The VM implements the function SetPreference(blkID ids.ID) to allow the consensus engine to notify the VM which block is currently preferred to be accepted. The VM should use this information to set the head of its blockchain. Most importantly, when the consensus engine calls BuildBlock, the VM should be sure to build on top of the block that is the most recently set preference.

Note: SetPreference will always be called with a block that has no verified children.

Implementing the Snowman VM Block

From the perspective of the consensus engine, the state of the VM can be defined as a linear chain starting from the genesis block through to the last accepted block.

Following the last accepted block, the consensus engine may have any number of different blocks that are processing. The configuration of the processing set can be defined as a tree with the last accepted block as the root.

In practice, this looks like the following:

    G
    |
    .
    .
    .
    |
    L
    |
    A
  /   \
 B     C

In this example, G -> ... -> L is the linear chain of blocks that have already been accepted by the consensus engine.

A, with parent block L, has been issued into consensus and is currently in processing. Blocks B and C, both with parent block A, have also been issued into consensus and are currently in processing as well.

We will call this state a possible configuration of the ChainVM from the view of the consensus engine, and we will try to define clearly the set of possible steps from this configuration to the next, which the ChainVM must implement correctly.

Given a configuration of the consensus engine, there are three possible actions the consensus engine may take:

  1. The consensus engine will attempt to verify a block whose parent is either the last accepted block or is currently in consensus.
  2. The consensus engine may change its preference (update the block that it currently prefers to accept).
  3. The consensus engine may arrive at a decision and call Accept/Reject on a series of blocks.

If the consensus engine arrives at a decision, then it may have decided one or more blocks and will perform the following steps:

  1. Call Accept on a block
  2. Call Reject on all transitive conflicts
  3. Repeat steps 1-2 until there are no more blocks to accept

Therefore, if the tree of blocks in consensus (with root L, the last accepted block), looks like the following:

    L
  /   \
 A     B
 |    / \
 C   D   G
    / \ 
   E   F

If the consensus engine decides A and C simultaneously, the consensus engine would perform the following operations:

  1. Accept(A)
  2. Reject(B), Reject(D), Reject(E), Reject(F), and Reject(G)
  3. Accept(C)

To see the actual code where Accept/Reject are performed, look here.

Block Statuses

A block's status must be one of Accepted, Rejected, Processing.

A block that is Accepted or Rejected is considered to be Decided.

Processing Blocks

If a block has status Processing we have the byte representation of the block and have parsed it but have not decided it.

Additionally, blocks with status Processing may or may not have had Verify() called on them. Therefore, a block with status Processing hasn't necessarily been added to consensus. That is, the consensus engine may not be trying to decide the block. Before adding a block to consensus, the consensus engine verifies the block to ensure it's valid. The VM defines what constitutes a valid block for that blockchain.

For example, when a node receives a block from the network it will call ParseBlock(blockBytes) and return a block to the consensus engine. This block has not yet been fully verified and is not in consensus at this point. When the consensus engine has fetched its full ancestry and is ready to issue it to consensus, it will verify all of the block's ancestors, call Verify() on this block as well, and if it returns a nil error, then the block will be added to consensus.

Accepted Blocks

After Accept has been called on a block, it must report status Accepted. Accepted blocks must still be retrievable by VM method GetBlock.

Rejected Blocks

After Reject has been called on a block, it should report its status as rejected. This is not required to be maintained forever. After Reject has been called, it may be convenient to keep it in a caching layer so that its status is easily accessible. For performance reasons, it may be optimal to report the status of a block as rejected until the last accepted block height is >= the rejected block height, since at that point the consensus engine can see its height and immediately see that the block does not need to be processed again.

Uniquifying Blocks

The consensus engine requires that the VM return a unique reference to blocks when they are in consensus.

To repeat, from the perspective of the VM, a block is in consensus if Verify() has been called on it and it returned no error, but the block has not yet been decided. In other words, Verify() has successfully returned, but not yet been followed by Accept() or Reject().

When a block is processing, the VM needs to ensure that any function on the VM that gets called and returns a block, returns a reference to that same block that the consensus engine has a reference to.

After a block has been decided, the consensus engine will not call Verify(), Accept(), or Reject() on the same block again. Since the block will not go through consensus again, it's safe to return a non-unique block if that block has already been decided.

If a block is processing, but hasn't been verified (i.e. it hasn't been added to consensus) then it is also safe to return a non-unique block.

Once Verify() has been called and returns a non-nil error, the VM must subsequently return a reference to the same block.

This means that the VM needs to handle uniquification and leads to very specific requirements for how blocks are cached. This is why the chain package was implemented to create a simple helper that helps a VM implement an efficient caching layer while correctly uniquifying blocks. For an example of how it's used, you can look at the rpcchainvm.

Snowman VM APIs

The VM must also implement CreateHandlers() which can return a map of extensions mapped to HTTP handlers that will be added to the node's API server. This allows the VM to expose APIs for querying and interacting with the blockchain implemented by the API.

Documentation

Overview

Package vms is a generated GoMock package.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrNotFound = errors.New("not found")
)

Functions

This section is empty.

Types

type Factory

type Factory interface {
	New(logging.Logger) (interface{}, error)
}

A Factory creates new instances of a VM

type Manager

type Manager interface {
	ids.Aliaser

	// Return a factory that can create new instances of the vm whose ID is
	// [vmID]
	GetFactory(vmID ids.ID) (Factory, error)

	// Map [vmID] to [factory]. [factory] creates new instances of the vm whose
	// ID is [vmID]
	RegisterFactory(ctx context.Context, vmID ids.ID, factory Factory) error

	// ListFactories returns all the IDs that have had factories registered.
	ListFactories() ([]ids.ID, error)

	// Versions returns the primary alias of the VM mapped to the reported
	// version of the VM for all the registered VMs that reported versions.
	Versions() (map[string]string, error)
}

Manager tracks a collection of VM factories, their aliases, and their versions. It has the following functionality:

  1. Register a VM factory. To register a VM is to associate its ID with a VMFactory which, when New() is called upon it, creates a new instance of that VM.
  2. Get a VM factory. Given the ID of a VM that has been registered, return the factory that the ID is associated with.
  3. Manage the aliases of VMs
  4. Manage the versions of VMs

func NewManager

func NewManager(log logging.Logger, aliaser ids.Aliaser) Manager

NewManager returns an instance of a VM manager

type MockFactory

type MockFactory struct {
	// contains filtered or unexported fields
}

MockFactory is a mock of Factory interface.

func NewMockFactory

func NewMockFactory(ctrl *gomock.Controller) *MockFactory

NewMockFactory creates a new mock instance.

func (*MockFactory) EXPECT

func (m *MockFactory) EXPECT() *MockFactoryMockRecorder

EXPECT returns an object that allows the caller to indicate expected use.

func (*MockFactory) New

func (m *MockFactory) New(arg0 logging.Logger) (interface{}, error)

New mocks base method.

type MockFactoryMockRecorder

type MockFactoryMockRecorder struct {
	// contains filtered or unexported fields
}

MockFactoryMockRecorder is the mock recorder for MockFactory.

func (*MockFactoryMockRecorder) New

func (mr *MockFactoryMockRecorder) New(arg0 interface{}) *gomock.Call

New indicates an expected call of New.

type MockManager

type MockManager struct {
	// contains filtered or unexported fields
}

MockManager is a mock of Manager interface.

func NewMockManager

func NewMockManager(ctrl *gomock.Controller) *MockManager

NewMockManager creates a new mock instance.

func (*MockManager) Alias

func (m *MockManager) Alias(arg0 ids.ID, arg1 string) error

Alias mocks base method.

func (*MockManager) Aliases

func (m *MockManager) Aliases(arg0 ids.ID) ([]string, error)

Aliases mocks base method.

func (*MockManager) EXPECT

func (m *MockManager) EXPECT() *MockManagerMockRecorder

EXPECT returns an object that allows the caller to indicate expected use.

func (*MockManager) GetFactory

func (m *MockManager) GetFactory(arg0 ids.ID) (Factory, error)

GetFactory mocks base method.

func (*MockManager) ListFactories

func (m *MockManager) ListFactories() ([]ids.ID, error)

ListFactories mocks base method.

func (*MockManager) Lookup

func (m *MockManager) Lookup(arg0 string) (ids.ID, error)

Lookup mocks base method.

func (*MockManager) PrimaryAlias

func (m *MockManager) PrimaryAlias(arg0 ids.ID) (string, error)

PrimaryAlias mocks base method.

func (*MockManager) PrimaryAliasOrDefault

func (m *MockManager) PrimaryAliasOrDefault(arg0 ids.ID) string

PrimaryAliasOrDefault mocks base method.

func (*MockManager) RegisterFactory

func (m *MockManager) RegisterFactory(arg0 context.Context, arg1 ids.ID, arg2 Factory) error

RegisterFactory mocks base method.

func (*MockManager) RemoveAliases

func (m *MockManager) RemoveAliases(arg0 ids.ID)

RemoveAliases mocks base method.

func (*MockManager) Versions

func (m *MockManager) Versions() (map[string]string, error)

Versions mocks base method.

type MockManagerMockRecorder

type MockManagerMockRecorder struct {
	// contains filtered or unexported fields
}

MockManagerMockRecorder is the mock recorder for MockManager.

func (*MockManagerMockRecorder) Alias

func (mr *MockManagerMockRecorder) Alias(arg0, arg1 interface{}) *gomock.Call

Alias indicates an expected call of Alias.

func (*MockManagerMockRecorder) Aliases

func (mr *MockManagerMockRecorder) Aliases(arg0 interface{}) *gomock.Call

Aliases indicates an expected call of Aliases.

func (*MockManagerMockRecorder) GetFactory

func (mr *MockManagerMockRecorder) GetFactory(arg0 interface{}) *gomock.Call

GetFactory indicates an expected call of GetFactory.

func (*MockManagerMockRecorder) ListFactories

func (mr *MockManagerMockRecorder) ListFactories() *gomock.Call

ListFactories indicates an expected call of ListFactories.

func (*MockManagerMockRecorder) Lookup

func (mr *MockManagerMockRecorder) Lookup(arg0 interface{}) *gomock.Call

Lookup indicates an expected call of Lookup.

func (*MockManagerMockRecorder) PrimaryAlias

func (mr *MockManagerMockRecorder) PrimaryAlias(arg0 interface{}) *gomock.Call

PrimaryAlias indicates an expected call of PrimaryAlias.

func (*MockManagerMockRecorder) PrimaryAliasOrDefault

func (mr *MockManagerMockRecorder) PrimaryAliasOrDefault(arg0 interface{}) *gomock.Call

PrimaryAliasOrDefault indicates an expected call of PrimaryAliasOrDefault.

func (*MockManagerMockRecorder) RegisterFactory

func (mr *MockManagerMockRecorder) RegisterFactory(arg0, arg1, arg2 interface{}) *gomock.Call

RegisterFactory indicates an expected call of RegisterFactory.

func (*MockManagerMockRecorder) RemoveAliases

func (mr *MockManagerMockRecorder) RemoveAliases(arg0 interface{}) *gomock.Call

RemoveAliases indicates an expected call of RemoveAliases.

func (*MockManagerMockRecorder) Versions

func (mr *MockManagerMockRecorder) Versions() *gomock.Call

Versions indicates an expected call of Versions.

Directories

Path Synopsis
block
Package block is a generated GoMock package.
Package block is a generated GoMock package.
block/executor
Package executor is a generated GoMock package.
Package executor is a generated GoMock package.
fxs
metrics
Package metrics is a generated GoMock package.
Package metrics is a generated GoMock package.
states
Package states is a generated GoMock package.
Package states is a generated GoMock package.
txs
Package txs is a generated GoMock package.
Package txs is a generated GoMock package.
txs/mempool
Package mempool is a generated GoMock package.
Package mempool is a generated GoMock package.
components
dione
Package dione is a generated GoMock package.
Package dione is a generated GoMock package.
feecollector
Package feecollector is a generated GoMock package.
Package feecollector is a generated GoMock package.
verify
Package verify is a generated GoMock package.
Package verify is a generated GoMock package.
api
blocks
Package blocks is a generated GoMock package.
Package blocks is a generated GoMock package.
blocks/executor
Package executor is a generated GoMock package.
Package executor is a generated GoMock package.
fx
Package fx is a generated GoMock package.
Package fx is a generated GoMock package.
state
Package state is a generated GoMock package.
Package state is a generated GoMock package.
txs
Package txs is a generated GoMock package.
Package txs is a generated GoMock package.
txs/builder
Package builder is a generated GoMock package.
Package builder is a generated GoMock package.
txs/mempool
Package mempool is a generated GoMock package.
Package mempool is a generated GoMock package.
utxo
Package utxo is a generated GoMock package.
Package utxo is a generated GoMock package.
Package proposervm is a generated GoMock package.
Package proposervm is a generated GoMock package.
proposer
Package proposer is a generated GoMock package.
Package proposer is a generated GoMock package.
state
Package state is a generated GoMock package.
Package state is a generated GoMock package.
Package registry is a generated GoMock package.
Package registry is a generated GoMock package.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL