vm

package
v1.0.3 Latest Latest
Warning

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

Go to latest
Published: Mar 6, 2024 License: Apache-2.0 Imports: 11 Imported by: 0

README

VM abstraction

Contents

  • General
  • The VM
    • Calling the VM
    • Result of running the VM
    • Structure of the VM
    • Deployment of the smart contract
    • VM type
    • Program binary and blob hash
  • Processor and the sandbox interface
    • Sandbox interface
    • SandboxView interface
  • Implementation of EVM. A Virtual Ethereum

General

By VM abstraction in ISC we understand a collection of abstract interfaces which makes the whole architecture of ISC and Wasp node agnostic about what exactly kind of deterministic computation machinery is used to run smart contract programs.

In ISC we distinguish two things:

  • The VM (Virtual Machine) itself
  • VM plugins, a pluggable part of VM

The VM itself is a deterministic executable, a "black box", which is used by the distributed part of the protocol, to calculate output (result) from inputs before coming to consensus among multiple VMs and finally submitting the result it to the ledger as the state update of the chain, the block.
Naturally, results of calculations, the output, is fully defined by inputs.

The VM contains multiple dynamically attached VM plugins or processors.
The VM invokes VM plugins to perform user-defined algorithms, the smart contracts.
The processor is attached to the VM through isc.VMProcessor interface.
Each VM type has own implementation of VMProcessor interface.
Usually, one processor represents one smart contract, however one processor can represent entirely new plugged-in VM, such as EVM.

For more details about implementation of VMProcessor interface see below.

In Wasp node, the VM-related code is mostly located in wasp/packages/vm directory.
The globally defined data types and definitions are located in wasp/packages/isc.

The VM

Calling the VM

The entry point to the VM in the Wasp codebase is the function MustRunVMTaskAsync function.
It is called each time the Wasp node needs to run computations.

The function MustRunVMTaskAsync start a parallel goroutine to run calculations.
Upon completion the VM notifies the calling code through the callback.
The MustRunVMTaskAsync takes as a parameter vm.VMTask:

type VMTask struct {
	Processors               *processors.Cache
	ChainInput               *ledgerstate.AliasOutput
	VirtualStateAccess       state.VirtualStateAccess
	SolidStateBaseline       coreutil.StateBaseline
	Requests                 []isc.Request
	ProcessedRequestsCount   uint16
	Timestamp                time.Time
	Entropy                  hashing.HashValue
	ValidatorFeeTarget       *isc.AgentID
	Log                      *logger.Logger
	OnFinish                 func(callResult dict.Dict, callError error, vmError error)
	ResultTransactionEssence *ledgerstate.TransactionEssence // if not nil it is a normal block
	RotationAddress          ledgerstate.Address             // if not nil, it is a rotation
	StartTime                time.Time
}```
At input, the most important parameters are:
Requests           []isc.Request
VirtualState       state.VirtualState 
* _VirtualState_ represents current state of the chain, a collection of key/value pairs.
It is _virtual_ in a sense that all updates to that state produced by smart contracts are accumulated in memory
and only are written (committed) into the database upon confirmation of the block

* _Requests_ represents a batch of requests. Each _request_ carries call parameters
as well as attached tokens (digital assets). Each request is processed by a smart contract it is targeting.
The requests update the _virtual state_ sequentially one by one, in each step producing a new virtual state.

### Result of running the VM
The result of running the task by the VM consists of:
* final state of the _VirtualState_
* _ResultBlock_, a sequence of mutations to the _Virtual state_.
* _ResultTransaction_, an _essence_ part (unsigned yet) of the _anchor transaction_ which will be sent to the
Tangle ledger for confirmation.

The _VirtualState_ at the output of the task is always equal to the _VirtualState_ at the output with applied all
mutations contained in the _block_.

The _VirtualState_ has _state hash_ (Merkle root or similar) which is deterministically calculated from the initial _VirtualState_ and the resulting _block_.

The hash of the resulting _VirtualState_ is contained in the _ResultTransaction_, therefore upon confirmation
of the transaction on the Tangle ledger the virtual state is immutably anchored.

### Structure of the VM

The _VM_ wraps many _processors_. The VM wrapper implements fee logic, call between processors, smart contract  
deployment and other generic logic. In general, one _processor_ represents one smart contract, the VM plugin.  
A _processor_ may implement any deterministic calculations as long as it conforms to the _VMProcessor_  
and other related interfaces.

![](VM.png)

 Significant part of the VM logic is implemented as _core smart contracts_. The core smart contracts also expose  
 core logic of each ISC chain to outside users: the core smart contracts can be called by requests just like any other  
 smart contract.

 The implementation of core smart contracts is hardcoded into the Wasp. Implementations of all core contract as well  
 as their unit tests can be found in [wasp/packages/vm/code](./core).

 Except core smart contracts, all other processors are plugged into the VM dynamically, hence _VM plugins_.

 All processors are alike: core contracts are attached to the VM just like any other VM plugin.

### Deployment of the smart contract
 The process of plugging a new smart contract (processor) into the VM is called _deployment_. The deployment  
 is handled by sending it the _deployContract_ request to the _root_ contract. As a result of the request,  
 a new smart contract (a _processor_, _VM plugin_) is deployed on the chain. The registry of deployed smart contracts  
 is maintained by the _root_ contract as a part of the chain's state.

 The _deployContract_ request takes two parameters :
* _VM type_ parameter defines interpreter of the smart contract binary code
* _blob hash_ parameter is a hash of the binary (a reference to it) which must be loaded into the interpreter to create a _processor_.
The binary, the blob, must be uploaded into the chain beforehand, usually with IPFS as a uploading/downloading service.

### VM type
All _VM types_ are statically predefined in the Wasp node. It means, to implement a new type of VM plugin, you will need
to modify the Wasp node by adding a new VM type. The _VM type_ is part of _VM abstraction_, so adding a new _VM type_ is transparent to the rest of the Wasp code.

A new _VM Type_ is introduced to the rest of the VM abstraction logic through the call to the function
 [`processors.RegisterVMType`](processors/factory.go#L20).

The call to `processors.RegisterVMType` takes name of the new _VM type_ and the constructor, a function which creates  
new `isc.Processor` object from the binary data of the program.

The following _VM types_ are pre-defined in the current release of the Wasp:
* `core` represents core contracts
* `native` represents example and other contracts (e.g. the `evm` contract) which conform to the native interface and are hardcoded before run
* `wasmtime` represents Wasmtime WebAssembly interpreter and native `Rust/Wasm` environment to create smart contracts.

To implement new types of interpreters, other languages or interpreters, a new _VM Type_
must be implemented into Wasp.

### Program binary and blob hash
To dynamically deploy a smart contract we need code of it in some binary format and dynamical linking of it  
to be able to call from VM. The very idea is to make the binary executable code of the smart contract immutable,  
which means it must be a part of the chain's state.

For example, `WebAssembly` (_wasm_) smart contracts produced by the `Rust/Wasm` environment provided together  
with the Wasp, are represented by `wasm` binaries. Other VM types may take different formats to represent its  
executable code.

To deploy a `wasmtime` smart contract on the chain, first we need to upload the corresponding `wasm` binary.  
All `wasm` binaries (as well as any other files of data) are kept in the registry handled by the `blob` core contract.  
To upload a `wasm` binary to the chain one must send a request to the `blob`. Each blob on the chain is referenced by  
its hash.

The smart contract deployment takes VM type and binary blob hash as parameters. It makes the smart contract  
deployment process completely transparent to the VM types and binary data formats of executables.

The only thing which is needed is to implement is the constructor function for the VM type and register  
it with `processors.RegisterVMType`. The rest is handled by the generic logic of the _VM_.

## Processor and the sandbox interface

In native and `wasmtime` implementations one _processor_ represents one smart contract. It gives full power
to the smart contracts on the ISC chain, such as manipulate native IOTA assets, call other smart contracts (processors)
on the same chain and send requests and assets to other ISC chains.

Each processor object implements two simple [interfaces](../isc/vmprocessor.go#L15): `isc.VMProcessor`:and `isc.VMProcessorEntryPoint`.

```go
type VMProcessor interface {
	GetEntryPoint(code Hname) (VMProcessorEntryPoint, bool)
	GetDefaultEntryPoint() VMProcessorEntryPoint 
	GetDescription() string
}

type VMProcessorEntryPoint interface {
	Call(ctx interface{}) (dict.Dict, error)
	IsView() bool
}

The smart contract is "plugged" into the VM with this interface.

Entry points

A processor (smart contract) is a collection of callable entry points.

Each entry point is identified in the processor with its hname (hashed name), a 4 byte value, normally first 4 bytes of blake2b hash of the smart contract function's name or signature. See isc.Hname.

Function GetEntryPoint returns entry point object with the existence flag.

GetDefaultEntryPoint must always return default entry point. It will be called each time when entry point with given hname is not found.

VMProcessorEntryPoint interface allows to Call() the entry point and passes it a context handler.
The call returns a dictionary of resulting values, a collection of key/value pairs and, optionally, error code.

There are two types of entry points: full entry points and view entry points.

  • full entry point only accepts context handlers of isc.Sandbox interface type. This type of context provides full access to the state of the smart contract so that the smart contract could modify it.

  • view entry point only accepts context handler of isc.SandboxView interface type. It provides limited read-only access to the state.

The type of entry point is recognized by IsView() function. Using Call() with the wrong context type will result panic in the VM.

The VM provides implementation of isc.Sandbox and isc.SandboxView interfaces. It limits access to the smart contract's state partition and its on-chain account of tokens.

Each new VM type has to provide its own VMProcessor and VMProcessorEntryPoint implementations.

Sandbox interface

The isc.Sandbox interface implements a number of functions which can be used by the processor's implementation
(a smart contract). Here are some of them:

  • Params() returns a dictionary (key/value pairs) of the call parameters
  • State() returns access to the VirtualState in the context of the call: a collection of key/value pairs.

All key/value pair storages, like virtual state and call parameters are defined in the kv package.
It defines different kinds of key/value pair collections and different encoding/decoding options for the binary data.

  • Balances() returns a collection of color/value pairs: balances of colored tokens which are in the control of the current smart contract
  • IncomingTransfer() represent colored balances which are coming with this call. Those tokens are already part of Balances()
  • Call() invokes entry point of another smart contract (processor) on the same chain. Note that the call is agnostic about type of VM of the called entry point.
  • Caller() is a secure identification of the calling entity: an address or another smart contract
  • Send() allows sending requests and funds to another chains, smart contracts or ordinary wallets.
  • Utils() implements a number of utility function which may be called by a smart contract on the VM host. For example hashing function or other deterministic state-less cryptographic functions which normally are faster to run on the host than on an interpreter.
SandboxView interface

The view entry points are called from outside, for example by a web server to query state of specific
smart contracts. By intention those entry points cannot modify the state of the chain.

The SandboxView interface must be passed as a parameter to the view entry points.
The SandboxView implements limited access to the state, fo example it doesn't have a concept of
IncomingTransfer or possibility of Send() tokens.
The State() interface provides read-only access to the VirtualState.

The logic of the VM ensures that full entry points can call all other entry points, while view entry points can only call
other view entry points.

Implementation of EVM on a Virtual Ethereum

The IOTA Foundation is contemplating a plan to ensure binary compatibility with EVM/Solidity ecosystem.
The goal is to be able to run EVM smart contracts on ISC chains. The EVM should be implemented in the framework of
the ISC VM Abstraction.
The EVM would be implemented as a processor and it will be able to access key/value store of the state through State()
interface of the Sandbox(). It essentially means the whole EVM chain would be implemented as a state of one ISC smart contract.
This way EVM would run in an isolated environment and Solidity code won't be able to access and manipulate native IOTA assets, hence Virtual Ethereum. To open EVM to access all spectrum of ISC functions would be the next step.

The Virtual Ethereum project is in the phase of definition therefore it is open for all kind of suggestions to architectural design with the final goal in mind: to be able to run native EVM code (binary compatibility) as
a VM on the ISC chain.

The external interfaces of Virtual Ethereum would be wrapped into the native transactions and calls of IOTA and ISC.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrOverflow                             = coreerrors.Register("overflow").Create()
	ErrNotEnoughBaseTokensBalance           = coreerrors.Register("not enough base tokens balance").Create()
	ErrNotEnoughNativeAssetBalance          = coreerrors.Register("not enough native tokens balance").Create()
	ErrNotEnoughFundsForAllowance           = coreerrors.Register("not enough funds for allowance").Create()
	ErrCreateFoundryMaxSupplyMustBePositive = coreerrors.Register("max supply must be positive").Create()
	ErrCreateFoundryMaxSupplyTooBig         = coreerrors.Register("max supply is too big").Create()
	ErrFoundryDoesNotExist                  = coreerrors.Register("foundry does not exist").Create()
	ErrCantModifySupplyOfTheToken           = coreerrors.Register("supply of the token is not controlled by the chain").Create()
	ErrNativeTokenSupplyOutOffBounds        = coreerrors.Register("token supply is out of bounds").Create()
	ErrFatalTxBuilderNotBalanced            = coreerrors.Register("fatal: tx builder is not balanced").Create()
	ErrInconsistentL2LedgerWithL1TxBuilder  = coreerrors.Register("fatal: L2 ledger is not consistent with the L1 tx builder").Create()
	ErrCantDestroyFoundryBeingCreated       = coreerrors.Register("can't destroy foundry which is being created").Create()

	ErrContractNotFound          = coreerrors.Register("contract with hname %08x not found")
	ErrTargetEntryPointNotFound  = coreerrors.Register("entry point not found").Create()
	ErrEntryPointCantBeAView     = coreerrors.Register("'init' entry point can't be a view").Create()
	ErrRepeatingInitCall         = coreerrors.Register("repeating init call").Create()
	ErrTooManyEvents             = coreerrors.Register("too many events issued for contract").Create()
	ErrPrivilegedCallFailed      = coreerrors.Register("privileged call failed").Create()
	ErrExceededPostedOutputLimit = coreerrors.Register("exceeded maximum number of %d posted outputs in one request").Create(42)
	ErrGasBudgetExceeded         = coreerrors.Register("gas budget exceeded").Create()
	ErrSenderUnknown             = coreerrors.Register("sender unknown").Create()
	ErrNotEnoughTokensLeftForGas = coreerrors.Register("not enough funds left to pay for gas").Create()
	ErrUnauthorized              = coreerrors.Register("unauthorized access").Create()
	ErrIllegalCall               = coreerrors.Register("illegal call - entrypoint cannot be called from contracts")
	ErrSendMultipleNFTs          = coreerrors.Register("cannot send more than 1 NFT").Create()
	ErrEVMExecutionReverted      = coreerrors.Register("execution reverted: %s") // hex-encoded revert data
)

Functions

This section is empty.

Types

type RequestResult added in v0.3.0

type RequestResult struct {
	// Request is the corresponding request in the task
	Request isc.Request
	// Return is the return value of the call
	Return dict.Dict
	// Receipt is the receipt produced after executing the request
	Receipt *blocklog.RequestReceipt
}

type VMTask

type VMTask struct {
	Processors         *processors.Cache
	AnchorOutput       *iotago.AliasOutput
	AnchorOutputID     iotago.OutputID
	Store              state.Store
	Requests           []isc.Request
	TimeAssumption     time.Time
	Entropy            hashing.HashValue
	ValidatorFeeTarget isc.AgentID
	// If EstimateGasMode is enabled, signature and nonce checks will be skipped
	EstimateGasMode bool
	// If EVMTracer is set, all requests will be executed normally up until the EVM
	// tx with the given index, which will then be executed with the given tracer.
	EVMTracer            *isc.EVMTracer
	EnableGasBurnLogging bool // for testing and Solo only

	Migrations *migrations.MigrationScheme // for testing and Solo only

	Log *logger.Logger
}

VMTask is task context (for batch of requests). It is used to pass parameters and take results It is assumed that all requests/inputs are unlock-able by aliasAddress of provided AnchorOutput at timestamp = Timestamp + len(Requests) nanoseconds

func (*VMTask) FinalStateTimestamp added in v1.0.3

func (task *VMTask) FinalStateTimestamp() time.Time

func (*VMTask) WillProduceBlock added in v1.0.3

func (task *VMTask) WillProduceBlock() bool

type VMTaskResult added in v1.0.3

type VMTaskResult struct {
	Task *VMTask

	// StateDraft is the uncommitted state resulting from the execution of the requests
	StateDraft state.StateDraft
	// RotationAddress is the next address after a rotation, or nil if there is no rotation
	RotationAddress iotago.Address
	// TransactionEssence is the transaction essence for the next block,
	// or nil if the task does not produce a normal block
	TransactionEssence *iotago.TransactionEssence
	// InputsCommitment is the inputs commitment necessary to sign the ResultTransactionEssence
	InputsCommitment []byte
	// RequestResults contains one result for each non-skipped request
	RequestResults []*RequestResult
}

Directories

Path Synopsis
core
blocklog
in the blocklog core contract the VM keeps indices of blocks and requests in an optimized way for fast checking and timestamp access.
in the blocklog core contract the VM keeps indices of blocks and requests in an optimized way for fast checking and timestamp access.
evm
Package evm contains the declaration of the evm core contract's interface.
Package evm contains the declaration of the evm core contract's interface.
evm/emulator
Package emulator contains the implementation of the EVMEmulator and subcomponents.
Package emulator contains the implementation of the EVMEmulator and subcomponents.
evm/evmimpl
Package evmimpl contains the implementation of the `evm` core contract.
Package evmimpl contains the implementation of the `evm` core contract.
evm/evmnames
package evmnames provides the names of EVM core contract functions and fields.
package evmnames provides the names of EVM core contract functions and fields.
evm/evmtest
Package evmtest contains solo tests for the evm core contract.
Package evmtest contains solo tests for the evm core contract.
governance
in the blocklog core contract the VM keeps indices of blocks and requests in an optimized way for fast checking and timestamp access.
in the blocklog core contract the VM keeps indices of blocks and requests in an optimized way for fast checking and timestamp access.
governance/governanceimpl
This file provides implementation for the governance SC, the ChainNode management functions.
This file provides implementation for the governance SC, the ChainNode management functions.
testcore/sbtests/sbtestsc
smart contract for testing
smart contract for testing
Package vmtxbuilder implements AnchorTransactionBuilder, a transaction builder used by the VM to construct anchor transaction.
Package vmtxbuilder implements AnchorTransactionBuilder, a transaction builder used by the VM to construct anchor transaction.

Jump to

Keyboard shortcuts

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