Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[x/programs] Add VM simulator #537

Merged
merged 33 commits into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
193ffbc
Add Simulator
hexfusion Oct 20, 2023
82e3f51
Add Simulator VM
hexfusion Oct 20, 2023
af36b43
Add support for runtime CallParam
hexfusion Oct 20, 2023
c8f7d36
Add documentation on custom runtime imports
hexfusion Oct 20, 2023
121b678
Make the counter program test more robust
hexfusion Oct 20, 2023
f739e42
React to change
hexfusion Oct 20, 2023
e2d0123
Ensure zap logger only prints to disk
hexfusion Oct 20, 2023
06f2a84
Add max_units to Step
hexfusion Oct 20, 2023
f094f5d
Cleanup
hexfusion Oct 20, 2023
54907a3
Add basic example
hexfusion Oct 20, 2023
aaed270
Fix passing Plan via stdin to simulator
hexfusion Oct 20, 2023
27c9e4e
Cleanup Rust SDK
hexfusion Oct 20, 2023
f508eed
Add README
hexfusion Oct 20, 2023
33cd060
Nit
hexfusion Oct 20, 2023
9ca237f
Fixup license file
hexfusion Oct 21, 2023
b9e89b6
Refactor Plan and improve error handling
hexfusion Oct 21, 2023
1d4a617
Remove description and name fields from types
hexfusion Oct 24, 2023
f4f083f
Add Rust simulator demo for token contract
hexfusion Oct 24, 2023
edb52ee
Plan cleanup and refactoring
hexfusion Oct 24, 2023
34c2c58
Fix lint errors
hexfusion Oct 24, 2023
9121567
Remove simulator YAML support from Rust SDK
hexfusion Oct 24, 2023
c17a8dc
Ensure Rust tests are run serial
hexfusion Oct 24, 2023
118bbac
Fix bug on unclean shutdown
hexfusion Oct 24, 2023
ab216b7
Bump wasmtime-go v14
hexfusion Oct 24, 2023
c92c896
Handle trap errors
hexfusion Oct 24, 2023
e9ba1b4
React to version bump
hexfusion Oct 24, 2023
61d422e
Ensure error coversion from bytes
hexfusion Oct 24, 2023
aa784dd
Improve error handling
hexfusion Oct 24, 2023
d051341
Rename TestingOnlyMode -> DebugMode
hexfusion Oct 24, 2023
b61c387
Bump stack for memory test
hexfusion Oct 24, 2023
7c8e843
Update tests to check against new Trap errs
hexfusion Oct 24, 2023
1455220
Improve test script
hexfusion Oct 24, 2023
875470d
Move simulator feature to dev dependency
hexfusion Oct 24, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ require (
github.com/NYTimes/gziphandler v1.1.1
github.com/ava-labs/avalanche-network-runner v1.7.2-0.20230825184751-fbe081616f02
github.com/ava-labs/avalanchego v1.10.12
github.com/bytecodealliance/wasmtime-go/v13 v13.0.0
github.com/bytecodealliance/wasmtime-go/v14 v14.0.0
github.com/cockroachdb/pebble v0.0.0-20230224221607-fccb83b60d5c
github.com/golang/mock v1.6.0
github.com/gorilla/mux v1.8.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
github.com/bytecodealliance/wasmtime-go/v13 v13.0.0 h1:o2PsUgSu6vMKr5S0+mz8EL3mZGQ0M8gHwS8/R0wY/wY=
github.com/bytecodealliance/wasmtime-go/v13 v13.0.0/go.mod h1:KmsZLdjjzNH/E5wbfoRehqP70tHzKlfNOi730VCAR4E=
github.com/bytecodealliance/wasmtime-go/v14 v14.0.0 h1:ur7S3P+PAeJmgllhSrKnGQOAmmtUbLQxb/nw2NZiaEM=
github.com/bytecodealliance/wasmtime-go/v14 v14.0.0/go.mod h1:tqOVEUjnXY6aGpSfM9qdVRR6G//Yc513fFYUdzZb/DY=
github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4=
github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
Expand Down
116 changes: 116 additions & 0 deletions x/programs/cmd/simulator/README.md
hexfusion marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Program VM simulator

## Introduction

The VM simulator provides a tool for testing and interacting with HyperSDK Wasm
`Programs`.

## Build

```sh
go build
```

## Getting Started

To try out out test token program its as easy as one command.

```sh
./simulator run ./cmd/testdata/basic_token.yaml
```
```json
{"id":0,"result":{"msg":"created key bob_key","timestamp":1697835142}}
{"id":1,"result":{"msg":"created key alice_key","timestamp":1697835142}}
{"id":2,"result":{"id":"2ut4fwdGE5FJG5w89CF3pVCjLrhiqCRZxB7ojtPnigh7QVU51i","timestamp":1697835142}}
{"id":3,"result":{"id":"5vQVVgXRygeYWcJ9bsRLLDs92wuS1B2qyndqEAmzXXf3ZQwAq","timestamp":1697835142}}
{"id":4,"result":{"id":"2pUQ85962MhjmQpzZsSGhFJCWLdeEWNU9W2ZZH4EnFYyAAF4Qr","timestamp":1697835142}}
{"id":5,"result":{"response":[10000],"timestamp":1697835142}}
```

## Testing a HyperSDK Programs

To test a HyperSDK Program you will need to create a `Plan` file which can be
either `JSON` and `YAML`. Look at the example `./cmd/testdata/basic_token.yaml`
for hints on usage.

### Deploy a HyperSDK Program

In this example we will create a new key `my_key` and deploy a new program

```yaml
# new_program.yaml
# example of creating a new key and deploying a new program using a `plan` file
steps:
- description: create my key
endpoint: key
method: create
params:
- name: key name
type: ed25519
value: my_key
- description: create my program
endpoint: execute # execute endpoint is for creating a transaction
method: program_create # program create is a method supported by the simulator
max_units: 100000
params:
- name: program_path
type: string
value: ./my_program.wasm
```

Next we will run the simulation

```sh
$./simulator run ./new_program.yaml
```
```json
{"id":0,"result":{"msg":"created key my_key","timestamp":1697835142}}
{"id":2,"result":{"id":"2ut4fwdGE5FJG5w89CF3pVCjLrhiqCRZxB7ojtPnigh7QVU51i","timestamp":1697835142}}
```

Congratulations you have just deployed your first HyperSDK program! Lets make
sure to keep track of the transaction ID
`2ut4fwdGE5FJG5w89CF3pVCjLrhiqCRZxB7ojtPnigh7QVU51i`.

### Interact with a HyperSDK Program

Now that the program is on chain lets interact with it.

```yaml
# play_program.yaml
name: play
description: Playing with new program
caller_key: my_key
steps:
- description: add two numbers
endpoint: readonly # readonly will return the result of the function
method: addition # name of the Wasm function to call
max_units: 10000
params:
- name: program_id
type: id
value: 2ut4fwdGE5FJG5w89CF3pVCjLrhiqCRZxB7ojtPnigh7QVU51i
- type: uint64
value: 100
- type: uint64
value: 100
require:
result:
operator: "=="
value: 200
```

### Interact with Rust!

The Rust SDK now allows for writing `Plans` in pure Rust.

## Deploy and Interact with HyperSDK Program

The above examples show how to deploy and interact with a `HyperSDK` program in
separate `Plan` files but we can also perform all of this in a single run. To reference a tx ID in you `Plan` just use the string `step_N` where `N` is the step number the tx was created


## Import Modules

Currently the simulator supports the `program` and `pstate` modules found in the
examples/imports directory.
242 changes: 242 additions & 0 deletions x/programs/cmd/simulator/cmd/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
// Copyright (C) 2023, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package cmd

import (
"encoding/json"
"fmt"
"strconv"

"gopkg.in/yaml.v2"
)

const (
ProgramCreate = "program_create"
hexfusion marked this conversation as resolved.
Show resolved Hide resolved
ProgramExecute = "execute"
)

type Plan struct {
// The key of the caller used in each step of the plan.
CallerKey string `yaml:"caller_key" json:"callerKey"`
// Steps to performed during simulation.
Steps []Step `json,yaml:"steps"`
}

type Step struct {
// The API endpoint to call. (required)
Endpoint Endpoint `json:"endpoint" yaml:"endpoint"`
// The method to call on the endpoint.
Method string `json:"method" yaml:"method"`
// The maximum number of units to consume for this step.
MaxUnits uint64 `json:"maxUnits" yaml:"max_units"`
// The parameters to pass to the method.
Params []Parameter `json:"params" yaml:"params"`
// Define required assertions against this step.
Require *Require `json:"require,omitempty" yaml:"require,omitempty"`
}

type Endpoint string

const (
/// Perform an operation against the key api.
EndpointKey Endpoint = "key"
/// Make a read-only call to a program function and return the result.
EndpointReadOnly Endpoint = "readonly"
/// Create a transaction on-chain from a possible state changing program
/// function call. A program's function can internally optionally call other
/// functions including program to program.
EndpointExecute Endpoint = "execute"
)

func newResponse(id int) *Response {
return &Response{
ID: id,
Result: &Result{},
}
}

type Response struct {
// The index of the step that generated this response.
ID int `json:"id" yaml:"id"`
// The result of the step.
Result *Result `json:"result,omitempty" yaml:"result,omitempty"`
// The error message if available.
Error string `json:"error,omitempty" yaml:"error,omitempty"`
}

func (r *Response) Print() error {
jsonBytes, err := json.Marshal(r)
if err != nil {
return fmt.Errorf("failed to marshal response: %s", err)
}

fmt.Println(string(jsonBytes))
return nil
}

func (r *Response) setError(err error) {
r.Error = err.Error()
}

func (r *Response) setTxID(id string) {
r.Result.ID = id
}

func (r *Response) getTxID() (string, bool) {
if r.Result.ID == "" {
return "", false
}
return r.Result.ID, true
}

func (r *Response) setBalance(balance uint64) {
r.Result.Balance = balance
}

func (r *Response) setResponse(response []uint64) {
r.Result.Response = response
}

func (r *Response) setMsg(msg string) {
r.Result.Msg = msg
}

func (r *Response) setTimestamp(timestamp int64) {
r.Result.Timestamp = uint64(timestamp)
}

type Result struct {
// The tx id of the transaction that was created.
ID string `json:"id,omitempty" yaml:"id,omitempty"`
// The balance after the step has completed.
Balance uint64 `json:"balance,omitempty" yaml:"balance,omitempty"`
// The response from the call.
Response []uint64 `json:"response,omitempty" yaml:"response,omitempty"`
// An optional message.
Msg string `json:"msg,omitempty" yaml:"msg,omitempty"`
// Timestamp of the response.
Timestamp uint64 `json:"timestamp,omitempty" yaml:"timestamp,omitempty"`
}

type Require struct {
// Assertions against the result of the step.
Result ResultAssertion `json,yaml:"result,omitempty"`
}

type ResultAssertion struct {
// The operator to use for the assertion.
Operator string `json,yaml:"operator"`
// The value to compare against.
Value string `json,yaml:"value"`
}

type Operator string

const (
NumericGt Operator = ">"
NumericLt Operator = "<"
NumericGe Operator = ">="
NumericLe Operator = "<="
NumericEq Operator = "=="
NumericNe Operator = "!="
// TODO: Add string operators?
)

type Parameter struct {
// The type of the parameter. (required)
Type Type `json,yaml:"type"`
// The value of the parameter. (required)
Value interface{} `json,yaml:"value"`
}

type Type string

const (
String Type = "string"
Bool Type = "bool"
ID Type = "id"
KeyEd25519 Type = "ed25519"
KeySecp256k1 Type = "secp256k1"
Uint64 Type = "u64"
)

// validateAssertion validates the assertion against the actual value.
func validateAssertion(actual uint64, require *Require) (bool, error) {
if require == nil {
return true, nil
}

assertion := require.Result
// convert the assertion value(string) to uint64
value, err := strconv.ParseUint(assertion.Value, 10, 64)
if err != nil {
return false, err
}

switch Operator(assertion.Operator) {
case NumericGt:
if actual > value {
return true, nil
}
case NumericLt:
if actual < value {
return true, nil
}
case NumericGe:
if actual >= value {
return true, nil
}
case NumericLe:
if actual <= value {
return true, nil
}
case NumericEq:
if actual == value {
return true, nil
}
case NumericNe:
if actual != value {
return true, nil
}
default:
return false, fmt.Errorf("invalid assertion operator: %s", assertion.Operator)
}

return false, nil
}

func unmarshalPlan(bytes []byte) (*Plan, error) {
var p Plan
switch {
case isJSON(string(bytes)):
if err := json.Unmarshal(bytes, &p); err != nil {
return nil, err
}
case isYAML(string(bytes)):
if err := yaml.Unmarshal(bytes, &p); err != nil {
return nil, err
}
default:
return nil, ErrInvalidConfigFormat
}

return &p, nil
}

func boolToUint64(b bool) uint64 {
if b {
return 1
}
return 0
}

func isJSON(s string) bool {
var js map[string]interface{}
return json.Unmarshal([]byte(s), &js) == nil
}

func isYAML(s string) bool {
var y map[string]interface{}
return yaml.Unmarshal([]byte(s), &y) == nil
}
Loading
Loading