The Golang client for SVM. Its primary goal is supplying an ergonomic API for go-spacemesh
git clone https://github.com/spacemeshos/go-svm
cd go-svm
make
make test # Optional. Rerun if you want to test any changes to `go-svm`.
make installEach binary (over-the-wire) transaction will always have two parts:
Envelope- A Transaction agnostic content.Message- Transaction-specific content.
Each Message will be preceded by the Envelope - together; both make a complete binary Transaction.
In addition to the data sent over the wire, there will be implicit fields inferred from it.
One such example is the Transaction Id. That field is part of the Context described later.
The computation of the TransactionId will be done externally to go-svm(i.e., go-spacemesh).
There are in total three types of transactions under SVM:
Deploy- For deploying Templates (seeDeploying a Templatelater).Spawn- For spawning new accounts out of existing Templates (seeSpawning an Accountlater).Call- For calling an existing account (seeCalling an Accountlater).
The Envelope contains pieces of data that are part of any transaction.
When the Full-Node (e.g., go-spacemesh) receives a binary transaction from the network, it needs to decode the Envelope part into a Golang struct.
The other part of the Transaction, a.ka the Message, should be kept as []byte
It's the job of SVM to decode the transaction Message. More information about each Message type appears later on this document.
An Envelope will contain the following fields:
Type- The transaction type (Deploy / Spawn / Call)Principal- TheAddressof theAccountpaying for theGas.Amount- For funding thetargetaccount (relevant only forSpawn/Calltransactions).TxNonce- The Transaction'snonce.GasLimit- Maximum units of Gas to be paid.GasFee- Fee per Unit of Gas.
And this is the corresponding Golang struct:
type Envelope struct {
Type TxType // Alias for `uint8`
Principal Address // Alias for `[20]byte`
Amount Amount // Alias for `uint64`
TxNonce TxNonce // A struct holding a pair of `uint64` (Golang has no `uint128` primitive)
GasLimit Gas // Alias for `uint64`
GasFee GasFee // Alias for `int`
}To create a new Envelope, use this helper function:
func NewEnvelope(principal Address, amount Amount, txNonce TxNonce, gasLimit Gas, gasFee GasFee) *EnvelopeA Message is essentially a blob of bytes. Each go-svm API expecting a Message will ask for it in its binary form (i.e. [] byte).
It's the job of SVM itself to decode a binary Message and figure out what's inside.
There are in total three types of transactions under SVM - each with its corresponding Message:
Deploy Message- TheMessageof aDeploytransaction.Spawn Message- TheMessageof aSpawntransaction.Call Message- TheMessageof aCalltransaction.
Each Transaction is detailed later in this document.
In addition to the Transaction, there is the Execution Context (or simply Context).
The Context structure will contain additional data (alongside the Transaction) to be used by SVM when executing a transaction.
It will contain data relevant to the currently executing Context within the Full-Node (i.e., go-spacemesh) and properties computed from the Transaction itself.
(such as the Transaction Id). It's the role of the Full-Node (e.g., go-spacemesh) to create a Context instance and pass it forward to go-svm.
Here is the current declaration of a Context
type Context struct {
Layer Layer // The `Layer` (alias for `uint64`) we're about to execute the Transaction in.
TxId TxId // The computed `Transaction Id` out of the `Transaction` data (`TxId` is an alias for `[32]byte`).
}For Creating a new Context, use this helper:
func NewContext(layer Layer, txId TxId) *ContextA Deploy Message will be generated using the Template Toolchain
By saying a Template Toolchain, we mean the process of:
- Compiling the Template code and emitting binary Wasm and Metadata files.
Currently, the only way to generate such Wasm is by writing Rust code using the
[SVM SDK](https://github.com/spacemeshos/svm/tree/master/crates/sdk)crate.
Here is a link for such an example Template: (execute the build.sh for compiling into Wasm)
https://github.com/spacemeshos/svm/tree/master/crates/runtime/tests/wasm/calldata
- Utilizing the
SVM CLIfor crafting a binaryDeploytransaction.
Here is CLI usage for the generation of a binary Spawn message:
svm-cli craft-deploy --smwasm Template.wasm --meta Template-meta.json --output template.svmIn the future, there might be other alternatives to achieve the above.
If Spacemesh has its Smart-Contracts programming language in the future, it'll make sense to let that language compiler take care of everything.
In such a case, the output will be a Deploy Message. From here, filling in the missing parts (Envelope and signing the Transaction) should be the same solution used today for the SVM SDK and SVM CLI.
Each Spawn Message contains the following fields:
template(Template Address) - TheTemplatewe'll spawn an account of.name(String) - The name of theAccount(optional).ctor(String) - The constructor identifier to execute.calldata(Blob) - The input for the constructor to run.
Generating a binary Spawn Message can be achieved in two ways using the SVM CLI and SVM Codec
Here is an example for a Spawn JSON
{
"version": 0,
"template": "b5eba98957e6a93173ffb50207cceeedfddb1a72",
"name": "My Account",
"ctor_name": "initialize",
"calldata": {
"abi": ["address", "bool"],
"data": ["8f20ed1a0e342c2a75b1b3f8014545dd3d886078", true]
}
}In order to turn it into a binary Spawn Message using the CLI execute:
svm-cli tx --tx-type=spawn --input=tx.json --output=tx.binUsing the CLI is very useful for tests inputs generation.
The SVM project ships with an artifact called svm_codec.wasm. That Wasm package could be used for encoding a transaction Message.
There has been implemented an npm package for interfacing against that Wasm package.
svm-codec-npm:
https://github.com/spacemeshos/svm-codec-npm
This npm package will be consumed by smapp or the Process Explorer.
Similarly, new clients could be added in the future (for example, a Golang client to be used by smrepl)
Each Call Message contains the following fields:
target(Account Address) - TheAddressof theAccountwhich we're calling.function(String) - The function's name to execute.verifydata(Blob) - The input for thesvm_verifyfunction.calldata(Blob) - The input for the function to run.
In a very similar manner to the Spawn - we can generate a binary Call Message given a JSON.
And the same information about the svm_codec.wasm applies here as well.
Here is an example for a Call Message given as a JSON:
{
"version": 0,
"target": "066818abe361dd44f425da19e17c45babc40e232",
"func_name": "store_addr",
"verifydata": {
"abi": [],
"data": []
},
"calldata": {
"abi": ["address"],
"data": ["102030405060708090102030405060708090AABB"]
}
}To turn it into a binary Call Message using the CLI execute:
svm-cli tx --tx-type=call --input=tx.json --output=tx.binInit is the entry point for interacting with SVM in any way. It runs internal
initialization logic; it is fully thread-safe and idempotent.
func Init() (*API, error)Creates a new SVM Runtime. You can think of it as opening a connection to SVM. Please make sure to call Init (see above) first.
Here is the `Create Runtime` API:
func (*API) NewRuntime() (*Runtime, error)When the usage of a Runtime is over, we need to release its resources. You can think of it as closing a connection.
And here is the `Destroy Runtime` API:
func (rt *Runtime) Destroy()Performs the verify stage as dictated by the Account Unification design.
Since the verify flow involves the running Wasm function as done when running a Call transaction, the output will also be of type CallReceipt.
This is the relevant API to be used:
func (rt *Runtime) Verify(env *Envelope, msg []byte, ctx *Context) (*CallReceipt, error)Signaling SVM that we are about to start playing a list of transactions under the input layer Layer.
The value of the Layer is expected to equal the last known committed/rewinded Layer plus one.
Any other layer given as input will result in an error.
For starting a new `Layer`, use the following:
func (rt *Runtime) Open(layer Layer) errorCommits SVM dirty changes. It also signals the termination of the current Layer.
In other words, after finishing executing the layer transactions, we should call a Commit.
The matching API:
func (rt *Runtime) Commit() (Layer, State, error)If the Commit went out fine, it would return a tuple consisting of:
- The
Layerwe have just committed. - The newly computed
Global State Root Hash - Setting
nilunder theerror
If the Commit errored, then the output will be:
- The
Layerwe have just tried to commit (but have failed) nilunder theStateposition.- The
errorthat occurred.
Rewinds SVM Global State to the given Layer. This capability is necessary for self-healing.
Here is the API for rewind:
func (rt *Runtime) Rewind(layer Layer) (State, error)If the rewind succeeds, it returns the Global-State Root Hash at that given point. (the error returned will be assigned with nil)
Otherwise, a nil will be placed under the State position, and the 2nd tuple element will contain the error that occurred.
Given an Account Address - retrieves its most basic information encapsulated within an Account struct.
Here is the API to be used for retrieving an account:
func (rt *Runtime) GetAccount(addr Address) (Account, error)And this is the definition of an Account at go-svm:
type Account struct {
Addr Address // The `Address` of the account
Balance Amount // The account's balance (`Amount` is an alias for `uint64`)
Counter TxNonce // The account's counter. It's a struct holding a pair of `uint64` (Golang has no `uint128` primitive)
}Increases an account's balance. The motivation for that API was supporting Rewards
The API for increasing an account's balance:
func (rt *Runtime) IncreaseBalance(addr Address, amount Amount)TODO: What should be the behavior of go-svm when there is no account with the given Address?
Deploying a Template exposes two dedicated APIs: ValidateDeploy and Deploy.
Syntactically validates the Deploy Message given in a binary form and returns whether it's valid or not.
The API for validation:
func (rt *Runtime) ValidateDeploy(msg []byte) (bool, error)Performs the actual deployment of a Template and returns a DeployReceipt.
The Deploy API:
func (rt *Runtime) Deploy(env *Envelope, msg []byte, ctx *Context) (*DeployReceipt, error)That is the DeployReceipt definition:
type DeployReceipt struct {
Success bool // Whether the Transaction succeeded or not
Error *RuntimeError // Returns `nil` when `Success` is true and otherwise the runtime error that occurred
TemplateAddr TemplateAddr // The `Template Address` for the newly deployed template
GasUsed Gas // The amount of `Gas` used during the transaction execution (in units of Gas)
Logs []Log // Logs created as part of transaction execution
}Performs the spawning of a new Account out of the existing Template.
Similarly to Deploy - spawning a new Account exposes two dedicated APIs: ValidateSpawn and Spawn.
Syntactically validates the Spawn Message given in a binary form and returns whether it's valid or not.
The validation API:
func (rt *Runtime) ValidateSpawn(msg []byte) (bool, error)Performs the spawning of a new Account out of the existing Template and returns a SpawnReceipt.
The Spawn API:
func (rt *Runtime) Spawn(env *Envelope, msg []byte, ctx *Context) (*SpawnReceipt, error)Here is the SpawnReceipt definition:
type SpawnReceipt struct {
Success bool // Whether the Transaction succeeded or not
Error *RuntimeError // Returns `nil` when `Success` is true and otherwise the runtime error that occurred
AccountAddr Address // The `Address` for the newly spawned Account
InitState State // The newly computed `Global-State Root Hash` after spawning the Account []byte // The data returned by the constructor running during spawning the account
GasUsed Gas // The amount of `Gas` used during the transaction execution (in units of Gas)
Logs []Log // Logs created as part of transaction execution
TouchedAccounts []Address // A list of `Account Addresses` engaged in any at least a single coins-transfer during transaction execution
}Syntactically validates the Call Message given in a binary form and returns whether it's valid or not.
The validation API:
func (rt *Runtime) ValidateCall(msg []byte) (bool, error)Performs the actual calling an Account and returns a CallReceipt.
The Call API:
func (rt *Runtime) Call(env *Envelope, msg []byte, ctx *Context) (*CallReceipt, error)Here is the CallReceipt definition:
type CallReceipt struct {
Success bool // Whether the Transaction succeeded or not
Error *RuntimeError // Returns `nil` when `Success` is true and otherwise the runtime error that occurred
NewState State // The newly computed `Global-State Root Hash` after calling the Account []byte // The data returned by calling the account
GasUsed Gas // The amount of `Gas` used during the transaction execution (in units of Gas)
Logs []Log // Logs created as part of transaction execution
TouchedAccounts []Address // A list of `Account Addresses` engaged in any at least a single coins-transfer during transaction execution
}Returns the number of living SVM Runtimes
SVM was designed to execute transactions sequentially. It means that the number of existing Runtime instances should not exceed one.
There are functions of SVM that could have been called in parallel (for example, validation) - it's not recommended at this stage to take extra caution and not do that.
This helper function is intended to be used for testing purposes. However, it could be used for telemetry/tracing/debugging as well.
The API:
func (*API) RuntimesCount() intReturns the number of living Receipts returned by SVM
It's the job of the go-svm internals to release binary Receipts returned by SVM
If there're no bugs, the reported living Receipt count should be zero after each transaction execution. The helper should be applied for testing purposes. However, the production code can log (with a fatal severity level) if this number somehow stops being zero.
The API:
func (*API) ReceiptsCount() intReturns the number of internal errors returned by SVM.
First, it's important to stress what we mean by saying an Error.
When a transaction has failed due to panic or running out-of-gas - SVM needs to return a valid Receipt setting Success to false
An error should be returned in the case that SVM itself panicked - this is undefined behavior.
We, of course, hope never to reach such a point since an internal error might occur only on one Operating-System. However, this will break the consensus.
If we, unfortunately, did hit an internal error, we need to make sure the error data returned by SVM will be freed.
This helper function should be used for testing. It's up to the go-svm client to decide what to do in case an internal error is being returned.
One way is to crash to process. Another alternative is to convert that error to Golang Receipt Struct and hope for the best. Turning the internal error to Receipt can ease debugging since
the Process Explorer will display that Receipt as well.