Raj Ranjan
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights New
    • Engagement control
    • Make a copy
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Note Insights Versions and GitHub Sync Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Make a copy Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       Owned this note    Owned this note      
    Published Linked with GitHub
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    # How to Build a Complex VM This is part of a series of tutorials for building a Virtual Machine (VM): - [Introduction to Virtual Machines](./introduction-to-vm.md) - [How to Build a Simple VM](./create-a-vm-timestampvm.md) - How to Build a Complex VM (this article) ## Introduction In this tutorial, we will learn all about how we can build a virtual machine by referencing [BlobVM](https://github.com/ava-labs/blobvm). It's a Virtual Machine that can be used to instantiate key-value blockchains for storing files like images, videos, etc. in an efficient way. Blobs are small chunks of data. In BlobVM, we divide a file into small blobs and store them as a key-value pair. The key of these small chunks of a file is linked together as the children of the root. This tree is also stored as JSON data against the root's key. ## Prerequisites Make sure you have followed the previous tutorials in this series: - [Introduction to Virtual Machines](./introduction-to-vm.md) - [How to Build a Simple VM](./create-a-vm-timestampvm.md) ## Components of the BlobVM The main task of a VM is to represent initial state (genesis state) and block structure containing state transitioning details. One of the most common structure to represent state transitions is **transaction**. When a block with transactions, is applied to the current state, the state transition will happen by simply executing the transactions. The order of transactions will matter here. BlobVM has the following components to handle the tasks from transaction to block acceptance: - **Transaction** - Transaction structure, initialization, execution etc. - **Mempool** - Heap (min and max) for keeping pending transactions locally on the node. - **Network** - Implements gossip of transactions from mempool to the network. - **Block** - Handles block creation, initialization, verification, parsing, etc. - **Builder** - Returns the built preferred block by including transactions from the mempool. - **Block Builder** - Initiates the gossip of new transactions, notifies the engine that new blocks are ready to be built, and also handles next block generation notification. - **Storage** - Stores and retrieves data of the chain's state. - **Virtual Machine** - Entry point for all the components to orchestrate them according to the consensus engine and API requests. - **Service** - API handlers for interacting with VM and initialized chain. - **Factory** - For creating new instances of the Virtual Machine. ## Transaction Lifecycle in BlobVM A Virtual Machine exposes APIs or handlers for users to make direct RPC to service. Every change on a chain happens through blocks and more specifically, transactions. A VM handles transactions internally since the consensus engine only cares about the block. Let's see, how a transaction goes through the network to update the chain's state: - User make RPC request to `service.IssueRawTx()` - `service.IssueRawTx()` is called using handlers to - Receive transaction bytes as arguments - Unmarshal bytes into a transaction object - Initialize transaction object with message digest, txID, etc. - Submit the transaction to VM - The transaction is submitted to the Virtual Machine - `vm.Submit()` to - Get the preferred (last accepted) block - Get the execution context of the preferred block that includes - Recent TxIDs (Txns in the lookback window e.g last 10s as defined in Genesis) - Recent BlockIDs - Recent Price and Cost - Next Price, Next Cost, etc. - Execute transaction locally with execution context, dummy database, and block - Add valid transaction to mempool - `mempool.newTxs` - Call `mempool.addPending()` to signal VM to build block for the newly added tx - The node running the VM, gossips new transactions to its peers from `mempool.newTxs` at regular intervals - VM signals consensus engine to build blocks out of pending transactions in the mempool - ProposerVM delays the request until it is the node's turn to propose a block - The consensus engine calls `vm.BuildBlock()` to get the block from VM - Engine calls `block.Verify()` method - Successfully verified blocks have been gossiped within the network for consensus - Blocks containing transactions are accepted or rejected according to the consensus results - Accepted blocks and related data are committed to the node's database ## Coding the Virtual Machine We have divided the components into 3 packages. We will be looking at each of these files and learning about their functions. - **[vm](https://github.com/ava-labs/blobvm/tree/master/vm)** - block_builder.go - chain_vm.go - network.go - service.go - vm.go - **[chain](https://github.com/ava-labs/blobvm/tree/master/chain)** - unsigned_tx.go - base_tx.go - transfer_tx.go - set_tx.go - tx.go - block.go - mempool.go - storage.go - builder.go - **[mempool](https://github.com/ava-labs/blobvm/tree/master/mempool)** - mempool.go ### Transactions At the very basic level, the state of a chain can only be updated by issuing a signed transaction. A signed transaction contains an unsigned transaction and a signature (of the sender). The signature is necessary to identify the sender. In a Virtual Machine, we can have multiple types of unsigned transactions, to achieve different tasks. In BlobVM, we have 2 types of unsigned transactions: - [TransferTx](https://github.com/ava-labs/blobvm/blob/master/chain/transfer_tx.go) - For transferring coins between the accounts - [SetTx](https://github.com/ava-labs/blobvm/blob/master/chain/set_tx.go) - For storing blob data on the chain A complete transaction consists of 2 parts: - Unsigned Transaction - Signature #### UnsignedTx All [unsigned transactions](https://github.com/ava-labs/blobvm/blob/master/chain/unsigned_tx.go) have a common basic functionality along with their customizations. `BaseTx` handles all the common functionality. For eg. `SetTx` and `TransferTx` have their implementation of the unsigned transaction, with a common extension from `BaseTx`. This will be explained later in the documentation. `TransferTx` and `SetTx` are unsigned transactions and have to implement the following methods: ```go type UnsignedTransaction interface { Copy() UnsignedTransaction GetBlockID() ids.ID GetMagic() uint64 GetPrice() uint64 SetBlockID(ids.ID) SetMagic(uint64) SetPrice(uint64) FeeUnits(*Genesis) uint64 // number of units to mine tx LoadUnits(*Genesis) uint64 // units that should impact fee rate ExecuteBase(*Genesis) error Execute(*TransactionContext) error TypedData() *tdata.TypedData Activity() *Activity } ``` #### BaseTx Most of the methods of an unsigned transaction are implemented by the [`BaseTx`](https://github.com/ava-labs/blobvm/blob/master/chain/base_tx.go). The values like blockID, transaction price, and magic number are set while creating an unsigned transaction. Let's look at these basic methods: - [`SetBlockID()`](https://github.com/ava-labs/blobvm/blob/master/chain/base_tx.go#L26) sets the block ID for the last accepted block. - [`GetBlockID()`](https://github.com/ava-labs/blobvm/blob/master/chain/base_tx.go#L22) returns the block ID. - [`SetMagic()`](https://github.com/ava-labs/blobvm/blob/master/chain/base_tx.go#L34) sets the magic number that will differentiate chains to prevent replay attacks - [`GetMagic()`](https://github.com/ava-labs/blobvm/blob/master/chain/base_tx.go#L30) gets the magic number. Magic number is defined in genesis. - [`SetPrice()`](https://github.com/ava-labs/blobvm/blob/master/chain/base_tx.go#L42) sets the price per fee unit for this transaction. - [`GetPrice()`](https://github.com/ava-labs/blobvm/blob/master/chain/base_tx.go#L38) gets the price for this transaction. - [`FeeUnits()`](https://github.com/ava-labs/blobvm/blob/master/chain/base_tx.go#L59) returns the fee units this transaction will consume. - [`LoadUnits()`](https://github.com/ava-labs/blobvm/blob/master/chain/base_tx.go#L63) same as fee units that return the units this transaction will consume. - [`ExecuteBase()`](https://github.com/ava-labs/blobvm/blob/master/chain/base_tx.go#L46) executes the basic checks for a transaction like magic number validation, minimum transaction price, etc. ```go func (b *BaseTx) ExecuteBase(g *Genesis) error { if b.BlockID == ids.Empty { return ErrInvalidBlockID } if b.Magic != g.Magic { return ErrInvalidMagic } if b.Price < g.MinPrice { return ErrInvalidPrice } return nil } ``` - [`Execute()`](https://github.com/ava-labs/blobvm/blob/master/chain/unsigned_tx.go#L34) executes the specific check for a transaction and may perform state change on the database instance provided as an argument. Each type of transaction should implement its own execute method. For example, `TransferTx` execute balance modification, i.e. add transfer amount to the receiver and deduct the same amount from the sender. A transaction is executed 2 times. Before [including](https://github.com/ava-labs/blobvm/blob/master/vm/vm.go#L428) it in mempool and during [verification](https://github.com/ava-labs/blobvm/blob/master/chain/block.go#L213) of the block containing this transaction. As mentioned earlier, verification of a block happens before gossiping it within the network for consensus. During a transaction's inclusion in the mempool, it is executed with a dummy database, and the database is aborted after the transaction's execution, so that, the transaction state is not saved to the disk. Whereas during a block's verification, all the transactions in the block is executed with the parent's database instance. After the block is accepted, the database is committed, so that, all the transaction's state is saved to the disk. By committing or aborting a database, we save or reject its state on the disk Let's have a detailed look on `TransferTx` and `SetTx`: #### TransferTx We create a transaction of type `TransferTx` when we want to transfer coins from one account to another. We can create this unsigned transaction using the `TransferTx` [struct](https://github.com/ava-labs/blobvm/blob/master/chain/transfer_tx.go#L16). ```go type TransferTx struct { *BaseTx `serialize:"true" json:"baseTx"` // To is the recipient of the [Units]. To common.Address `serialize:"true" json:"to"` // Units are transferred to [To]. Units uint64 `serialize:"true" json:"units"` } ``` As you can see above, the `TransferTx` also includes `BaseTx` because most of the methods of an unsigned transaction are already implemented there. The most important method that every unsigned transaction should implement is [`Execute()`](https://github.com/ava-labs/blobvm/blob/master/chain/transfer_tx.go#L26) method. It performs transaction-related checks that are not present in basic transactions and can also change the state. ```go func (t *TransferTx) Execute(c *TransactionContext) error { // Must transfer to someone if bytes.Equal(t.To[:], zeroAddress[:]) { return ErrNonActionable } // This prevents someone from transferring to themselves. if bytes.Equal(t.To[:], c.Sender[:]) { return ErrNonActionable } if t.Units == 0 { return ErrNonActionable } if _, err := ModifyBalance(c.Database, c.Sender, false, t.Units); err != nil { return err } if _, err := ModifyBalance(c.Database, t.To, true, t.Units); err != nil { return err } return nil } ``` The execution method above does the checks like address should not be empty, sender and receiver should not be same and fee units should not be 0. It also performs the state change by modifying the sender and receiver's balance. #### SetTx Transaction of type `SetTx` is used for storing blob data on the chain. We can create this unsigned transaction by using the `SetTx` [struct](https://github.com/ava-labs/blobvm/blob/master/chain/set_tx.go#L15). The `value` field holds the blob bytes. ```go type SetTx struct { *BaseTx `serialize:"true" json:"baseTx"` Value []byte `serialize:"true" json:"value"` } ``` `SetTx` has also implemented its own [FeeUnits](https://github.com/ava-labs/blobvm/blob/master/chain/set_tx.go#L48) method. This is to compensate the network according to the size of the blob a particular transaction wants to store. ```go func (s *SetTx) FeeUnits(g *Genesis) uint64 { // We don't subtract by 1 here because we want to charge extra for any // value-based interaction (even if it is small or a delete). return s.BaseTx.FeeUnits(g) + valueUnits(g, uint64(len(s.Value))) } ``` The [`Execute()`](https://github.com/ava-labs/blobvm/blob/master/chain/set_tx.go#L21) method for `SetTx` does specific checks like blob value should not be empty, its size should not exceed maximum allowed limit and blob should not be already existing by comparing the blob's hash. ```go func (s *SetTx) Execute(t *TransactionContext) error { g := t.Genesis switch { case len(s.Value) == 0: return ErrValueEmpty case uint64(len(s.Value)) > g.MaxValueSize: return ErrValueTooBig } k := ValueHash(s.Value) // Do not allow duplicate value setting _, exists, err := GetValueMeta(t.Database, k) if err != nil { return err } if exists { return ErrKeyExists } return PutKey(t.Database, k, &ValueMeta{ Size: uint64(len(s.Value)), TxID: t.TxID, Created: t.BlockTime, }) } ``` Apart from typical checks, it will also store the [metadata](https://github.com/ava-labs/blobvm/blob/master/chain/set_tx.go#L41) on the provided database instance. #### Signed Transaction We cannot issue unsigned transactions to the network. The sender needs to add its signature with the unsigned transaction to make a [signed transaction](https://github.com/ava-labs/blobvm/blob/master/chain/tx.go). A signature is basically an [ECDSA](https://cryptobook.nakov.com/digital-signatures/ecdsa-sign-verify-messages) signature (using sender's private key) of the [KECCAK256](https://keccak.team/keccak.html) hash of [typed](https://eips.ethereum.org/EIPS/eip-712) unsigned transaction data (Digest hash). ```go type Transaction struct { UnsignedTransaction `serialize:"true" json:"unsignedTransaction"` Signature []byte `serialize:"true" json:"signature"` digestHash []byte bytes []byte id ids.ID size uint64 sender common.Address } ``` A new signed transaction can be created using [`NewTx`](https://github.com/ava-labs/blobvm/blob/master/chain/tx.go#L25) function by passing the unsigned transaction and the signature. To populate the rest of the fields like digestHash, bytes, id, etc. we can call the new transaction's [`init()`](https://github.com/ava-labs/blobvm/blob/master/chain/tx.go#L45) method. The code is self-explanatory. ```go func NewTx(utx UnsignedTransaction, sig []byte) *Transaction { return &Transaction{ UnsignedTransaction: utx, Signature: sig, } } ``` The unsigned transaction's `Execute()` method is never called directly, even before adding it to mempool or during verification of a block. The `Execute()` method of the signed transaction performs the execution of the underlying unsigned transaction. It performs the following task: - Executes the basic part of the unsigned transaction (`ExecuteBase`). - Parent block of the transaction (blockID) should be one of the recent blocks. - Transaction ID must not be recently executed. - Modify the sender's balance to reduce transaction fees. - Checks if transaction price per unit is more than the next expected block's price. - Execute the underlying unsigned transaction. - Set transaction ID as the key on the database passed as an argument. ```go func (t *Transaction) Execute(g *Genesis, db database.Database, blk *StatelessBlock, context *Context) error { if err := t.UnsignedTransaction.ExecuteBase(g); err != nil { return err } if !context.RecentBlockIDs.Contains(t.GetBlockID()) { // Hash must be recent to be any good // Should not happen beause of mempool cleanup return ErrInvalidBlockID } if context.RecentTxIDs.Contains(t.ID()) { // Tx hash must not be recently executed (otherwise could be replayed) // // NOTE: We only need to keep cached tx hashes around as long as the // block hash referenced in the tx is valid return ErrDuplicateTx } // Ensure sender has balance if _, err := ModifyBalance(db, t.sender, false, t.FeeUnits(g)*t.GetPrice()); err != nil { return err } if t.GetPrice() < context.NextPrice { return ErrInsufficientPrice } if err := t.UnsignedTransaction.Execute(&TransactionContext{ Genesis: g, Database: db, BlockTime: uint64(blk.Tmstmp), TxID: t.id, Sender: t.sender, }); err != nil { return err } if err := SetTransaction(db, t); err != nil { return err } return nil } ``` Here's the overview on how to create and issue a signed transaction: - Create an unsigned transaction with required fields. ```go utx := &chain.SetTx{ BaseTx: &chain.BaseTx{}, Value: "chunk data", } ``` - Set unsigned transaction parameters. ```go utx.SetBlockID(la) utx.SetMagic(g.Magic) utx.SetPrice(price + blockCost/utx.FeeUnits(g)) ``` - Calculate [digest hash](https://github.com/ava-labs/blobvm/blob/master/chain/tx.go#L41) for unsigned transaction. ```go dh, err := chain.DigestHash(utx) ``` - [Sign](https://github.com/ava-labs/blobvm/blob/master/chain/crypto.go#L17) the digest hash with the sender's private key. ```go sig, err := chain.Sign(dh, priv) ``` - Create and initialize the new signed transaction. ```go tx := chain.NewTx(utx, sig) if err := tx.Init(g); err != nil { return ids.Empty, 0, err } ``` - Issue the raw transaction bytes to the client ```go txID, err = cli.IssueRawTx(ctx, tx.Bytes()) ``` ### Mempool [Mempool](https://github.com/ava-labs/blobvm/blob/master/mempool/mempool.go) is temporary memory for storing pending transactions. These are maintained at the local node level and can be flushed out when the node restarts. The new transactions received directly from a user or through app gossip, are stored inside this mempool. Mempool is ideally a Max Heap that pushes new transactions into the heap according to the transaction's price. It keeps the transaction with the highest price at the top. Mempool is [initialized](https://github.com/ava-labs/blobvm/blob/master/vm/vm.go#L151) during the initialization of VM. ```go vm.mempool = mempool.New(vm.genesis, vm.config.MempoolSize) ``` Whenever a transaction is submitted to VM, it first gets initialized, verified, and executed locally. If the transaction looks valid, then it is [added](https://github.com/ava-labs/blobvm/blob/master/vm/vm.go#L431) to mempool. ```go vm.mempool.Add(tx) ``` `Add` method of mempool is implemented in the [mempool.go](https://github.com/ava-labs/blobvm/blob/master/mempool/mempool.go#L43) file. It has the following functions: - Verifies if the transaction ID exists in the mempool or not - Optimistically add the transaction to the max heap - If the max heap size is larger than the configured size, then it will pop the minimum price tx. - Add the transaction to `mempool.newTxs` - Add notification in the `mempool.Pending` channel to indicate production of a new block There are many other methods implemented. The code is well commented for you to understand. ### Block Builder [Block builder](https://github.com/ava-labs/blobvm/blob/master/vm/block_builder.go) acts as a notifying service to the consensus engine. It serves the following functions: - Regularly initiates the gossip of new transactions - Regularly notifies the consensus engine that new blocks are ready to be built - Handles next block generation notification VM has 3 block building status: - **dontBuild** - There are no pending transactions and so block building is halted - **building** - The engine has been notified and the block building is in process. - **mayBuild** - There are pending transactions but waiting for the build interval before notifying During initialization of VM, [NewTimeBuilder](https://github.com/ava-labs/blobvm/blob/master/vm/block_builder.go#L79) is returned to VM as `vm.builder`. The NewTimeBuilder also implements `Build()` and `Gossip()` methods and are invoked during VM initialization. ```go go vm.builder.Build() go vm.builder.Gossip() ``` [Gossip()](https://github.com/ava-labs/blobvm/blob/master/vm/block_builder.go#L183) method initiates gossip of new transactions from the mempool at regular intervals as set initially by `vm.config.GossipInterval`. Functions of [Build()](https://github.com/ava-labs/blobvm/blob/master/vm/block_builder.go#L166) method: - Calls `signalTxsReady()` whenever it receives pending transaction signal from mempool - `signalTxsReady()` method does nothing if the status is other than `dontBuild` - If status is `dontBuild`, it will call `markBuilding()` method - `markBuilding()` notifies consensus engine that it has pending transactions - And will set the block status to `building` Whenever the consensus engine calls `vm.BuildBlock()`, the VM builds the block from the transactions in mempool and then calls the block builder method [HandleGenerateBlock()](https://github.com/ava-labs/blobvm/blob/master/vm/block_builder.go#L121). Let's have a look at this function: - If there are still pending transactions, then it will set the status to `mayBuild` - After BuildInterval, as set during VM initialization, it will call `buildBlockTwoStageTimer()`. - `buildBlockTwoStageTimer` will call the `markBuilding()` method if the status is `mayBuild` It also has 3 channels for gracefully shutting down VM. ```go vm.doneBuild = make(chan struct{}) vm.doneGossip = make(chan struct{}) vm.builderStop = make(chan struct{}) ``` `doneBuild` and `doneGossip` will prevent shutting down until `Build()` and `Gossip()` are stopped. Whereas `builderStop` is the channel to stop `Build()` and `Gossip()`. See the below [snippet](https://github.com/ava-labs/blobvm/blob/master/vm/vm.go#L228) from `blobvm/vm/vm.go` ```go func (vm *VM) Shutdown() error { close(vm.stop) <-vm.doneBuild <-vm.doneGossip if vm.ctx == nil { return nil } return vm.db.Close() } ``` ### Network [Network](https://github.com/ava-labs/blobvm/blob/master/vm/network.go) has the responsibility to gossip new transactions from the node's local mempool to every peer of the node. It implements the `GossipNewTxs()` function that will be called by the block builder at regular intervals. ```go func (n *PushNetwork) GossipNewTxs(newTxs []*chain.Transaction) error { txs := []*chain.Transaction{} // Gossip at most the target units of a block at once for _, tx := range newTxs { if _, exists := n.gossipedTxs.Get(tx.ID()); exists { log.Debug("already gossiped, skipping", "txId", tx.ID()) continue } n.gossipedTxs.Put(tx.ID(), nil) txs = append(txs, tx) } return n.sendTxs(txs) } ``` It puts the recently gossiped transactions into a cache, so that, we do not have to re-send these transactions. Block builder also calls `RegossipTxs()` that will pop the transactions from mempool and gossip them even if they are in the cache. The actual transfer of transactions happens through the `sendTxs()` method. It then sends the transactions as bytes to the `avalanchego` service through `appSender` that is being provided to VM during initialization. The `avalanchego` then gossips the transactions to the subnet's validators. ```go title="/blobvm/vm/network.go" func (n *PushNetwork) sendTxs(txs []*chain.Transaction) error { if err := n.vm.appSender.SendAppGossip(b); err != nil { log.Warn( "GossipTxs failed", "error", err, ) return err } } ``` The other validators similarly receive incoming transactions from other validators via the `avalanchego` service on `vm/network.go`'s `AppGossip()` method. Once transaction bytes are received, it submits unmarshalled transaction objects to the VM. ```go func (vm *VM) AppGossip(nodeID ids.NodeID, msg []byte) error { txs := make([]*chain.Transaction, 0) if _, err := chain.Unmarshal(msg, &txs); err != nil { return nil } // submit incoming gossip log.Debug("AppGossip transactions are being submitted", "txs", len(txs)) if errs := vm.Submit(txs...); len(errs) > 0 { for _, err := range errs { } } return nil } ``` ### Block A block before getting accepted (or rejected) and being committed to the database, goes through a proposal by a node, verification, and consensus. Once it's persisted on the chain's state it becomes a **stateful** block. Until then it's a **stateless** block. After acceptance, its stateless version is put into the cache, and the stateful version is put into the database. [Stateful block](https://github.com/ava-labs/blobvm/blob/master/chain/block.go#L26) has only required fields in its structure like parentID, timestamp, height, transactions, etc. since it will be stored on the database. ```go type StatefulBlock struct { Prnt ids.ID `serialize:"true" json:"parent"` Tmstmp int64 `serialize:"true" json:"timestamp"` Hght uint64 `serialize:"true" json:"height"` Price uint64 `serialize:"true" json:"price"` Cost uint64 `serialize:"true" json:"cost"` AccessProof common.Hash `serialize:"true" json:"accessProof"` Txs []*Transaction `serialize:"true" json:"txs"` } ``` Whereas [Stateless block](https://github.com/ava-labs/blobvm/blob/master/chain/block.go#L39) contains parameters like the block's ID, status, timestamp, database instance (for getting stored when accepted), along with the aspiring stateful block. A stateless block is never written on the database but remains in the node's memory or cache. ```go type StatelessBlock struct { *StatefulBlock `serialize:"true" json:"block"` id ids.ID st choices.Status t time.Time bytes []byte vm VM children []*StatelessBlock onAcceptDB *versiondb.Database } ``` Let's have a look at the fields of StatelessBlock: - **StatefulBlock** - It is the block that will be committed to the database once it is accepted. - **bytes** - It is the serialized form of the StatefulBlock - **id** - It is the Keccak256 hash of the bytes field. - **st** - It is the status of the block and could be - processing, accepted, or rejected. - **children** - To store the child blocks in the stateless block - **onAcceptDB** - It is a DB instance that we can use to save the block to the database The VM signals the consensus engine to build a block, whenever a new transaction is added to the mempool. But ProposerVM will delay the notification until it is the node's turn to build the block. When it is the node's turn, the consensus engine will receive the notification and will call the VM's `BuildBlock()` method. When the consensus engine calls VM to build a block, the VM invokes [`NewBlock()`](https://github.com/ava-labs/blobvm/blob/master/chain/block.go#L52) function to get the stateless block using arguments parent block, timestamp and recent context. ```go func NewBlock(vm VM, parent snowman.Block, tmstp int64, context *Context) *StatelessBlock { return &StatelessBlock{ StatefulBlock: &StatefulBlock{ Tmstmp: tmstp, Prnt: parent.ID(), Hght: parent.Height() + 1, Price: context.NextPrice, Cost: context.NextCost, }, vm: vm, st: choices.Processing, } } ``` A newly created block can be initialized with necessary details of a stateless block like block ID, bytes, and timestamp using the block's [`init()`](https://github.com/ava-labs/blobvm/blob/master/chain/block.go#L112) method. This method is generally called when we have complete and final information about the stateful block inside. For example, this method is called at the end of the builder's [`chain.BuildBlock()`](https://github.com/ava-labs/blobvm/blob/master/chain/builder.go#L85) method. ```go func (b *StatelessBlock) init() error { bytes, err := Marshal(b.StatefulBlock) if err != nil { return err } b.bytes = bytes id, err := ids.ToID(crypto.Keccak256(b.bytes)) if err != nil { return err } b.id = id b.t = time.Unix(b.StatefulBlock.Tmstmp, 0) g := b.vm.Genesis() for _, tx := range b.StatefulBlock.Txs { if err := tx.Init(g); err != nil { return err } } return nil } ``` We can also get the initialized stateless block from the stateful block using the [`ParseStatefulBlock()`](https://github.com/ava-labs/blobvm/blob/master/chain/block.go#L78) method. This is generally used when we have fetched a stateful block from the database, but we also need a stateless block, as used [here](https://github.com/ava-labs/blobvm/blob/master/vm/vm.go#L348). A block once built, has 3 states: - **Verified** - A verified block can be accepted or rejected - Store block to database (will not be saved until accepted) - Add to `vm.verifiedBlocks` - Remove block transactions from mempool - **Rejected** - Delete block from `vm.verifiedBlocks` - Re-add txns to mempool - **Accepted** - Commit block to database to permanently store it - Delete block from `vm.verifiedBlocks` - Add block to `vm.blocks` cache When the consensus engine receives the built block, it calls the block's [`Verify()`](https://github.com/ava-labs/blobvm/blob/master/chain/block.go#L227) method. This method serves the following functions\* - - At least 1 transaction and block timestamp should be within 10s in the future (futureBound). ```go if len(b.Txs) == 0 { return nil, nil, ErrNoTxs } if b.Timestamp().Unix() >= time.Now().Add(futureBound).Unix() { return nil, nil, ErrTimestampTooLate } ``` - Transactions' total gas units should not exceed the block gas limit. ```go blockSize := uint64(0) for _, tx := range b.Txs { blockSize += tx.LoadUnits(g) if blockSize > g.MaxBlockSize { return nil, nil, ErrBlockTooBig } } ``` - Verify the parent block is available and has a timestamp earlier than the block timestamp. ```go parent, err := b.vm.GetStatelessBlock(b.Prnt) if err != nil { log.Debug("could not get parent", "id", b.Prnt) return nil, nil, err } if b.Timestamp().Unix() < parent.Timestamp().Unix() { return nil, nil, ErrTimestampTooEarly } ``` - Check cost and price for the current block with respect to the lookback window from the parent block timestamp. This information is provided in the [execution context](https://github.com/ava-labs/blobvm/blob/master/vm/chain_vm.go#L64) of the block. ```go context, err := b.vm.ExecutionContext(b.Tmstmp, parent) if err != nil { return nil, nil, err } if b.Cost != context.NextCost { return nil, nil, ErrInvalidCost } if b.Price != context.NextPrice { return nil, nil, ErrInvalidPrice } ``` - Create a new DB instance on top of the parent's DB instance, and link it with block's `onAccept`. ```go parentState, err := parent.onAccept() if err != nil { return nil, nil, err } onAcceptDB := versiondb.New(parentState) ``` Now using this `onAccept`, we can commit the block to the chain's state (or database). - Check if the extra fee from all the included transactions is greater than the required surplus ```go surplusFee := uint64(0) for _, tx := range b.Txs { if err := tx.Execute(g, onAcceptDB, b, context); err != nil { return nil, nil, err } surplusFee += (tx.GetPrice() - b.Price) * tx.FeeUnits(g) } // Ensure enough fee is paid to compensate for block production speed requiredSurplus := b.Price * b.Cost if surplusFee < requiredSurplus { return nil, nil, fmt.Errorf("%w: required=%d found=%d", ErrInsufficientSurplus, requiredSurplus, surplusFee) } ``` The verification is not complete until it saves the verified state on the local memory: - Put the block on the database against the block's ID as its key - Put the block ID as the last accepted block on the database - Put the block to the VM's verified block's map Note that the block is not saved to the database until it's accepted i.e. the `Accept()` method is called. ```go // Set last accepted block and store if err := SetLastAccepted(b.onAcceptDB, b); err != nil { return err } parent.addChild(b) b.vm.Verified(b) ``` [`SetLastAccepted()`](https://github.com/ava-labs/blobvm/blob/master/chain/storage.go#L193) function shown above will set the last accepted block ID and also store the block to database memory, but will not commit until it is accepted. Once a block is verified, it will be sent to the network to achieve consensus on it. The verified blocks has 2 verdicts - **Accepted** or **Rejected**. A block's [`Accept()`](https://github.com/ava-labs/blobvm/blob/master/chain/block.go#L246) or [`Reject()`](https://github.com/ava-labs/blobvm/blob/master/chain/block.go#L261) method will be called depending upon the consensus result. ```go func (b *StatelessBlock) Accept() error { if err := b.onAcceptDB.Commit(); err != nil { return err } for _, child := range b.children { if err := child.onAcceptDB.SetDatabase(b.vm.State()); err != nil { return err } } b.st = choices.Accepted b.vm.Accepted(b) return nil } // implements "snowman.Block.choices.Decidable" func (b *StatelessBlock) Reject() error { b.st = choices.Rejected b.vm.Rejected(b) return nil } ``` ### Builder [Builder](https://github.com/ava-labs/blobvm/blob/master/chain/builder.go) implements the block building and returns the built block to the caller. It tries to build a new block from the preferred block as parent and transactions from mempool. It has a `BuildBlock()` function that is called when consensus engine called `vm.BuildBlock()`. It performs the following task: - Get the parent's stateless block using the preferred ID ```go parent, err := vm.GetStatelessBlock(preferred) ``` - Get execution context that has recent block IDs, recent transaction IDs, next prices, etc. ```go context, err := vm.ExecutionContext(nextTime, parent) ``` - Gets a new block, based on the above information ```go b := NewBlock(vm, parent, nextTime, context) ``` - Prune the mempool that belongs to recent blocks ```go mempool.Prune(context.RecentBlockIDs) ``` - Loads the transaction from mempool to aspiring block. It iterates through all the mempool transactions but ignores transactions whose gas units are [exceeding](https://github.com/ava-labs/blobvm/blob/master/chain/builder.go#L65) the remaining block limit, or whose price is [less](https://github.com/ava-labs/blobvm/blob/master/chain/builder.go#L59) that this block's price. ```go for mempool.Len() > 0 { next, price := mempool.PopMax() if price < b.Price { mempool.Add(next) log.Debug("skipping tx: too low price", "block price", b.Price, "tx price", price) break } nextLoad := next.LoadUnits(g) if units+nextLoad > g.MaxBlockSize { unusableTxs = append(unusableTxs, next) log.Debug("skipping tx: too large", "block size", units, "tx load", nextLoad) continue // could be txs that fit that are smaller } // Verify that changes pass tvdb := versiondb.New(vdb) if err := next.Execute(g, tvdb, b, context); err != nil { log.Debug("skipping tx: failed verification", "err", err) continue } if err := tvdb.Commit(); err != nil { return nil, err } b.Txs = append(b.Txs, next) units += nextLoad } ``` - Once transactions are added to the block, it will initialize the block to have the marshaled representation and block hash. It will also verify the block. ```go if err := b.init(); err != nil { return nil, err } _, _, err = b.verify() ``` Finally, it will return the block to the caller, which is the VM and hence the consensus engine. ### Storage [Storage](https://github.com/ava-labs/blobvm/blob/master/chain/storage.go) handles all the database operations like storing transactions, blocks, account balance, etc. Everything is stored as key-value pair for all types (block, transaction, balance, etc.) of data. We prefix different types of keys with a unique byte. For eg. the block identifier is prefixed with `0x0` and the transaction ID with `0x1`. Similarly, we have [prefix](https://github.com/ava-labs/blobvm/blob/master/chain/storage.go#L29) for other types as well. The prefix and original ID are separated by a `ByteDelimiter`. Prefixing is necessary for identifying the type of raw byte a particular key is pointing to. ```go const ( blockPrefix = 0x0 txPrefix = 0x1 txValuePrefix = 0x2 keyPrefix = 0x3 balancePrefix = 0x4 linkedTxLRUSize = 512 ByteDelimiter byte = '/' ) ``` We have 5 types of key-value pairs in total - - **Block** - For storing block data. - **Transaction** - For storing transaction ID. - **Transaction Value** - For storing transaction data i.e. the blob. - **Key** - For storing metadata of a blob like blob size, associated txID and timestamp. - **Balance** - For storing account balance. Prefixing each type of data is handled in separate functions. For eg., block prefixing is handled [here](https://github.com/ava-labs/blobvm/blob/master/chain/storage.go#L47). It takes the `blockID` and returns something like `0x0/<blockID>` as a byte array. ```go // [blockPrefix] + [delimiter] + [blockID] func PrefixBlockKey(blockID ids.ID) (k []byte) { k = make([]byte, 2+len(blockID)) k[0] = blockPrefix k[1] = ByteDelimiter copy(k[2:], blockID[:]) return k } ``` We have the following functions that will perform the write operation on the database. Every function is passed with a database instance where we want to store the data. - - [SetBalance()](https://github.com/ava-labs/blobvm/blob/master/chain/storage.go#L312) and [ModifyBalance()](https://github.com/ava-labs/blobvm/blob/master/chain/storage.go#L319) - Update account balance - [SetTransaction()](https://github.com/ava-labs/blobvm/blob/master/chain/storage.go#L268) - Set transaction ID - [PutKey()](https://github.com/ava-labs/blobvm/blob/master/chain/storage.go#L258) - Set blob metadata. This will be called when executing the `SetTx`. - [SetLastAccepted()](https://github.com/ava-labs/blobvm/blob/master/chain/storage.go#L193) - Add new block. This will be called when a block is verified. - [linkValues()](https://github.com/ava-labs/blobvm/blob/master/chain/storage.go#L142) - Put block's transaction values to database. This will be called when adding new block with `SetLastAccepted()` function call. Let's have a closer look at these functions: - **SetBalance** - This function will set the balance of an address that will be passed as an argument. This is called [internally](https://github.com/ava-labs/blobvm/blob/master/chain/storage.go#L336) by the `ModifyBalance()` function and while [allocating](https://github.com/ava-labs/blobvm/blob/master/chain/genesis.go#L115) the coins to airdrop addresses while creating the genesis block. ```go func SetBalance(db database.KeyValueWriter, address common.Address, bal uint64) error { k := PrefixBalanceKey(address) b := make([]byte, 8) binary.BigEndian.PutUint64(b, bal) return db.Put(k, b) } ``` - **ModifyBalance** - This function will modify the balance of an address depending upon the parameters `add` and `change`. It will simply perform the `SafeAdd` and `SafeSub` operation on the existing balance. `add` bool parameter indicates whether to add or subtract the `change` amount from the existing balance. Finally, it will call the `SetBalance` function with the updated amount. It is called while executing the transactions. For transfer transactions, it will [reduce](https://github.com/ava-labs/blobvm/blob/master/chain/transfer_tx.go#L39) the sender's balance and [increase](https://github.com/ava-labs/blobvm/blob/master/chain/transfer_tx.go#L42) the receiver's balance. And for every transaction, it will be called to [reduce](https://github.com/ava-labs/blobvm/blob/master/chain/tx.go#L104) the fee from the sender. ```go func ModifyBalance(db database.KeyValueReaderWriter, address common.Address, add bool, change uint64) (uint64, error) { b, err := GetBalance(db, address) if err != nil { return 0, err } var ( n uint64 xflow bool ) if add { n, xflow = smath.SafeAdd(b, change) } else { n, xflow = smath.SafeSub(b, change) } if xflow { return 0, fmt.Errorf("%w: bal=%d, addr=%v, add=%t, prev=%d, change=%d", ErrInvalidBalance, b, address, add, b, change) } return n, SetBalance(db, address, n) } ``` - **SetTransaction** - This function will simply put a key representing transaction ID to indicate the existence of a particular transaction. ```go func SetTransaction(db database.KeyValueWriter, tx *Transaction) error { k := PrefixTxKey(tx.ID()) return db.Put(k, nil) } ``` - **PutKey** - This function will store the metadata of a blob. Metadata includes blob size, transaction ID, and timestamp. It will be [called](https://github.com/ava-labs/blobvm/blob/master/chain/set_tx.go#L41) while executing the transaction (`SetTx`). ```go func PutKey(db database.KeyValueWriter, key common.Hash, vmeta *ValueMeta) error { // [keyPrefix] + [delimiter] + [key] k := ValueKey(key) rvmeta, err := Marshal(vmeta) if err != nil { return err } return db.Put(k, rvmeta) } ``` - **SetLastAccepted** - This function will set the last accepted block ID and also store the block (passed in the argument) in the database. It will call the `linkValues` function to store the block transactions on the database. It will be [called](https://github.com/ava-labs/blobvm/blob/master/chain/block.go#L236) at the end of the block verification process. ```go func SetLastAccepted(db database.KeyValueWriter, block *StatelessBlock) error { bid := block.ID() if err := db.Put(lastAccepted, bid[:]); err != nil { return err } ogTxs, err := linkValues(db, block) if err != nil { return err } sbytes, err := Marshal(block.StatefulBlock) if err != nil { return err } if err := db.Put(PrefixBlockKey(bid), sbytes); err != nil { return err } // Restore the original transactions in the block in case it is cached for // later use. block.Txs = ogTxs return nil } ``` - **linkValues** - This function is called to store the block transaction values that are associated with a blob (i.e. `SetTx`) on the database. It will loop over the transactions and will [store](https://github.com/ava-labs/blobvm/blob/master/chain/storage.go#L160) the blob value. Since the values are now stored separately, it will [replace](https://github.com/ava-labs/blobvm/blob/master/chain/storage.go#L163) the blob values in the block transactions with their transaction ID. The replaced value can again be restored by calling the [`restoreValues()`](https://github.com/ava-labs/blobvm/blob/master/chain/storage.go#L173) function with that block. ```go func linkValues(db database.KeyValueWriter, block *StatelessBlock) ([]*Transaction, error) { g := block.vm.Genesis() ogTxs := make([]*Transaction, len(block.Txs)) for i, tx := range block.Txs { switch t := tx.UnsignedTransaction.(type) { case *SetTx: if len(t.Value) == 0 { ogTxs[i] = tx continue } // Copy transaction for later cptx := tx.Copy() if err := cptx.Init(g); err != nil { return nil, err } ogTxs[i] = cptx if err := db.Put(PrefixTxValueKey(tx.ID()), t.Value); err != nil { return nil, err } t.Value = tx.id[:] // used to properly parse on restore default: ogTxs[i] = tx } } return ogTxs, nil } ``` The [other functions](https://github.com/ava-labs/blobvm/blob/master/chain/storage.go) are for reading the data that we have stored using the above functions. You can learn about these functions through the comments. ### Service [Service](https://github.com/ava-labs/blobvm/blob/master/vm/public_service.go) implements the API handlers to access the functions of the VM. VM has a method called [`CreateHandlers()`](https://github.com/ava-labs/blobvm/blob/master/vm/vm.go#L265) that will return the HTTP handler. ```go func (vm *VM) CreateHandlers() (map[string]*common.HTTPHandler, error) { apis := map[string]*common.HTTPHandler{} public, err := newHandler(Name, &PublicService{vm: vm}) if err != nil { return nil, err } apis[PublicEndpoint] = public return apis, nil } ``` In the above function, a new RPC server is created with the `PublicService`. It implements all the RPCs supported by the Virtual Machine. It has access to a VM instance in its structure, that is passed while creating it (see the above function). ```go type PublicService struct { vm *VM } ``` Let's have a look at the [`IssueRawTx()`](https://github.com/ava-labs/blobvm/blob/master/vm/public_service.go#L63) service method - ```go func (svc *PublicService) IssueRawTx(_ *http.Request, args *IssueRawTxArgs, reply *IssueRawTxReply) error { tx := new(chain.Transaction) if _, err := chain.Unmarshal(args.Tx, tx); err != nil { return err } // otherwise, unexported tx.id field is empty if err := tx.Init(svc.vm.genesis); err != nil { return err } reply.TxID = tx.ID() errs := svc.vm.Submit(tx) if len(errs) == 0 { return nil } if len(errs) == 1 { return errs[0] } return fmt.Errorf("%v", errs) } ``` It accepts `IssueRawTxArgs` that contain transaction bytes. It processes the request as following - Unmarshal transaction bytes into VM-defined transaction object - Initialize transaction object with txID, digest hash, etc. - Submit transaction object to VM. Similarly, all other services are implemented. ### Virtual Machine We have learned about all the components used in the BlobVM. Most of these components are referenced in the `vm.go` file, which acts as the entry point for the consensus engine as well as users interacting with the blockchain. For example, the engine calls `vm.BuildBlock()`, that in turn calls `chain.BuildBlock()`. Another example is when a user issues a raw transaction through service APIs, the `vm.Submit()` method is called. Let's look at some of the important methods of `vm.go` that must be implemented: #### [Initialize](https://github.com/ava-labs/blobvm/blob/master/vm/vm.go#L92) This method is called whenever we want to build a chain from the registered VM. This initialized the database manager, mempool, genesis block, caches, and finally start the block building notifier and transaction gossiper. Let's look at the parameters this method takes: - **ctx** - Metadata about the VM that includes information as mentioned [here](https://github.com/ava-labs/avalanchego/blob/master/snow/context.go#L37). - **dbManager** - The manager of the database this VM will persist data to. - **genesisBytes** - The byte-encoding of the genesis information of this VM. - **upgradeBytes** - To facilitate network upgrades - **configBytes** - VM specific [configurations](https://github.com/ava-labs/blobvm/blob/master/vm/config.go#L10) like BuildInterval, GossipInterval, etc. - **toEngine** - The channel used to send messages to the consensus engine. - **fxs** - Feature extensions that attach to this VM. - **appSender** - For sending data to `avalanchego` for things like gossiping. It performs the following task: - [Load configurations](https://github.com/ava-labs/blobvm/blob/master/vm/vm.go#L104) - If there are no config bytes, then it will simply load the [default configurations](https://github.com/ava-labs/blobvm/blob/master/vm/config.go#L19). But if the bytes are passed, then it will unmarshal it to `vm.config`. ```go vm.config.SetDefaults() if len(configBytes) > 0 { if err := ejson.Unmarshal(configBytes, &vm.config); err != nil { return fmt.Errorf("failed to unmarshal config %s: %w", string(configBytes), err) } } ``` - Setting up of context, channels, block builder, gossiper, caches, etc. as shown [below](https://github.com/ava-labs/blobvm/blob/master/vm/vm.go#L112). ```go vm.ctx = ctx vm.db = dbManager.Current().Database vm.activityCache = make([]*chain.Activity, vm.config.ActivityCacheSize) // Init channels before initializing other structs vm.stop = make(chan struct{}) vm.builderStop = make(chan struct{}) vm.doneBuild = make(chan struct{}) vm.doneGossip = make(chan struct{}) vm.appSender = appSender vm.network = vm.NewPushNetwork() vm.blocks = &cache.LRU{Size: blocksLRUSize} vm.verifiedBlocks = make(map[ids.ID]*chain.StatelessBlock) vm.toEngine = toEngine vm.builder = vm.NewTimeBuilder() ``` - [Unmarshal](https://github.com/ava-labs/blobvm/blob/master/vm/vm.go#L138) and [verify](https://github.com/ava-labs/blobvm/blob/master/chain/genesis.go#L86) genesis data. ```go vm.genesis = new(chain.Genesis) if err := ejson.Unmarshal(genesisBytes, vm.genesis); err != nil { log.Error("could not unmarshal genesis bytes") return err } if err := vm.genesis.Verify(); err != nil { log.Error("genesis is invalid") return err } ``` - Sets the [mempool](https://github.com/ava-labs/blobvm/blob/master/vm/vm.go#L151) ```go vm.mempool = mempool.New(vm.genesis, vm.config.MempoolSize) ``` - Checks the [existence](https://github.com/ava-labs/blobvm/blob/master/vm/vm.go#L131) of any last accepted block in the database ```go has, err := chain.HasLastAccepted(vm.db) if err != nil { log.Error("could not determine if have last accepted") return err } ``` - If the last accepted block is [found](https://github.com/ava-labs/blobvm/blob/master/vm/vm.go#L154), it will update the parameters like `vm.preferred` and `vm.lastAccepted` with the last accepted block and start bootstrapping from that block ```go blkID, err := chain.GetLastAccepted(vm.db) if err != nil { log.Error("could not get last accepted", "err", err) return err } blk, err := vm.GetStatelessBlock(blkID) if err != nil { log.Error("could not load last accepted", "err", err) return err } vm.preferred, vm.lastAccepted = blkID, blk log.Info("initialized blobvm from last accepted", "block", blkID) ``` - If there is no last accepted block in the database, it will [load](https://github.com/ava-labs/blobvm/blob/master/chain/genesis.go#L96) the genesis state and set genesis block as `vm.preferred` and `vm.lastAccepted`. ```go genesisBlk, err := chain.ParseStatefulBlock( vm.genesis.StatefulBlock(), nil, choices.Accepted, vm, ) if err != nil { log.Error("unable to init genesis block", "err", err) return err } // Set Balances if err := vm.genesis.Load(vm.db, vm.AirdropData); err != nil { log.Error("could not set genesis allocation", "err", err) return err } if err := chain.SetLastAccepted(vm.db, genesisBlk); err != nil { log.Error("could not set genesis as last accepted", "err", err) return err } gBlkID := genesisBlk.ID() vm.preferred, vm.lastAccepted = gBlkID, genesisBlk log.Info("initialized blobvm from genesis", "block", gBlkID) ``` - Finally, it will start the builder and gossiper as explained in the [block builder](#block-builder) section. ```go go vm.builder.Build() go vm.builder.Gossip() ``` #### [GetBlock](https://github.com/ava-labs/blobvm/blob/master/vm/vm.go#L318) It will return the stateless block corresponding to the ID parameter passed. ```go func (vm *VM) GetBlock(id ids.ID) (snowman.Block, error) { b, err := vm.GetStatelessBlock(id) if err != nil { log.Warn("failed to get block", "err", err) } return b, err } ``` The [`vm.GetStatelessBlock()`](https://github.com/ava-labs/blobvm/blob/master/vm/vm.go#L326) method will perform the following task: - Returns the stateless block, if present in the cache (only accepted blocks are there) - Returns the stateless block from the [verfied blocks](https://github.com/ava-labs/blobvm/blob/master/vm/vm.go#L65) mapping, if not present in the cache - Return the stateless block (parsed from the stateful block) from the database, if not found above ```go func (vm *VM) GetStatelessBlock(blkID ids.ID) (*chain.StatelessBlock, error) { // has the block been cached from previous "Accepted" call bi, exist := vm.blocks.Get(blkID) if exist { blk, ok := bi.(*chain.StatelessBlock) if !ok { return nil, fmt.Errorf("unexpected entry %T found in LRU cache, expected *chain.StatelessBlock", bi) } return blk, nil } // has the block been verified, not yet accepted if blk, exists := vm.verifiedBlocks[blkID]; exists { return blk, nil } // not found in memory, fetch from disk if accepted stBlk, err := chain.GetBlock(vm.db, blkID) if err != nil { return nil, err } // If block on disk, it must've been accepted return chain.ParseStatefulBlock(stBlk, nil, choices.Accepted, vm) } ``` The [`chain.ParseStatefulBlock()`](https://github.com/ava-labs/blobvm/blob/master/chain/block.go#L78) function will parse the stateful block into the stateless block and return it to the caller. #### [ParseBlock](https://github.com/ava-labs/blobvm/blob/master/vm/vm.go#L353) It will parse the bytes into the stateless block and return it to the caller. This basically calls `chain.ParseBlock()` for fetching stateless block. It also checks if it already has this block in the cache, verified blocks list, or in the database. If it is found there, it will return that stateless block. ```go func (vm *VM) ParseBlock(source []byte) (snowman.Block, error) { newBlk, err := chain.ParseBlock( source, choices.Processing, vm, ) if err != nil { log.Error("could not parse block", "err", err) return nil, err } log.Debug("parsed block", "id", newBlk.ID()) // If we have seen this block before, return it with the most // up-to-date info if oldBlk, err := vm.GetBlock(newBlk.ID()); err == nil { log.Debug("returning previously parsed block", "id", oldBlk.ID()) return oldBlk, nil } return newBlk, nil } ``` The [`chain.ParseBlock()`](https://github.com/ava-labs/blobvm/blob/master/chain/block.go#L66) function will unmarshal the byte representation of a block into a stateful block, and finally return the stateless block from it using `ParseStatfulBlock()`. ```go func ParseBlock( source []byte, status choices.Status, vm VM, ) (*StatelessBlock, error) { blk := new(StatefulBlock) if _, err := Unmarshal(source, blk); err != nil { return nil, err } return ParseStatefulBlock(blk, source, status, vm) } ``` #### [BuildBlock](https://github.com/ava-labs/blobvm/blob/master/vm/vm.go#L377) The consensus engine calls the `vm.BuildBlock()` method whenever it is the node's turn to propose a block (there must be a pending transaction in the mempool) and returns the stateless block. This makes use of the `chain.BuildBlock()` method to get the block. This is explained in the [Builder](#builder) section. This method will also handle the next block generation using VM's `HandlerGenerateBlock()` method, as explained in the [Block Builder](#block-builder) section. ```go func (vm *VM) BuildBlock() (snowman.Block, error) { log.Debug("BuildBlock triggered") blk, err := chain.BuildBlock(vm, vm.preferred) vm.builder.HandleGenerateBlock() if err != nil { log.Debug("BuildBlock failed", "error", err) return nil, err } sblk, ok := blk.(*chain.StatelessBlock) if !ok { return nil, fmt.Errorf("unexpected snowman.Block %T, expected *StatelessBlock", blk) } log.Debug("BuildBlock success", "blkID", blk.ID(), "txs", len(sblk.Txs)) return blk, nil } ``` #### [SetPreference](https://github.com/ava-labs/blobvm/blob/master/vm/vm.go#L437) This method sets the block ID preferred by this node. ```go func (vm *VM) SetPreference(id ids.ID) error { log.Debug("set preference", "id", id) vm.preferred = id return nil } ``` #### [LastAccepted](https://github.com/ava-labs/blobvm/blob/master/vm/vm.go#L445) This method returns the block ID last accepted by the node. ```go func (vm *VM) LastAccepted() (ids.ID, error) { return vm.lastAccepted.ID(), nil } ``` ## Conclusion This documentation covers the implementation and explanations of a Virtual Machine by taking reference from BlobVM. Different VMs can have different implementations depending upon their use case. A common thing among them could be the interface for a linear or DAG VM. You can learn about using BlobVM in more detail through the [README](https://github.com/ava-labs/blobvm/blob/master/README.md) provided in its repository.

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully