From 71f7d4b6e65b99a470b422daef38248363b2a889 Mon Sep 17 00:00:00 2001 From: Rodrigo Villar Date: Thu, 31 Oct 2024 15:37:26 -0400 Subject: [PATCH 01/15] start cli tutorial --- .../{morpheusvm.md => 1_morpheusvm.md} | 0 .../morpheusvm/{options.md => 2_options.md} | 0 .../morpheusvm/{testing.md => 3_testing.md} | 0 docs/tutorials/morpheusvm/4_cli.md | 198 ++++++++++++++++++ 4 files changed, 198 insertions(+) rename docs/tutorials/morpheusvm/{morpheusvm.md => 1_morpheusvm.md} (100%) rename docs/tutorials/morpheusvm/{options.md => 2_options.md} (100%) rename docs/tutorials/morpheusvm/{testing.md => 3_testing.md} (100%) create mode 100644 docs/tutorials/morpheusvm/4_cli.md diff --git a/docs/tutorials/morpheusvm/morpheusvm.md b/docs/tutorials/morpheusvm/1_morpheusvm.md similarity index 100% rename from docs/tutorials/morpheusvm/morpheusvm.md rename to docs/tutorials/morpheusvm/1_morpheusvm.md diff --git a/docs/tutorials/morpheusvm/options.md b/docs/tutorials/morpheusvm/2_options.md similarity index 100% rename from docs/tutorials/morpheusvm/options.md rename to docs/tutorials/morpheusvm/2_options.md diff --git a/docs/tutorials/morpheusvm/testing.md b/docs/tutorials/morpheusvm/3_testing.md similarity index 100% rename from docs/tutorials/morpheusvm/testing.md rename to docs/tutorials/morpheusvm/3_testing.md diff --git a/docs/tutorials/morpheusvm/4_cli.md b/docs/tutorials/morpheusvm/4_cli.md new file mode 100644 index 0000000000..1dc530a646 --- /dev/null +++ b/docs/tutorials/morpheusvm/4_cli.md @@ -0,0 +1,198 @@ +# CLI + +In the previous sections, we have gone over how to implement MorpheusVM from +scratch and how we can test our implementation. Now that we're confident that we +have a correct version of MorpheusVM, we can now utilize the HyperSDK-CLI tool +to actually interact with our VM! + +In this tutorial, we'll go over the following: + +- Installing the CLI locally +- Setting up the CLI +- Interacting with MorpheusVM via the CLI + +## CLI Installation + +To start, we'll want to compile the HyperSDK-CLI. If you're in +`examples/tutorial`, you can run the following: + +```bash +go build -o ./hypersdk-cli ../../cmd/hypersdk-cli/ +``` + +To confirm that your build of the HyperSDK-CLI was successful, run the following +command: + +```bash +./hypersdk-cli +``` + +You should see the following: + +```bash +A CLI application for performing read and write actions on HyperSDK-based chains. + +Usage: + hypersdk-cli [command] + +Available Commands: + actions Print the list of actions available in the ABI + address Print current key address + balance Get the balance of an address + completion Generate the autocompletion script for the specified shell + endpoint Manage endpoint + help Help about any command + key Manage keys + ping Ping the endpoint + read Read data from the chain + tx Execute a transaction on the chain + +Flags: + --endpoint string Override the default endpoint + -h, --help help for hypersdk-cli + --key string Private ED25519 key as hex string + -o, --output string Output format (text or json) (default "text") + +Use "hypersdk-cli [command] --help" for more information about a command. +``` + +With the HyperSDK-CLI built, we can now move on to setting it up. + +## CLI Setup + +In this section, we'll want to both start our network along with passing in the +necessary information that the HyperSDK-CLI needs to interact with our network. +To start, run the following command from `./examples/tutorial`: + +```bash +./scripts/run.sh +``` + +If all goes well, you will eventually see the following message: + +```bash +Ran 1 of 8 Specs in 19.085 seconds +SUCCESS! -- 1 Passed | 0 Failed | 0 Pending | 7 Skipped +PASS +``` + +This means that our network is now running in the background. Focusing now on +the HyperSDK-CLI, we now want to store the private key of our (test!) account +and the RPC endpoint. We can do this by executing the following commands: + +```bash +./hypersdk-cli endpoint set --endpoint=http://localhost:9650/ext/bc/morpheusvm/ +./hypersdk-cli key set --key=0x323b1d8f4eed5f0da9da93071b034f2dce9d2d22692c172f3cb252a64ddfafd01b057de320297c29ad0c1f589ea216869cf1938d88c9fbd70d6748323dbf2fa7 +``` + +Your command line should look like the following: + +```bash +AVL-0W5L7Y:tutorial rodrigo.villar$ ./hypersdk-cli endpoint set --endpoint=http://localhost:9650/ext/bc/morpheusvm/ +Endpoint set to: http://localhost:9650/ext/bc/morpheusvm/ +AVL-0W5L7Y:tutorial rodrigo.villar$ ./hypersdk-cli key set --key=0x323b1d8f4eed5f0da9da93071b034f2dce9d2d22692c172f3cb252a64ddfafd01b057de320297c29ad0c1f589ea216869cf1938d88c9fbd70d6748323dbf2fa7 +✅ Key added successfully! +Address: 0x00c4cb545f748a28770042f893784ce85b107389004d6a0e0d6d7518eeae1292d9 +``` + +With this, we're now ready to interact with our implementation of MorpheusVM! + +## Interacting with MorpheusVM + +As a sanity test, let's first check that we can interact with our running VM - +to do this, run the following command: + +```bash +./hypersdk-cli ping +``` + +If successful, you should see the following: + +```bash +✅ Ping succeeded +``` + +Next, let's see the current balance of our account. We'll run the following +command: + +```bash +./hypersdk-cli balance +``` + +This should give us the following result: + +```bash +✅ Balance: 10000000000000 +``` + +Since the account we are using is specified as a prefunded account in the +genesis of our VM (via `DefaultGenesis`), our account balance is as expected. +Having read into the state of our VM, let's now try writing to our VM by sending +a transaction via the CLI. In particular, we want to send a transaction with the +following action: + +- Transfer + - Recipient: the zero address + - Value: 12 + - Memo: "Hello World!" (in hex) + +With our action specified, let's call the following command: + +```bash +./hypersdk-cli tx Transfer --data to=0x000000000000000000000000000000000000000000000000000000000000000000,value=12,memo=0x48656c6c6f20576f726c6421 +``` + +If all goes well, you should see the following: + +```bash +✅ Transaction successful (txID: Cg6N7x6Z2apwMc46heJ8mFMFk2H9CEhNxiUsicrNMnDbyC3ZU) +sender_balance: 9999999969888 +receiver_balance: 12 +``` + +Congrats! You've just sent a transaction to your implementation of MorpheusVM. +To double check that your transaction did indeed go through, we can again query +the balance of our account: + +```bash +./hypersdk-cli balance +✅ Balance: 9999999969888 +``` + +However, the CLI is not just limited to just the `Transfer` action. To see what +actions you can call, you can use the following: + +```bash +./hypersdk-cli actions + +--- +Transfer + +Inputs: + to: Address + value: uint64 + memo: []uint8 + +Outputs: + sender_balance: uint64 + receiver_balance: uint64 +``` + +Now that we have a good idea of what we can do via the HyperSDK-CLI, it's time +to shut down our VM. To do this, we can run the following: + +```bash +./scripts/stop.sh +``` + +If all went well, you should see the following: + +```bash +Removing symlink /Users/rodrigo.villar/.tmpnet/networks/latest_morpheusvm-e2e-tests +Stopping network +``` + +## Conclusion + +In this section of the MorpheusVM tutorial, we were able to interact with our +implementation of MorpheusVM by using the HyperSDK-CLI. From b0eee403011154d960640d1d3c395075aa0d5018 Mon Sep 17 00:00:00 2001 From: Rodrigo Villar Date: Fri, 1 Nov 2024 10:31:31 -0400 Subject: [PATCH 02/15] add setup interlude --- docs/tutorials/morpheusvm/1_morpheusvm.md | 2 +- docs/tutorials/morpheusvm/4_interlude.md | 187 ++++++++++++++++++ .../morpheusvm/{4_cli.md => 5_cli.md} | 48 ----- 3 files changed, 188 insertions(+), 49 deletions(-) create mode 100644 docs/tutorials/morpheusvm/4_interlude.md rename docs/tutorials/morpheusvm/{4_cli.md => 5_cli.md} (76%) diff --git a/docs/tutorials/morpheusvm/1_morpheusvm.md b/docs/tutorials/morpheusvm/1_morpheusvm.md index 1f9aae0f53..e26278c01f 100644 --- a/docs/tutorials/morpheusvm/1_morpheusvm.md +++ b/docs/tutorials/morpheusvm/1_morpheusvm.md @@ -72,7 +72,7 @@ import ( ) const ( - Name = "tutorialvm" + Name = "morpheusvm" ) var ID ids.ID diff --git a/docs/tutorials/morpheusvm/4_interlude.md b/docs/tutorials/morpheusvm/4_interlude.md new file mode 100644 index 0000000000..009c20bf1e --- /dev/null +++ b/docs/tutorials/morpheusvm/4_interlude.md @@ -0,0 +1,187 @@ +# Interlude + +In the previous sections, we built our own implementation of MorpheusVM and +tested it to see that it worked as expected. In the upcoming sections, we will +be completing our VM journey by spinning up `TutorialVM` on a local network and +interacting with it via the HyperSDK-CLI. + +However, before we get to the exciting part of this tutorial series, we need to +have some things set up before we can spin up `TutorialVM` on a local network. +In this interlude, we will focus on the following: + +- Setting up our run/stop scripts +- Setting up our VM binary generator +- Installing our CLI + +Let's get started! + +## Script Setup + +The script that we'll be using will allow us to start and stop a local network +running `TutorialVM`. To get started, in `examples/tutorial`, run the following commands: + +```bash +mkdir scripts +copy ../morpheusvm/scripts/run.sh +copy ../morpheusvm/scripts/stop.sh +``` + +The commands above created a new folder named `scripts` and copied the run/stop +scripts from MorpheusVM into our scripts folder. + +## Adding a VM Binary Generator + +Although the run/stop scripts we just created will take care of spinning up and +stopping our network, it still requires us to define a way for our VM binary to +be generated. + +To start, let's run the following commands: + +```bash +mkdir -p cmd/tutorialvm +mkdir cmd/tutorial/vm/version +touch cmd/tutorialvm/main.go +``` + +Let's first focus on `main.go`. Here, let's implement the following: + +```go +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package main + +import ( + "context" + "fmt" + "os" + + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/utils/ulimit" + "github.com/ava-labs/avalanchego/vms/rpcchainvm" + "github.com/spf13/cobra" + + "github.com/ava-labs/hypersdk/examples/tutorialvm/cmd/morpheusvm/version" + "github.com/ava-labs/hypersdk/examples/tutorialvm/vm" +) + +var rootCmd = &cobra.Command{ + Use: "morpheusvm", + Short: "BaseVM agent", + SuggestFor: []string{"morpheusvm"}, + RunE: runFunc, +} + +func init() { + cobra.EnablePrefixMatching = true +} + +func init() { + rootCmd.AddCommand( + version.NewCommand(), + ) +} + +func main() { + if err := rootCmd.Execute(); err != nil { + fmt.Fprintf(os.Stderr, "morpheusvm failed %v\n", err) + os.Exit(1) + } + os.Exit(0) +} + +func runFunc(*cobra.Command, []string) error { + if err := ulimit.Set(ulimit.DefaultFDLimit, logging.NoLog{}); err != nil { + return fmt.Errorf("%w: failed to set fd limit correctly", err) + } + + vm, err := vm.New() + if err != nil { + return err + } + return rpcchainvm.Serve(context.TODO(), vm) +} +``` + +Next, in `version.go`, let's implement the following: + +```go +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package version + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/ava-labs/hypersdk/examples/tutorialvm/consts" +) + +func init() { + cobra.EnablePrefixMatching = true +} + +// NewCommand implements "morpheusvm version" command. +func NewCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "version", + Short: "Prints out the verson", + RunE: versionFunc, + } + return cmd +} + +func versionFunc(*cobra.Command, []string) error { + fmt.Printf("%s@%s (%s)\n", consts.Name, consts.Version, consts.ID) + return nil +} +``` + +## CLI Installation + +To start, we'll want to compile the HyperSDK-CLI. If you're in +`examples/tutorial`, you can run the following: + +```bash +go build -o ./hypersdk-cli ../../cmd/hypersdk-cli/ +``` + +To confirm that your build of the HyperSDK-CLI was successful, run the following +command: + +```bash +./hypersdk-cli +``` + +You should see the following: + +```bash +A CLI application for performing read and write actions on HyperSDK-based chains. + +Usage: + hypersdk-cli [command] + +Available Commands: + actions Print the list of actions available in the ABI + address Print current key address + balance Get the balance of an address + completion Generate the autocompletion script for the specified shell + endpoint Manage endpoint + help Help about any command + key Manage keys + ping Ping the endpoint + read Read data from the chain + tx Execute a transaction on the chain + +Flags: + --endpoint string Override the default endpoint + -h, --help help for hypersdk-cli + --key string Private ED25519 key as hex string + -o, --output string Output format (text or json) (default "text") + +Use "hypersdk-cli [command] --help" for more information about a command. +``` + +With all the above set up, we're now ready to use the CLI! diff --git a/docs/tutorials/morpheusvm/4_cli.md b/docs/tutorials/morpheusvm/5_cli.md similarity index 76% rename from docs/tutorials/morpheusvm/4_cli.md rename to docs/tutorials/morpheusvm/5_cli.md index 1dc530a646..4fadfe69c3 100644 --- a/docs/tutorials/morpheusvm/4_cli.md +++ b/docs/tutorials/morpheusvm/5_cli.md @@ -7,57 +7,9 @@ to actually interact with our VM! In this tutorial, we'll go over the following: -- Installing the CLI locally - Setting up the CLI - Interacting with MorpheusVM via the CLI -## CLI Installation - -To start, we'll want to compile the HyperSDK-CLI. If you're in -`examples/tutorial`, you can run the following: - -```bash -go build -o ./hypersdk-cli ../../cmd/hypersdk-cli/ -``` - -To confirm that your build of the HyperSDK-CLI was successful, run the following -command: - -```bash -./hypersdk-cli -``` - -You should see the following: - -```bash -A CLI application for performing read and write actions on HyperSDK-based chains. - -Usage: - hypersdk-cli [command] - -Available Commands: - actions Print the list of actions available in the ABI - address Print current key address - balance Get the balance of an address - completion Generate the autocompletion script for the specified shell - endpoint Manage endpoint - help Help about any command - key Manage keys - ping Ping the endpoint - read Read data from the chain - tx Execute a transaction on the chain - -Flags: - --endpoint string Override the default endpoint - -h, --help help for hypersdk-cli - --key string Private ED25519 key as hex string - -o, --output string Output format (text or json) (default "text") - -Use "hypersdk-cli [command] --help" for more information about a command. -``` - -With the HyperSDK-CLI built, we can now move on to setting it up. - ## CLI Setup In this section, we'll want to both start our network along with passing in the From f364023b6bb0d846652d646cbd7e14e537f097f5 Mon Sep 17 00:00:00 2001 From: Rodrigo Villar Date: Mon, 4 Nov 2024 12:27:44 -0500 Subject: [PATCH 03/15] start doc on procedural tests --- docs/tutorials/morpheusvm/3_testing.md | 112 +++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/docs/tutorials/morpheusvm/3_testing.md b/docs/tutorials/morpheusvm/3_testing.md index edbb65ea9d..3183f5c4d0 100644 --- a/docs/tutorials/morpheusvm/3_testing.md +++ b/docs/tutorials/morpheusvm/3_testing.md @@ -16,6 +16,9 @@ This section will consist of the following: - Implementing a way to feed our workload tests to the HyperSDK workload test framework +In addition to adding workload tests, we will also add procedural tests which +will allow us to write singular unit tests. + ## Workload Scripts We start by reusing the workload script from MorpheusVM. In `tutorial/`, create @@ -392,6 +395,115 @@ integration test framework is time-intensive. By using the HyperSDK integration test framework, we can defer most tasks to it and solely focus on defining the workload tests. +## Registry Tests + +Now that we've added our workload tests, we can focus on adding a simple +registry test. In short, registry tests allow us to run an integration test on +our VM in a unit-test-esque manner. To start, run the following command: + +```bash +touch ./tests/transfer.go +``` + +As the name suggests, we'll be writing a registry test to test the `Transfer` +action. Within `transfer.go`, we can start by pasting the following in: + +```go +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package tests + +import ( + "context" + "time" + + "github.com/stretchr/testify/require" + + "github.com/ava-labs/hypersdk/auth" + "github.com/ava-labs/hypersdk/chain" + "github.com/ava-labs/hypersdk/crypto/ed25519" + "github.com/ava-labs/hypersdk/examples/morpheusvm/actions" + "github.com/ava-labs/hypersdk/examples/morpheusvm/tests/workload" + "github.com/ava-labs/hypersdk/tests/registry" + + tworkload "github.com/ava-labs/hypersdk/tests/workload" + ginkgo "github.com/onsi/ginkgo/v2" +) + +// TestsRegistry initialized during init to ensure tests are identical during ginkgo +// suite construction and test execution +// ref https://onsi.github.io/ginkgo/#mental-model-how-ginkgo-traverses-the-spec-hierarchy +var TestsRegistry = ®istry.Registry{} + +var _ = registry.Register(TestsRegistry, "Transfer Transaction", func(t ginkgo.FullGinkgoTInterface, tn tworkload.TestNetwork) { + +}) +``` + +In the code above, we have `TestsRegistry`: as the name suggest, this is a +registry of all the procedural tests that we want to run against our VM. +Afterwards, we have the following snippet: + +```go +registry.Register(TestsRegistry, "Transfer Transaction", func(t ginkgo.FullGinkgoTInterface, tn tworkload.TestNetwork) { + +}) +``` + +Here, we are adding a procedural test to `TestRegistry`. However, what we're +missing is the actual test logic itself. In short, here's what we want to do in +our testing logic: + +- Setup necessary values +- Create our test TX +- Send our TX +- Require that our TX is sent and that the outputs are as expected + +Focusing on the first step, we can write the following inside the anonymous +function: + +```go + require := require.New(t) + other, err := ed25519.GeneratePrivateKey() + require.NoError(err) + toAddress := auth.NewED25519Address(other.PublicKey()) + + networkConfig := tn.Configuration().(*workload.NetworkConfiguration) + spendingKey := networkConfig.Keys()[0] +``` + +Next, we'll create our test transaction. In short, we'll want to send a value of +`1` to `To`. Therefore, we have: + +```go + tx, err := tn.GenerateTx(context.Background(), []chain.Action{&actions.Transfer{ + To: toAddress, + Value: 1, + }}, + auth.NewED25519Factory(spendingKey), + ) + require.NoError(err) +``` + +Finally, we'll want to send our TX and do the checks mentioned in the last step. +This step will consist of the following: + +- Creating a context with a deadline of 2 seconds + - If the test takes longer than 2 seconds, it will fail +- Calling `ConfirmTxs` with our TX being passed in + +The function `ConfirmTXs`, in particular, is useful as it checks that our TX was +sent and that, if finalized, our transaction has the expected outputs. We have +the following: + +```go + timeoutCtx, timeoutCtxFnc := context.WithDeadline(context.Background(), time.Now().Add(2*time.Second)) + defer timeoutCtxFnc() + + require.NoError(tn.ConfirmTxs(timeoutCtx, []*chain.Transaction{tx})) +``` + ## Testing Our VM Putting everything together, its now time to test our work! To do this, run the From f75171c5cfa59ddff5e4d357d6f22913891701f1 Mon Sep 17 00:00:00 2001 From: Rodrigo Villar Date: Mon, 4 Nov 2024 13:33:05 -0500 Subject: [PATCH 04/15] first pass of removing state manager refs --- docs/tutorials/morpheusvm/1_morpheusvm.md | 104 ++++++++++------------ 1 file changed, 45 insertions(+), 59 deletions(-) diff --git a/docs/tutorials/morpheusvm/1_morpheusvm.md b/docs/tutorials/morpheusvm/1_morpheusvm.md index e26278c01f..fe1613e2da 100644 --- a/docs/tutorials/morpheusvm/1_morpheusvm.md +++ b/docs/tutorials/morpheusvm/1_morpheusvm.md @@ -10,7 +10,7 @@ this tutorial is as follows: - Creating Transfer, Pt. 1 - Implementing Storage - Read/write functions - - StateManager + - BalanceHandler - Creating Transfer, Pt. 2 - Bringing Everything Together - Options @@ -198,65 +198,43 @@ constructing and writing key-value pairs directly from our action. When building a VM with the HyperSDK, the VM developer defines their own state storage layout. The HyperSDK also stores metadata in the current state, and defers to the VM where to -store that data. Note: this will be changed in the future, so that developers get a default out -of the box and can choose to override it if needed instead of needing to provide it. +store that data. With that in mind, we'll break the storage down into two components: - Read/Write Helper Functions for VM specific state -- StateManager interface to tell the HyperSDK where to store its metadata +- `BalanceHandler` interface to tell the HyperSDK how to modify account balances Before that, in `tutorial/`, create a new folder called `storage`. ### Separating the State into Partitions -To start off, we'll add single byte prefixes to separate out the state into -metadata required by the HyperSDK and an address -> balance mapping. +To start off, we'll add a single byte prefix to separate a portion of our state +out for storing account balances. -Let's create the file `storage.go` in `storage/` with prefixes for balance, height, timestamp, and fees: +Let's create the file `storage.go` in `storage/` with a prefix for balances: ```golang package storage -const ( - // Active state - balancePrefix = 0x0 - heightPrefix = 0x1 - timestampPrefix = 0x2 - feePrefix = 0x3 +import ( + "github.com/ava-labs/hypersdk/state/metadata" ) -var ( - heightKey = []byte{heightPrefix} - timestampKey = []byte{timestampPrefix} - feeKey = []byte{feePrefix} -) +const balancePrefix byte = metadata.DefaultMinimumPrefix ``` -### Implementing HyperSDK Metadata Handlers - -Now, we'll implement functions that return the key where the HyperSDK should -store the chain height, timestamp, and fee dimensions. This is currently defined by -the VM, so that it has full control of its own state layout, but could be abstracted -away and moved into the HyperSDK in the future. - -Let's add the following functions to `storage.go`: - -```golang -func HeightKey() (k []byte) { - return heightKey -} +In the above, we are referring to `metadata.DefaultMinimumPrefix`. In reality, +there are several partitions of state that the HyperSDK requires (e.g. a +partition for storing the latest fees, a partition for storing the latest block +height, etc.). However, as we'll see later on, we can pass in a default layout +for the rest of the required partitions and rely on +`metadata.DefaultMinimumPrefix` for defining any remaining partitions without +having to worry about collisions in state. -func TimestampKey() (k []byte) { - return timestampKey -} - -func FeeKey() (k []byte) { - return feeKey -} -``` +### Implementing HyperSDK Metadata Handlers -Next, we'll define the `BalanceKey` function. `BalanceKey` will return the +We'll define the `BalanceKey` function. `BalanceKey` will return the state key that stores the provided address' balance. The HyperSDK requires using [size-encoded storage keys](../../explanation/features.md#size-encoded-storage-keys), @@ -461,13 +439,13 @@ var ( Going back to `storage.go`, you should see that the errors from behind are now gone. -### State Manager +### Balance Handler -Now we'll implement the `chain.StateManager` interface to tell the HyperSDK -how to modify our VM's state when it needs to store metadata or charge fees. +Now, we'll implement the `chain.BalanceHandler` interface to tell the HyperSDK +how to modify account balances. Let's start off by creating a new `state_manager.go` file in `storage/` and adding a new -`StateManager` type with function stubs for each of the required functions: +`BalanceHandler` type with function stubs for each of the required functions: ```golang package storage @@ -480,35 +458,43 @@ import ( "github.com/ava-labs/hypersdk/state" ) -var _ chain.StateManager = (*StateManager)(nil) +var _ chain.BalanceHandler = (*BalanceHandler)(nil) -type StateManager struct{} +type BalanceHandler struct{} -func (s *StateManager) FeeKey() []byte { +func (*BalanceHandler) SponsorStateKeys(addr codec.Address) state.Keys { panic("unimplemented") } -func (s *StateManager) HeightKey() []byte { - panic("unimplemented") -} - -func (s *StateManager) TimestampKey() []byte { - panic("unimplemented") -} - -func (s *StateManager) AddBalance(ctx context.Context, addr codec.Address, mu state.Mutable, amount uint64, createAccount bool) error { +func (*BalanceHandler) CanDeduct( + ctx context.Context, + addr codec.Address, + im state.Immutable, + amount uint64, +) error { panic("unimplemented") } -func (s *StateManager) CanDeduct(ctx context.Context, addr codec.Address, im state.Immutable, amount uint64) error { +func (*BalanceHandler) Deduct( + ctx context.Context, + addr codec.Address, + mu state.Mutable, + amount uint64, +) error { panic("unimplemented") } -func (s *StateManager) Deduct(ctx context.Context, addr codec.Address, mu state.Mutable, amount uint64) error { +func (*BalanceHandler) AddBalance( + ctx context.Context, + addr codec.Address, + mu state.Mutable, + amount uint64, + createAccount bool, +) error { panic("unimplemented") } -func (s *StateManager) SponsorStateKeys(addr codec.Address) state.Keys { +func (*BalanceHandler) GetBalance(ctx context.Context, addr codec.Address, im state.Immutable) (uint64, error) { panic("unimplemented") } ``` From 47d21c620f1b7c94084206f93081bfa9f29a18dd Mon Sep 17 00:00:00 2001 From: Rodrigo Villar Date: Mon, 4 Nov 2024 13:51:13 -0500 Subject: [PATCH 05/15] better DefaultMinimumPrefix explanation --- docs/tutorials/morpheusvm/1_morpheusvm.md | 45 ++++++++++------------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/docs/tutorials/morpheusvm/1_morpheusvm.md b/docs/tutorials/morpheusvm/1_morpheusvm.md index fe1613e2da..e8cef9b961 100644 --- a/docs/tutorials/morpheusvm/1_morpheusvm.md +++ b/docs/tutorials/morpheusvm/1_morpheusvm.md @@ -224,13 +224,25 @@ import ( const balancePrefix byte = metadata.DefaultMinimumPrefix ``` -In the above, we are referring to `metadata.DefaultMinimumPrefix`. In reality, -there are several partitions of state that the HyperSDK requires (e.g. a -partition for storing the latest fees, a partition for storing the latest block -height, etc.). However, as we'll see later on, we can pass in a default layout -for the rest of the required partitions and rely on -`metadata.DefaultMinimumPrefix` for defining any remaining partitions without -having to worry about collisions in state. +In addition to defining a prefix for account balances, the HyperSDK actually +requires us to define other prefixes as well (such as those for storing fees, +the latest block height, etc.). However, we can pass in a default layout which +handles those other prefixes, leaving us with just having to define a balance +prefix. + +As for `metadata.DefaultMinimumPrefix`, this prefix is the lowest prefix +available to us if we use the default layout. By using this prefix, we have the +following contract: + +- Using any user-defined prefix that is equal to or greater than `metadata.DefaultMinimumPrefix` + will not result in any state collisions with the HyperSDK +- Using any user-defined prefix that is less than `metadata.DefaultMinimumPrefix` will + most likely cause a state collision with the HyperSDK + +In layman's terms, we can think of prefixes less than +`metadata.DefaultMinimumPrefix` as prefixes that *only the HyperSDK should +touch*, and any prefixes greater than `metadata.DefaultMinimumPrefix` as +prefixes that the VM developer can use. ### Implementing HyperSDK Metadata Handlers @@ -502,24 +514,7 @@ func (*BalanceHandler) GetBalance(ctx context.Context, addr codec.Address, im st The type assertion should pass, so now we can go through and implement each function correctly. -For each of the metadata functions, we'll simply return the state keys -we already defined when partitioning our state: - -```golang -func (*StateManager) HeightKey() []byte { - return HeightKey() -} - -func (*StateManager) TimestampKey() []byte { - return TimestampKey() -} - -func (*StateManager) FeeKey() []byte { - return FeeKey() -} -``` - -Now, we'll implement the balance handler functions re-using the helpers +We'll implement the balance handler functions re-using the helpers we've already implemented: ```golang From 0f859fe0a0efb8b59a68be9b47bfc02093b03d81 Mon Sep 17 00:00:00 2001 From: Rodrigo Villar Date: Mon, 4 Nov 2024 13:56:35 -0500 Subject: [PATCH 06/15] remove statemanager --- docs/tutorials/morpheusvm/1_morpheusvm.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/tutorials/morpheusvm/1_morpheusvm.md b/docs/tutorials/morpheusvm/1_morpheusvm.md index e8cef9b961..cd719ca48d 100644 --- a/docs/tutorials/morpheusvm/1_morpheusvm.md +++ b/docs/tutorials/morpheusvm/1_morpheusvm.md @@ -244,7 +244,7 @@ In layman's terms, we can think of prefixes less than touch*, and any prefixes greater than `metadata.DefaultMinimumPrefix` as prefixes that the VM developer can use. -### Implementing HyperSDK Metadata Handlers +### Defining Account Balance Keys We'll define the `BalanceKey` function. `BalanceKey` will return the state key that stores the provided address' balance. @@ -518,7 +518,7 @@ We'll implement the balance handler functions re-using the helpers we've already implemented: ```golang -func (*StateManager) CanDeduct( +func (*BalanceHandler) CanDeduct( ctx context.Context, addr codec.Address, im state.Immutable, @@ -534,7 +534,7 @@ func (*StateManager) CanDeduct( return nil } -func (*StateManager) Deduct( +func (*BalanceHandler) Deduct( ctx context.Context, addr codec.Address, mu state.Mutable, @@ -543,7 +543,7 @@ func (*StateManager) Deduct( return SubBalance(ctx, mu, addr, amount) } -func (*StateManager) AddBalance( +func (*BalanceHandler) AddBalance( ctx context.Context, addr codec.Address, mu state.Mutable, @@ -553,6 +553,10 @@ func (*StateManager) AddBalance( _, err := AddBalance(ctx, mu, addr, amount, createAccount) return err } + +func (*BalanceHandler) GetBalance(ctx context.Context, addr codec.Address, im state.Immutable) (uint64, error) { + return GetBalance(ctx, im, addr) +} ``` Finally, we need to implement `SponsorStateKeys`. From f97ded8ae048eb49b8b104b4ce07bfb403544798 Mon Sep 17 00:00:00 2001 From: Rodrigo Villar Date: Mon, 4 Nov 2024 14:12:44 -0500 Subject: [PATCH 07/15] add result doc --- docs/tutorials/morpheusvm/1_morpheusvm.md | 35 +++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/docs/tutorials/morpheusvm/1_morpheusvm.md b/docs/tutorials/morpheusvm/1_morpheusvm.md index cd719ca48d..fec3c293f7 100644 --- a/docs/tutorials/morpheusvm/1_morpheusvm.md +++ b/docs/tutorials/morpheusvm/1_morpheusvm.md @@ -659,6 +659,41 @@ var ( ) ``` +Next, we'll want to define a return type for `Execute()`. In this case, we want +to define a struct which stores the following values: + +- The new balance of the sender +- The new balance of the recipient + +By using a struct which implements the `codec.Typed` interface, applications +such as a frontend for our VM will be able to marshal/unmarshal our struct in a +clean manner. To start, let's define the following: + +```go +var _ codec.Typed = (*TransferResult)(nil) + +type TransferResult struct { + SenderBalance uint64 `serialize:"true" json:"sender_balance"` + ReceiverBalance uint64 `serialize:"true" json:"receiver_balance"` +} + +func (*TransferResult) GetTypeID() uint8 { + panic("unimplemented") +} +``` + +Here, we have `TransferResult` which has as fields the new balances we mentioned +earlier along with a `GetTypeID()` method. This method requires us to return a +unique identifier associated with this result. Since we already assigned a +unique type ID to our `Transfer` action, we can use this ID for our result +(action IDs and result IDs are different). Therefore, we have: + +```go +func (*TransferResult) GetTypeID() uint8 { + return mconsts.TransferID // Common practice is to use the action ID +} +``` + We can now define `Execute()`. Recall that we should first check any invariants and then execute the necessary state changes. Therefore, we have the following: From 57326185ec18d2827cc41d49d6ab1a2fb5113d37 Mon Sep 17 00:00:00 2001 From: Rodrigo Villar Date: Mon, 4 Nov 2024 14:16:34 -0500 Subject: [PATCH 08/15] option nit --- docs/tutorials/morpheusvm/2_options.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/tutorials/morpheusvm/2_options.md b/docs/tutorials/morpheusvm/2_options.md index 7a38196f33..4aaa05bb91 100644 --- a/docs/tutorials/morpheusvm/2_options.md +++ b/docs/tutorials/morpheusvm/2_options.md @@ -272,10 +272,6 @@ func (*Parser) AuthCodec() chain.AuthCodec { return AuthParser } -func (*Parser) StateManager() chain.StateManager { - return &storage.StateManager{} -} - func NewParser(genesis *genesis.DefaultGenesis) chain.Parser { return &Parser{genesis: genesis} } From 55fa1dc22b2dd2aa12516451d7f868a63f690627 Mon Sep 17 00:00:00 2001 From: Rodrigo Villar Date: Mon, 4 Nov 2024 15:15:15 -0500 Subject: [PATCH 09/15] rewrite testing doc --- docs/tutorials/morpheusvm/3_testing.md | 529 +++++++++++++------------ 1 file changed, 265 insertions(+), 264 deletions(-) diff --git a/docs/tutorials/morpheusvm/3_testing.md b/docs/tutorials/morpheusvm/3_testing.md index 3183f5c4d0..f193690c26 100644 --- a/docs/tutorials/morpheusvm/3_testing.md +++ b/docs/tutorials/morpheusvm/3_testing.md @@ -7,17 +7,22 @@ Let's quickly recap what we've done so far: With the above, our code should work exactly like the version of MorpheusVM found in `examples/`. To verify this though, we're going to apply the same -workload tests used in MorpheusVM against our VM. +integration tests used in MorpheusVM against our VM. This section will consist of the following: - Implementing a bash script to run our workload tests -- Implementing the workload tests itself -- Implementing a way to feed our workload tests to the HyperSDK workload test - framework +- Implementing workload integration tests +- Implementing procedural integration tests +- Registering our integration tests -In addition to adding workload tests, we will also add procedural tests which -will allow us to write singular unit tests. +At the end of this section, you'll have implemented integration tests that will +have tested your VM via the following: + +- Workload tests: this method tests your VM by applying a large quantity of + generic transactions and making sure that each is applied as expected +- Procedural tests: this method tests your VM by applying a specific transaction + and making sure that it is applied as expected ## Workload Scripts @@ -27,11 +32,13 @@ called `tests.integration.sh` and paste the following: ```bash #!/usr/bin/env bash +# Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +# See the file LICENSE for licensing terms. set -e if ! [[ "$0" =~ scripts/tests.integration.sh ]]; then - echo "must be run from tutorial root" + echo "must be run from morpheusvm root" exit 255 fi @@ -59,9 +66,9 @@ run \ go tool cover -html=integration.coverage.out -o=integration.coverage.html ``` -This workload script will both set up our testing environment and execute the -workload tests. To make sure that our script will run at the end of this -section, run the following command: +This script will both set up our testing environment and execute the workload +tests. To make sure that our script will run at the end of this section, run the +following command: ```bash chmod +x ./scripts/tests.integration.sh @@ -70,20 +77,25 @@ chmod +x ./scripts/tests.integration.sh ## Implementing Our Workload Tests Start by creating a subdirectory in `tutorial/` named `tests`. Within `tests/`, -create a directory called `workload`. Within `workload/`, create a file called `workload.go`. This file is where we will -define the workload tests that will be used against our VM. +create a directory called `workload`. Within `workload`, create the following +files: -## Workload Initialization +- `generator.go` +- `genesis.go` -We start by defining the values necesssary for the workload tests along with an -`init()` function: +In short, `generator.go` will be responsible for generating transactions that +contain the `Transfer` action while `genesis.go` will be responsible for +providing the network configuration for our integration tests. -```golang +### Implementing the Generator + +In `generator.go`, we start by implementing the following: + +```go package workload import ( "context" - "math" "time" "github.com/ava-labs/avalanchego/ids" @@ -94,128 +106,54 @@ import ( "github.com/ava-labs/hypersdk/auth" "github.com/ava-labs/hypersdk/chain" "github.com/ava-labs/hypersdk/codec" - "github.com/ava-labs/hypersdk/crypto/bls" "github.com/ava-labs/hypersdk/crypto/ed25519" - "github.com/ava-labs/hypersdk/crypto/secp256r1" - "github.com/ava-labs/hypersdk/examples/tutorial/actions" - "github.com/ava-labs/hypersdk/examples/tutorial/consts" - "github.com/ava-labs/hypersdk/examples/tutorial/vm" - "github.com/ava-labs/hypersdk/fees" - "github.com/ava-labs/hypersdk/genesis" + "github.com/ava-labs/hypersdk/examples/morpheusvm/actions" + "github.com/ava-labs/hypersdk/examples/morpheusvm/consts" + "github.com/ava-labs/hypersdk/examples/morpheusvm/vm" "github.com/ava-labs/hypersdk/tests/workload" ) -const ( - initialBalance uint64 = 10_000_000_000_000 - txCheckInterval = 100 * time.Millisecond -) +var _ workload.TxGenerator = (*TxGenerator)(nil) -var ( - _ workload.TxWorkloadFactory = (*workloadFactory)(nil) - _ workload.TxWorkloadIterator = (*simpleTxWorkload)(nil) - ed25519HexKeys = []string{ - "323b1d8f4eed5f0da9da93071b034f2dce9d2d22692c172f3cb252a64ddfafd01b057de320297c29ad0c1f589ea216869cf1938d88c9fbd70d6748323dbf2fa7", //nolint:lll - "8a7be2e0c9a2d09ac2861c34326d6fe5a461d920ba9c2b345ae28e603d517df148735063f8d5d8ba79ea4668358943e5c80bc09e9b2b9a15b5b15db6c1862e88", //nolint:lll - } - ed25519PrivKeys = make([]ed25519.PrivateKey, len(ed25519HexKeys)) - ed25519Addrs = make([]codec.Address, len(ed25519HexKeys)) - ed25519AuthFactories = make([]*auth.ED25519Factory, len(ed25519HexKeys)) -) +const txCheckInterval = 100 * time.Millisecond -func init() { - for i, keyHex := range ed25519HexKeys { - privBytes, err := codec.LoadHex(keyHex, ed25519.PrivateKeyLen) - if err != nil { - panic(err) - } - priv := ed25519.PrivateKey(privBytes) - ed25519PrivKeys[i] = priv - ed25519AuthFactories[i] = auth.NewED25519Factory(priv) - addr := auth.NewED25519Address(priv.PublicKey()) - ed25519Addrs[i] = addr - } -} -``` - -Our workload tests revolve around the following workflow: - -- Use transaction workloads to generate an arbitrary TX - - This TX contains just a `Transfer` action -- Send generated TX to a running instance of our VM -- Assert that the TX execute and applied the correct state changes - -The code snippet above provides the foundation to define `workloadFactory` and -`simpleTxWorkload`. We now implement these structs along with their methods: - -```golang -type workloadFactory struct { - factories []*auth.ED25519Factory - addrs []codec.Address +type TxGenerator struct { + factory *auth.ED25519Factory } -func New(minBlockGap int64) (*genesis.DefaultGenesis, workload.TxWorkloadFactory, error) { - customAllocs := make([]*genesis.CustomAllocation, 0, len(ed25519Addrs)) - for _, prefundedAddr := range ed25519Addrs { - customAllocs = append(customAllocs, &genesis.CustomAllocation{ - Address: prefundedAddr, - Balance: initialBalance, - }) +func NewTxGenerator(key ed25519.PrivateKey) *TxGenerator { + return &TxGenerator{ + factory: auth.NewED25519Factory(key), } +} +``` - genesis := genesis.NewDefaultGenesis(customAllocs) - // Set WindowTargetUnits to MaxUint64 for all dimensions to iterate full mempool during block building. - genesis.Rules.WindowTargetUnits = fees.Dimensions{math.MaxUint64, math.MaxUint64, math.MaxUint64, math.MaxUint64, math.MaxUint64} - // Set all limits to MaxUint64 to avoid limiting block size for all dimensions except bandwidth. Must limit bandwidth to avoid building - // a block that exceeds the maximum size allowed by AvalancheGo. - genesis.Rules.MaxBlockUnits = fees.Dimensions{1800000, math.MaxUint64, math.MaxUint64, math.MaxUint64, math.MaxUint64} - genesis.Rules.MinBlockGap = minBlockGap +Next, we'll want to implement a method to our `TxGenerator` that will allow it +to produce a valid transaction with `Transfer` on the fly. Therefore, we have +the following: - return genesis, &workloadFactory{ - factories: ed25519AuthFactories, - addrs: ed25519Addrs, - }, nil -} -func (f *workloadFactory) NewSizedTxWorkload(uri string, size int) (workload.TxWorkloadIterator, error) { +```go +func (g *TxGenerator) GenerateTx(ctx context.Context, uri string) (*chain.Transaction, workload.TxAssertion, error) { + // TODO: no need to generate the clients every tx cli := jsonrpc.NewJSONRPCClient(uri) lcli := vm.NewJSONRPCClient(uri) - return &simpleTxWorkload{ - factory: f.factories[0], - cli: cli, - lcli: lcli, - size: size, - }, nil -} -type simpleTxWorkload struct { - factory *auth.ED25519Factory - cli *jsonrpc.JSONRPCClient - lcli *vm.JSONRPCClient - count int - size int -} - -func (g *simpleTxWorkload) Next() bool { - return g.count < g.size -} - -func (g *simpleTxWorkload) GenerateTxWithAssertion(ctx context.Context) (*chain.Transaction, workload.TxAssertion, error) { - g.count++ - other, err := ed25519.GeneratePrivateKey() + to, err := ed25519.GeneratePrivateKey() if err != nil { return nil, nil, err } - aother := auth.NewED25519Address(other.PublicKey()) - parser, err := g.lcli.Parser(ctx) + toAddress := auth.NewED25519Address(to.PublicKey()) + parser, err := lcli.Parser(ctx) if err != nil { return nil, nil, err } - _, tx, _, err := g.cli.GenerateTransaction( + _, tx, _, err := cli.GenerateTransaction( ctx, parser, []chain.Action{&actions.Transfer{ - To: aother, + To: toAddress, Value: 1, }}, g.factory, @@ -225,184 +163,173 @@ func (g *simpleTxWorkload) GenerateTxWithAssertion(ctx context.Context) (*chain. } return tx, func(ctx context.Context, require *require.Assertions, uri string) { - indexerCli := indexer.NewClient(uri) - success, _, err := indexerCli.WaitForTransaction(ctx, txCheckInterval, tx.ID()) - require.NoError(err) - require.True(success) - lcli := vm.NewJSONRPCClient(uri) - balance, err := lcli.Balance(ctx, aother) - require.NoError(err) - require.Equal(uint64(1), balance) + confirmTx(ctx, require, uri, tx.ID(), toAddress, 1) }, nil } +``` -func (f *workloadFactory) NewWorkloads(uri string) ([]workload.TxWorkloadIterator, error) { - blsPriv, err := bls.GeneratePrivateKey() - if err != nil { - return nil, err - } - blsPub := bls.PublicFromPrivateKey(blsPriv) - blsAddr := auth.NewBLSAddress(blsPub) - blsFactory := auth.NewBLSFactory(blsPriv) - - secpPriv, err := secp256r1.GeneratePrivateKey() - if err != nil { - return nil, err - } - secpPub := secpPriv.PublicKey() - secpAddr := auth.NewSECP256R1Address(secpPub) - secpFactory := auth.NewSECP256R1Factory(secpPriv) +In addition to generating a valid transaction, this method returns an anonymous +function which calls `confirmTX`. `confirmTX` sends the generated TX to the VM, +makes sure that it was accepted, and checks that the result associated with the +TX is expected. With this in mind, we have: - cli := jsonrpc.NewJSONRPCClient(uri) - networkID, _, blockchainID, err := cli.Network(context.Background()) - if err != nil { - return nil, err - } +```golang +func confirmTx(ctx context.Context, require *require.Assertions, uri string, txID ids.ID, receiverAddr codec.Address, receiverExpectedBalance uint64) { + indexerCli := indexer.NewClient(uri) + success, _, err := indexerCli.WaitForTransaction(ctx, txCheckInterval, txID) + require.NoError(err) + require.True(success) lcli := vm.NewJSONRPCClient(uri) + balance, err := lcli.Balance(ctx, receiverAddr) + require.NoError(err) + require.Equal(receiverExpectedBalance, balance) + txRes, _, err := indexerCli.GetTx(ctx, txID) + require.NoError(err) + // TODO: perform exact expected fee, units check, and output check + require.NotZero(txRes.Fee) + require.Len(txRes.Outputs, 1) + transferOutputBytes := []byte(txRes.Outputs[0]) + require.Equal(consts.TransferID, transferOutputBytes[0]) + reader := codec.NewReader(transferOutputBytes, len(transferOutputBytes)) + transferOutputTyped, err := vm.OutputParser.Unmarshal(reader) + require.NoError(err) + transferOutput, ok := transferOutputTyped.(*actions.TransferResult) + require.True(ok) + require.Equal(receiverExpectedBalance, transferOutput.ReceiverBalance) +} +``` - generator := &mixedAuthWorkload{ - addressAndFactories: []addressAndFactory{ - {address: f.addrs[1], authFactory: f.factories[1]}, - {address: blsAddr, authFactory: blsFactory}, - {address: secpAddr, authFactory: secpFactory}, - }, - balance: initialBalance, - cli: cli, - lcli: lcli, - networkID: networkID, - chainID: blockchainID, - } +With our generator complete, we can now move onto implementing the network +configuration. - return []workload.TxWorkloadIterator{generator}, nil -} +### Implementing the Network Configuration -type addressAndFactory struct { - address codec.Address - authFactory chain.AuthFactory -} +In `genesis.go`, we first start by implementing a function which returns the +genesis of our VM: -type mixedAuthWorkload struct { - addressAndFactories []addressAndFactory - balance uint64 - cli *jsonrpc.JSONRPCClient - lcli *vm.JSONRPCClient - networkID uint32 - chainID ids.ID - count int -} +```go +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. -func (g *mixedAuthWorkload) Next() bool { - return g.count < len(g.addressAndFactories)-1 -} +package workload -func (g *mixedAuthWorkload) GenerateTxWithAssertion(ctx context.Context) (*chain.Transaction, workload.TxAssertion, error) { - defer func() { g.count++ }() +import ( + "encoding/json" + "math" + "time" - sender := g.addressAndFactories[g.count] - receiver := g.addressAndFactories[g.count+1] - expectedBalance := g.balance - 1_000_000 + "github.com/ava-labs/avalanchego/ids" - parser, err := g.lcli.Parser(ctx) - if err != nil { - return nil, nil, err - } - _, tx, _, err := g.cli.GenerateTransaction( - ctx, - parser, - []chain.Action{&actions.Transfer{ - To: receiver.address, - Value: expectedBalance, - }}, - sender.authFactory, - ) - if err != nil { - return nil, nil, err - } - g.balance = expectedBalance + "github.com/ava-labs/hypersdk/auth" + "github.com/ava-labs/hypersdk/codec" + "github.com/ava-labs/hypersdk/crypto/ed25519" + "github.com/ava-labs/hypersdk/examples/morpheusvm/consts" + "github.com/ava-labs/hypersdk/examples/morpheusvm/vm" + "github.com/ava-labs/hypersdk/fees" + "github.com/ava-labs/hypersdk/genesis" + "github.com/ava-labs/hypersdk/tests/workload" +) - return tx, func(ctx context.Context, require *require.Assertions, uri string) { - indexerCli := indexer.NewClient(uri) - success, _, err := indexerCli.WaitForTransaction(ctx, txCheckInterval, tx.ID()) - require.NoError(err) - require.True(success) - lcli := vm.NewJSONRPCClient(uri) - balance, err := lcli.Balance(ctx, receiver.address) - require.NoError(err) - require.Equal(expectedBalance, balance) - // TODO check tx fee + units (not currently available via API) - }, nil -} -``` +const ( + // default initial balance for each address + InitialBalance uint64 = 10_000_000_000_000 +) -## Using Our Workload Tests +var _ workload.TestNetworkConfiguration = &NetworkConfiguration{} -With our workload tests implemented, we now define a way for which our workload -tests can be utilized. To start, create a new folder named `integration` in -`tests/`. Inside `integration/`, create a new file `integration_test.go`. Here -copy-paste the following: +// hardcoded initial set of ed25519 keys. Each will be initialized with InitialBalance +var ed25519HexKeys = []string{ + "323b1d8f4eed5f0da9da93071b034f2dce9d2d22692c172f3cb252a64ddfafd01b057de320297c29ad0c1f589ea216869cf1938d88c9fbd70d6748323dbf2fa7", //nolint:lll + "8a7be2e0c9a2d09ac2861c34326d6fe5a461d920ba9c2b345ae28e603d517df148735063f8d5d8ba79ea4668358943e5c80bc09e9b2b9a15b5b15db6c1862e88", //nolint:lll +} -```golang -package integration_test +func newGenesis(keys []ed25519.PrivateKey, minBlockGap time.Duration) *genesis.DefaultGenesis { + // allocate the initial balance to the addresses + customAllocs := make([]*genesis.CustomAllocation, 0, len(keys)) + for _, key := range keys { + customAllocs = append(customAllocs, &genesis.CustomAllocation{ + Address: auth.NewED25519Address(key.PublicKey()), + Balance: InitialBalance, + }) + } -import ( - "encoding/json" - "testing" + genesis := genesis.NewDefaultGenesis(customAllocs) - "github.com/stretchr/testify/require" + // Set WindowTargetUnits to MaxUint64 for all dimensions to iterate full mempool during block building. + genesis.Rules.WindowTargetUnits = fees.Dimensions{math.MaxUint64, math.MaxUint64, math.MaxUint64, math.MaxUint64, math.MaxUint64} - "github.com/ava-labs/hypersdk/auth" - "github.com/ava-labs/hypersdk/crypto/ed25519" - "github.com/ava-labs/hypersdk/examples/tutorial/vm" - "github.com/ava-labs/hypersdk/tests/integration" + // Set all limits to MaxUint64 to avoid limiting block size for all dimensions except bandwidth. Must limit bandwidth to avoid building + // a block that exceeds the maximum size allowed by AvalancheGo. + genesis.Rules.MaxBlockUnits = fees.Dimensions{1800000, math.MaxUint64, math.MaxUint64, math.MaxUint64, math.MaxUint64} + genesis.Rules.MinBlockGap = minBlockGap.Milliseconds() - lconsts "github.com/ava-labs/hypersdk/examples/tutorial/consts" - tutorialWorkload "github.com/ava-labs/hypersdk/examples/tutorial/tests/workload" - ginkgo "github.com/onsi/ginkgo/v2" -) + genesis.Rules.NetworkID = uint32(1) + genesis.Rules.ChainID = ids.GenerateTestID() -func TestIntegration(t *testing.T) { - ginkgo.RunSpecs(t, "tutorial integration test suites") + return genesis } +``` -var _ = ginkgo.BeforeSuite(func() { - require := require.New(ginkgo.GinkgoT()) - genesis, workloadFactory, err := tutorialWorkload.New(0 /* minBlockGap: 0ms */) - require.NoError(err) +Next, using the values in `ed25519HexKeys`, we'll implement a function that +returns our private test keys: - genesisBytes, err := json.Marshal(genesis) - require.NoError(err) +```go +func newDefaultKeys() []ed25519.PrivateKey { + testKeys := make([]ed25519.PrivateKey, len(ed25519HexKeys)) + for i, keyHex := range ed25519HexKeys { + bytes, err := codec.LoadHex(keyHex, ed25519.PrivateKeyLen) + if err != nil { + panic(err) + } + testKeys[i] = ed25519.PrivateKey(bytes) + } - randomEd25519Priv, err := ed25519.GeneratePrivateKey() - require.NoError(err) + return testKeys +} +``` - randomEd25519AuthFactory := auth.NewED25519Factory(randomEd25519Priv) +Finally, we implement the network configuration required for our VM integrationt +tests: - // Setup imports the integration test coverage - integration.Setup( - vm.New, - genesisBytes, - lconsts.ID, - vm.CreateParser, - workloadFactory, - randomEd25519AuthFactory, - ) -}) +```go +type NetworkConfiguration struct { + workload.DefaultTestNetworkConfiguration + keys []ed25519.PrivateKey +} + +func (n *NetworkConfiguration) Keys() []ed25519.PrivateKey { + return n.keys +} + +func NewTestNetworkConfig(minBlockGap time.Duration) (*NetworkConfiguration, error) { + keys := newDefaultKeys() + genesis := newGenesis(keys, minBlockGap) + genesisBytes, err := json.Marshal(genesis) + if err != nil { + return nil, err + } + return &NetworkConfiguration{ + DefaultTestNetworkConfiguration: workload.NewDefaultTestNetworkConfiguration( + genesisBytes, + consts.Name, + vm.NewParser(genesis)), + keys: keys, + }, nil +} ``` -In `integration_test.go`, we are feeding our workload tests along with various -other values to the HyperSDK integration test library. Implementing an entire -integration test framework is time-intensive. By using the HyperSDK integration -test framework, we can defer most tasks to it and solely focus on defining the -workload tests. +Having implemented our workload tests, we can now move onto our procedural +tests. -## Registry Tests +## Implementing our Procedural Tests -Now that we've added our workload tests, we can focus on adding a simple -registry test. In short, registry tests allow us to run an integration test on -our VM in a unit-test-esque manner. To start, run the following command: +While workload tests allow us to test our transactions en masse, procedural +tests allow us to run an integration test on +our VM in a unit-test-esque manner. To start, in the `tests` folder, run the +following command: ```bash -touch ./tests/transfer.go +touch transfer.go ``` As the name suggests, we'll be writing a registry test to test the `Transfer` @@ -504,9 +431,79 @@ the following: require.NoError(tn.ConfirmTxs(timeoutCtx, []*chain.Transaction{tx})) ``` +## Registering our Workload Tests + +Although we've defined the integration tests themselves, we still need to +register them with the HyperSDK. That is, we need to pass our integration tests +into the HyperSDK integration test framework, which will run these tests on our +behalf. + + implemented, we now define a way for which our workload +tests can be utilized. To start, create a new folder named `integration` in +`tests/`. Inside `integration/`, create a new file `integration_test.go`. Here +copy-paste the following: + +```go +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package integration_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + _ "github.com/ava-labs/hypersdk/examples/morpheusvm/tests" // include the tests that are shared between the integration and e2e + + "github.com/ava-labs/hypersdk/auth" + "github.com/ava-labs/hypersdk/crypto/ed25519" + "github.com/ava-labs/hypersdk/examples/morpheusvm/tests/workload" + "github.com/ava-labs/hypersdk/examples/morpheusvm/vm" + "github.com/ava-labs/hypersdk/tests/integration" + + lconsts "github.com/ava-labs/hypersdk/examples/morpheusvm/consts" + ginkgo "github.com/onsi/ginkgo/v2" +) + +func TestIntegration(t *testing.T) { + ginkgo.RunSpecs(t, "morpheusvm integration test suites") +} + +var _ = ginkgo.BeforeSuite(func() { + require := require.New(ginkgo.GinkgoT()) + + testingNetworkConfig, err := workload.NewTestNetworkConfig(0) + require.NoError(err) + + randomEd25519Priv, err := ed25519.GeneratePrivateKey() + require.NoError(err) + + randomEd25519AuthFactory := auth.NewED25519Factory(randomEd25519Priv) + + generator := workload.NewTxGenerator(testingNetworkConfig.Keys()[0]) + // Setup imports the integration test coverage + integration.Setup( + vm.New, + testingNetworkConfig, + lconsts.ID, + generator, + randomEd25519AuthFactory, + ) +}) +``` + +In `integration_test.go`, we are feeding our workload tests along with various +other values to the HyperSDK integration test library. Implementing an entire +integration test framework is time-intensive. By using the HyperSDK integration +test framework, we can defer most tasks to it and solely focus on defining the +workload tests. The setup mentioned above also implicitly sets up our procedural +tests as well. + + ## Testing Our VM -Putting everything together, its now time to test our work! To do this, run the +Putting everything together, it's now time to test our work! To do this, run the following command: ```bash @@ -516,13 +513,13 @@ following command: If all goes well, you should see the following message in your command line: ```bash -Ran 12 of 12 Specs in 1.083 seconds +Ran 12 of 12 Specs in 1.614 seconds SUCCESS! -- 12 Passed | 0 Failed | 0 Pending | 0 Skipped PASS -coverage: 61.8% of statements in github.com/ava-labs/hypersdk/... -composite coverage: 60.1% of statements +coverage: 61.9% of statements in github.com/ava-labs/hypersdk/... +composite coverage: 60.4% of statements -Ginkgo ran 1 suite in 9.091254458s +Ginkgo ran 1 suite in 10.274886041s Test Suite Passed ``` @@ -535,4 +532,8 @@ equivalent to MorpheusVM. In particular, you started by building a base version of MorpheusVM which introduced the concepts of actions and storage in the context of token transfers. In the `options` section, you then extended your VM by adding an option which allowed your VM to spin-up a JSON-RPC server. Finally, -in this section, we added workload tests to make sure our VM works as expected. +in this section, we added integration tests to make sure our VM works as expected. + +In the final two sections, we'll explore the HyperSDK-CLI which will allow us to +interact with our VM by reading from it and being able to send TXs in real time +from the command line! From b313dc021f5cc1531afab4f1a89eeb9c11e822ac Mon Sep 17 00:00:00 2001 From: Rodrigo Villar Date: Mon, 4 Nov 2024 16:37:42 -0500 Subject: [PATCH 10/15] fix mistakes found during walkthrough --- docs/tutorials/morpheusvm/1_morpheusvm.md | 17 ++- docs/tutorials/morpheusvm/2_options.md | 8 +- docs/tutorials/morpheusvm/3_testing.md | 24 ++--- docs/tutorials/morpheusvm/4_interlude.md | 122 ++++++++++++++++++++-- 4 files changed, 141 insertions(+), 30 deletions(-) diff --git a/docs/tutorials/morpheusvm/1_morpheusvm.md b/docs/tutorials/morpheusvm/1_morpheusvm.md index fec3c293f7..9b130ba3c6 100644 --- a/docs/tutorials/morpheusvm/1_morpheusvm.md +++ b/docs/tutorials/morpheusvm/1_morpheusvm.md @@ -187,7 +187,6 @@ func (t *Transfer) ComputeUnits(chain.Rules) uint64 { func (t *Transfer) ValidRange(chain.Rules) (start int64, end int64) { panic("unimplemented") } - ``` Now that we've defined our action, we'll move on to implementing the state layout for @@ -540,7 +539,8 @@ func (*BalanceHandler) Deduct( mu state.Mutable, amount uint64, ) error { - return SubBalance(ctx, mu, addr, amount) + _, err := SubBalance(ctx, mu, addr, amount) + return err } func (*BalanceHandler) AddBalance( @@ -578,7 +578,7 @@ permissions, since the HyperSDK may need to both read/write this balance when it handles fees: ```golang -func (*StateManager) SponsorStateKeys(addr codec.Address) state.Keys { +func (*BalanceHandler) SponsorStateKeys(addr codec.Address) state.Keys { return state.Keys{ string(BalanceKey(addr)): state.Read | state.Write, } @@ -785,6 +785,12 @@ import ( "github.com/ava-labs/hypersdk/chain" "github.com/ava-labs/hypersdk/codec" "github.com/ava-labs/hypersdk/examples/tutorial/actions" + "github.com/ava-labs/hypersdk/examples/tutorial/consts" + "github.com/ava-labs/hypersdk/examples/tutorial/storage" + "github.com/ava-labs/hypersdk/genesis" + "github.com/ava-labs/hypersdk/state/metadata" + "github.com/ava-labs/hypersdk/vm" + "github.com/ava-labs/hypersdk/vm/defaultvm" ) var ( @@ -801,7 +807,7 @@ func init() { errs := &wrappers.Errs{} errs.Add( - ActionParser.Register(&actions.Transfer{}, actions.UnmarshalTransfer), + ActionParser.Register(&actions.Transfer{}, nil), AuthParser.Register(&auth.ED25519{}, auth.UnmarshalED25519), AuthParser.Register(&auth.SECP256R1{}, auth.UnmarshalSECP256R1), @@ -828,7 +834,8 @@ func New(options ...vm.Option) (*vm.VM, error) { return defaultvm.New( consts.Version, genesis.DefaultGenesisFactory{}, - &storage.StateManager{}, + &storage.BalanceHandler{}, + metadata.NewDefaultManager(), ActionParser, AuthParser, OutputParser, diff --git a/docs/tutorials/morpheusvm/2_options.md b/docs/tutorials/morpheusvm/2_options.md index 4aaa05bb91..0fab372212 100644 --- a/docs/tutorials/morpheusvm/2_options.md +++ b/docs/tutorials/morpheusvm/2_options.md @@ -14,6 +14,8 @@ Before we build out our JSON-RPC function, we'll first need to add the following function to `storage/storage.go`: ```golang +type ReadState func(context.Context, [][]byte) ([][]byte, []error) + func GetBalanceFromState( ctx context.Context, f ReadState, @@ -260,15 +262,15 @@ func (p *Parser) Rules(_ int64) chain.Rules { return p.genesis.Rules } -func (*Parser) ActionCodec() chain.ActionCodec { +func (*Parser) ActionCodec() *codec.TypeParser[chain.Action] { return ActionParser } -func (*Parser) OutputCodec() chain.OutputCodec { +func (*Parser) OutputCodec() *codec.TypeParser[codec.Typed] { return OutputParser } -func (*Parser) AuthCodec() chain.AuthCodec { +func (*Parser) AuthCodec() *codec.TypeParser[chain.Auth] { return AuthParser } diff --git a/docs/tutorials/morpheusvm/3_testing.md b/docs/tutorials/morpheusvm/3_testing.md index f193690c26..0ea56a070f 100644 --- a/docs/tutorials/morpheusvm/3_testing.md +++ b/docs/tutorials/morpheusvm/3_testing.md @@ -107,9 +107,9 @@ import ( "github.com/ava-labs/hypersdk/chain" "github.com/ava-labs/hypersdk/codec" "github.com/ava-labs/hypersdk/crypto/ed25519" - "github.com/ava-labs/hypersdk/examples/morpheusvm/actions" - "github.com/ava-labs/hypersdk/examples/morpheusvm/consts" - "github.com/ava-labs/hypersdk/examples/morpheusvm/vm" + "github.com/ava-labs/hypersdk/examples/tutorial/actions" + "github.com/ava-labs/hypersdk/examples/tutorial/consts" + "github.com/ava-labs/hypersdk/examples/tutorial/vm" "github.com/ava-labs/hypersdk/tests/workload" ) @@ -223,8 +223,8 @@ import ( "github.com/ava-labs/hypersdk/auth" "github.com/ava-labs/hypersdk/codec" "github.com/ava-labs/hypersdk/crypto/ed25519" - "github.com/ava-labs/hypersdk/examples/morpheusvm/consts" - "github.com/ava-labs/hypersdk/examples/morpheusvm/vm" + "github.com/ava-labs/hypersdk/examples/tutorial/consts" + "github.com/ava-labs/hypersdk/examples/tutorial/vm" "github.com/ava-labs/hypersdk/fees" "github.com/ava-labs/hypersdk/genesis" "github.com/ava-labs/hypersdk/tests/workload" @@ -350,8 +350,8 @@ import ( "github.com/ava-labs/hypersdk/auth" "github.com/ava-labs/hypersdk/chain" "github.com/ava-labs/hypersdk/crypto/ed25519" - "github.com/ava-labs/hypersdk/examples/morpheusvm/actions" - "github.com/ava-labs/hypersdk/examples/morpheusvm/tests/workload" + "github.com/ava-labs/hypersdk/examples/tutorial/actions" + "github.com/ava-labs/hypersdk/examples/tutorial/tests/workload" "github.com/ava-labs/hypersdk/tests/registry" tworkload "github.com/ava-labs/hypersdk/tests/workload" @@ -454,20 +454,20 @@ import ( "github.com/stretchr/testify/require" - _ "github.com/ava-labs/hypersdk/examples/morpheusvm/tests" // include the tests that are shared between the integration and e2e + _ "github.com/ava-labs/hypersdk/examples/tutorial/tests" // include the tests that are shared between the integration and e2e "github.com/ava-labs/hypersdk/auth" "github.com/ava-labs/hypersdk/crypto/ed25519" - "github.com/ava-labs/hypersdk/examples/morpheusvm/tests/workload" - "github.com/ava-labs/hypersdk/examples/morpheusvm/vm" + "github.com/ava-labs/hypersdk/examples/tutorial/tests/workload" + "github.com/ava-labs/hypersdk/examples/tutorial/vm" "github.com/ava-labs/hypersdk/tests/integration" - lconsts "github.com/ava-labs/hypersdk/examples/morpheusvm/consts" + lconsts "github.com/ava-labs/hypersdk/examples/tutorial/consts" ginkgo "github.com/onsi/ginkgo/v2" ) func TestIntegration(t *testing.T) { - ginkgo.RunSpecs(t, "morpheusvm integration test suites") + ginkgo.RunSpecs(t, "tutorial integration test suites") } var _ = ginkgo.BeforeSuite(func() { diff --git a/docs/tutorials/morpheusvm/4_interlude.md b/docs/tutorials/morpheusvm/4_interlude.md index 009c20bf1e..67fd9fa29c 100644 --- a/docs/tutorials/morpheusvm/4_interlude.md +++ b/docs/tutorials/morpheusvm/4_interlude.md @@ -10,6 +10,7 @@ have some things set up before we can spin up `TutorialVM` on a local network. In this interlude, we will focus on the following: - Setting up our run/stop scripts +- Adding end-to-end tests - Setting up our VM binary generator - Installing our CLI @@ -21,13 +22,113 @@ The script that we'll be using will allow us to start and stop a local network running `TutorialVM`. To get started, in `examples/tutorial`, run the following commands: ```bash -mkdir scripts -copy ../morpheusvm/scripts/run.sh -copy ../morpheusvm/scripts/stop.sh +cp ../morpheusvm/scripts/run.sh ./scripts/run.sh +cp ../morpheusvm/scripts/stop.sh ./scripts/stop.sh + +chmod +x ./scripts/run.sh +chmod +x ./scripts/stop.sh ``` The commands above created a new folder named `scripts` and copied the run/stop -scripts from MorpheusVM into our scripts folder. +scripts from MorpheusVM into our scripts folder, along with giving them +execute permissions. + +Before moving for3ward, in lines 68-70, make sure to change it from this: + +```bash +go build \ +-o "${HYPERSDK_DIR}"/avalanchego-"${VERSION}"/plugins/qCNyZHrs3rZX458wPJXPJJypPf6w423A84jnfbdP2TPEmEE9u \ +./cmd/morpheusvm +``` + +to this: + +```bash +go build \ +-o "${HYPERSDK_DIR}"/avalanchego-"${VERSION}"/plugins/qCNyZHrs3rZX458wPJXPJJypPf6w423A84jnfbdP2TPEmEE9u \ +./cmd/tutorialvm +``` + +## Adding End-to-End Tests + +The scripts above, while extremely useful, work only if we define end-to-end +(e2e) tests for our VM. Since we don't focus on e2e tests in this tutorial, we +can just copy-and-paste a predefined implementation in. To do this, first run +the following: + +```bash +mkdir tests/e2e +touch tests/e2e/e2e_test.go +``` + +Then, in e2e_test.go, write the following: + +```go +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package e2e_test + +import ( + "testing" + "time" + + "github.com/ava-labs/avalanchego/tests/fixture/e2e" + "github.com/stretchr/testify/require" + + _ "github.com/ava-labs/hypersdk/examples/tutorial/tests" // include the tests that are shared between the integration and e2e + + "github.com/ava-labs/hypersdk/abi" + "github.com/ava-labs/hypersdk/auth" + "github.com/ava-labs/hypersdk/examples/tutorial/consts" + "github.com/ava-labs/hypersdk/examples/tutorial/tests/workload" + "github.com/ava-labs/hypersdk/examples/tutorial/vm" + "github.com/ava-labs/hypersdk/tests/fixture" + + he2e "github.com/ava-labs/hypersdk/tests/e2e" + ginkgo "github.com/onsi/ginkgo/v2" +) + +const owner = "tutorial-e2e-tests" + +var flagVars *e2e.FlagVars + +func TestE2e(t *testing.T) { + ginkgo.RunSpecs(t, "tutorial e2e test suites") +} + +func init() { + flagVars = e2e.RegisterFlags() +} + +// Construct tmpnet network with a single tutorial Subnet +var _ = ginkgo.SynchronizedBeforeSuite(func() []byte { + require := require.New(ginkgo.GinkgoT()) + + testingNetworkConfig, err := workload.NewTestNetworkConfig(100 * time.Millisecond) + require.NoError(err) + + expectedABI, err := abi.NewABI(vm.ActionParser.GetRegisteredTypes(), vm.OutputParser.GetRegisteredTypes()) + require.NoError(err) + + firstKey := testingNetworkConfig.Keys()[0] + generator := workload.NewTxGenerator(firstKey) + spamKey := &auth.PrivateKey{ + Address: auth.NewED25519Address(firstKey.PublicKey()), + Bytes: firstKey[:], + } + tc := e2e.NewTestContext() + he2e.SetWorkload(testingNetworkConfig, generator, expectedABI, nil, spamKey) + + return fixture.NewTestEnvironment(tc, flagVars, owner, testingNetworkConfig, consts.ID).Marshal() +}, func(envBytes []byte) { + // Run in every ginkgo process + + // Initialize the local test environment from the global state + e2e.InitSharedTestEnvironment(ginkgo.GinkgoT(), envBytes) +}) + +``` ## Adding a VM Binary Generator @@ -39,7 +140,8 @@ To start, let's run the following commands: ```bash mkdir -p cmd/tutorialvm -mkdir cmd/tutorial/vm/version +mkdir cmd/tutorialvm/version +touch cmd/tutorialvm/version/version.go touch cmd/tutorialvm/main.go ``` @@ -61,8 +163,8 @@ import ( "github.com/ava-labs/avalanchego/vms/rpcchainvm" "github.com/spf13/cobra" - "github.com/ava-labs/hypersdk/examples/tutorialvm/cmd/morpheusvm/version" - "github.com/ava-labs/hypersdk/examples/tutorialvm/vm" + "github.com/ava-labs/hypersdk/examples/tutorial/cmd/tutorialvm/version" + "github.com/ava-labs/hypersdk/examples/tutorial/vm" ) var rootCmd = &cobra.Command{ @@ -116,7 +218,7 @@ import ( "github.com/spf13/cobra" - "github.com/ava-labs/hypersdk/examples/tutorialvm/consts" + "github.com/ava-labs/hypersdk/examples/tutorial/consts" ) func init() { @@ -145,14 +247,14 @@ To start, we'll want to compile the HyperSDK-CLI. If you're in `examples/tutorial`, you can run the following: ```bash -go build -o ./hypersdk-cli ../../cmd/hypersdk-cli/ +go install github.com/ava-labs/hypersdk/cmd/hypersdk-cli@4510f51720d2e0fdecfd7fa08350e7c3eab3cf53 ``` To confirm that your build of the HyperSDK-CLI was successful, run the following command: ```bash -./hypersdk-cli +hypersdk-cli ``` You should see the following: From 4c31c734eafc9197ce10cd5822b21da4af0e03bf Mon Sep 17 00:00:00 2001 From: Rodrigo Villar Date: Mon, 4 Nov 2024 16:50:34 -0500 Subject: [PATCH 11/15] update cli hash --- docs/tutorials/morpheusvm/4_interlude.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/morpheusvm/4_interlude.md b/docs/tutorials/morpheusvm/4_interlude.md index 67fd9fa29c..8e0365c09e 100644 --- a/docs/tutorials/morpheusvm/4_interlude.md +++ b/docs/tutorials/morpheusvm/4_interlude.md @@ -247,7 +247,7 @@ To start, we'll want to compile the HyperSDK-CLI. If you're in `examples/tutorial`, you can run the following: ```bash -go install github.com/ava-labs/hypersdk/cmd/hypersdk-cli@4510f51720d2e0fdecfd7fa08350e7c3eab3cf53 +go install github.com/ava-labs/hypersdk/cmd/hypersdk-cli@b2ad4d38aec5b2958a02b209b58eafc6891c51cd ``` To confirm that your build of the HyperSDK-CLI was successful, run the following From f719bb4b29ad44901de5595fdf855a8e53bdc39c Mon Sep 17 00:00:00 2001 From: Rodrigo Villar Date: Tue, 5 Nov 2024 11:16:16 -0500 Subject: [PATCH 12/15] reduce diff --- docs/tutorials/morpheusvm/1_morpheusvm.md | 23 +++------- docs/tutorials/morpheusvm/3_testing.md | 53 ++++++++--------------- docs/tutorials/morpheusvm/4_interlude.md | 31 +++++-------- docs/tutorials/morpheusvm/5_cli.md | 12 ++--- 4 files changed, 39 insertions(+), 80 deletions(-) diff --git a/docs/tutorials/morpheusvm/1_morpheusvm.md b/docs/tutorials/morpheusvm/1_morpheusvm.md index 9b130ba3c6..74701afe82 100644 --- a/docs/tutorials/morpheusvm/1_morpheusvm.md +++ b/docs/tutorials/morpheusvm/1_morpheusvm.md @@ -226,22 +226,13 @@ const balancePrefix byte = metadata.DefaultMinimumPrefix In addition to defining a prefix for account balances, the HyperSDK actually requires us to define other prefixes as well (such as those for storing fees, the latest block height, etc.). However, we can pass in a default layout which -handles those other prefixes, leaving us with just having to define a balance -prefix. +handles those other prefixes. -As for `metadata.DefaultMinimumPrefix`, this prefix is the lowest prefix -available to us if we use the default layout. By using this prefix, we have the -following contract: - -- Using any user-defined prefix that is equal to or greater than `metadata.DefaultMinimumPrefix` - will not result in any state collisions with the HyperSDK -- Using any user-defined prefix that is less than `metadata.DefaultMinimumPrefix` will - most likely cause a state collision with the HyperSDK - -In layman's terms, we can think of prefixes less than -`metadata.DefaultMinimumPrefix` as prefixes that *only the HyperSDK should -touch*, and any prefixes greater than `metadata.DefaultMinimumPrefix` as -prefixes that the VM developer can use. +What prefix do we use though? Since we're using a default layout, we can use +`metadata.DefaultMinimumPrefix` (i.e. the lowest prefix available to us). By +using this prefix (or anything greater than it), we avoid any state colisions +with the HyperSDK. Using any prefix less than `metadata.DefaultMinimumPrefix` +may result in a state collision that could break your VM! ### Defining Account Balance Keys @@ -686,7 +677,7 @@ Here, we have `TransferResult` which has as fields the new balances we mentioned earlier along with a `GetTypeID()` method. This method requires us to return a unique identifier associated with this result. Since we already assigned a unique type ID to our `Transfer` action, we can use this ID for our result -(action IDs and result IDs are different). Therefore, we have: +(action IDs and result IDs are different). We have: ```go func (*TransferResult) GetTypeID() uint8 { diff --git a/docs/tutorials/morpheusvm/3_testing.md b/docs/tutorials/morpheusvm/3_testing.md index 0ea56a070f..168a328c1b 100644 --- a/docs/tutorials/morpheusvm/3_testing.md +++ b/docs/tutorials/morpheusvm/3_testing.md @@ -13,17 +13,11 @@ This section will consist of the following: - Implementing a bash script to run our workload tests - Implementing workload integration tests + - These tests generate a large quantity of generic transactions - Implementing procedural integration tests + - These tests test for a specific transaction - Registering our integration tests -At the end of this section, you'll have implemented integration tests that will -have tested your VM via the following: - -- Workload tests: this method tests your VM by applying a large quantity of - generic transactions and making sure that each is applied as expected -- Procedural tests: this method tests your VM by applying a specific transaction - and making sure that it is applied as expected - ## Workload Scripts We start by reusing the workload script from MorpheusVM. In `tutorial/`, create @@ -66,9 +60,7 @@ run \ go tool cover -html=integration.coverage.out -o=integration.coverage.html ``` -This script will both set up our testing environment and execute the workload -tests. To make sure that our script will run at the end of this section, run the -following command: +Let's make sure that our script can be executed: ```bash chmod +x ./scripts/tests.integration.sh @@ -129,8 +121,7 @@ func NewTxGenerator(key ed25519.PrivateKey) *TxGenerator { ``` Next, we'll want to implement a method to our `TxGenerator` that will allow it -to produce a valid transaction with `Transfer` on the fly. Therefore, we have -the following: +to produce a valid transaction with `Transfer` on the fly. We have: ```go @@ -170,8 +161,7 @@ func (g *TxGenerator) GenerateTx(ctx context.Context, uri string) (*chain.Transa In addition to generating a valid transaction, this method returns an anonymous function which calls `confirmTX`. `confirmTX` sends the generated TX to the VM, -makes sure that it was accepted, and checks that the result associated with the -TX is expected. With this in mind, we have: +makes sure that it was accepted, and checks that the TX outputs are as expected. ```golang func confirmTx(ctx context.Context, require *require.Assertions, uri string, txID ids.ID, receiverAddr codec.Address, receiverExpectedBalance uint64) { @@ -288,7 +278,7 @@ func newDefaultKeys() []ed25519.PrivateKey { } ``` -Finally, we implement the network configuration required for our VM integrationt +Finally, we implement the network configuration required for our VM integration tests: ```go @@ -318,22 +308,20 @@ func NewTestNetworkConfig(minBlockGap time.Duration) (*NetworkConfiguration, err } ``` -Having implemented our workload tests, we can now move onto our procedural -tests. +We now move onto our procedural tests. ## Implementing our Procedural Tests -While workload tests allow us to test our transactions en masse, procedural -tests allow us to run an integration test on -our VM in a unit-test-esque manner. To start, in the `tests` folder, run the +The benefit of using procedural tests is that we write an integration test in a +unit-test-esque manner. To start, in the `tests` folder, run the following command: ```bash touch transfer.go ``` -As the name suggests, we'll be writing a registry test to test the `Transfer` -action. Within `transfer.go`, we can start by pasting the following in: +We'll be writing a registry test to test the `Transfer` +action. Within `transfer.go`, we write the following: ```go // Copyright (C) 2024, Ava Labs, Inc. All rights reserved. @@ -368,7 +356,7 @@ var _ = registry.Register(TestsRegistry, "Transfer Transaction", func(t ginkgo.F }) ``` -In the code above, we have `TestsRegistry`: as the name suggest, this is a +In the code above, we have `TestsRegistry`: this is a registry of all the procedural tests that we want to run against our VM. Afterwards, we have the following snippet: @@ -378,8 +366,8 @@ registry.Register(TestsRegistry, "Transfer Transaction", func(t ginkgo.FullGinkg }) ``` -Here, we are adding a procedural test to `TestRegistry`. However, what we're -missing is the actual test logic itself. In short, here's what we want to do in +Here, we are adding a procedural test to `TestRegistry`. However, we're +missing the test itself. In short, here's what we want to do in our testing logic: - Setup necessary values @@ -420,7 +408,7 @@ This step will consist of the following: - If the test takes longer than 2 seconds, it will fail - Calling `ConfirmTxs` with our TX being passed in -The function `ConfirmTXs`, in particular, is useful as it checks that our TX was +The function `ConfirmTXs` is useful as it checks that our TX was sent and that, if finalized, our transaction has the expected outputs. We have the following: @@ -434,12 +422,7 @@ the following: ## Registering our Workload Tests Although we've defined the integration tests themselves, we still need to -register them with the HyperSDK. That is, we need to pass our integration tests -into the HyperSDK integration test framework, which will run these tests on our -behalf. - - implemented, we now define a way for which our workload -tests can be utilized. To start, create a new folder named `integration` in +register them with the HyperSDK. To start, create a new folder named `integration` in `tests/`. Inside `integration/`, create a new file `integration_test.go`. Here copy-paste the following: @@ -494,13 +477,11 @@ var _ = ginkgo.BeforeSuite(func() { ``` In `integration_test.go`, we are feeding our workload tests along with various -other values to the HyperSDK integration test library. Implementing an entire -integration test framework is time-intensive. By using the HyperSDK integration +other values to the HyperSDK integration test library. By using the HyperSDK integration test framework, we can defer most tasks to it and solely focus on defining the workload tests. The setup mentioned above also implicitly sets up our procedural tests as well. - ## Testing Our VM Putting everything together, it's now time to test our work! To do this, run the diff --git a/docs/tutorials/morpheusvm/4_interlude.md b/docs/tutorials/morpheusvm/4_interlude.md index 8e0365c09e..1c0742da59 100644 --- a/docs/tutorials/morpheusvm/4_interlude.md +++ b/docs/tutorials/morpheusvm/4_interlude.md @@ -1,13 +1,10 @@ # Interlude -In the previous sections, we built our own implementation of MorpheusVM and -tested it to see that it worked as expected. In the upcoming sections, we will -be completing our VM journey by spinning up `TutorialVM` on a local network and -interacting with it via the HyperSDK-CLI. +In the previous sections, we built and tested our own implementation of +MorpheusVM. In the upcoming sections, we'll be spinning up `TutorialVM` on a +local network and interacting with it via the HyperSDK-CLI. -However, before we get to the exciting part of this tutorial series, we need to -have some things set up before we can spin up `TutorialVM` on a local network. -In this interlude, we will focus on the following: +In this section, we'll: - Setting up our run/stop scripts - Adding end-to-end tests @@ -18,8 +15,7 @@ Let's get started! ## Script Setup -The script that we'll be using will allow us to start and stop a local network -running `TutorialVM`. To get started, in `examples/tutorial`, run the following commands: +To get started, in `examples/tutorial`, run the following commands: ```bash cp ../morpheusvm/scripts/run.sh ./scripts/run.sh @@ -33,7 +29,7 @@ The commands above created a new folder named `scripts` and copied the run/stop scripts from MorpheusVM into our scripts folder, along with giving them execute permissions. -Before moving for3ward, in lines 68-70, make sure to change it from this: +Before moving forward, in lines 68-70, make sure to change it from this: ```bash go build \ @@ -51,17 +47,15 @@ go build \ ## Adding End-to-End Tests -The scripts above, while extremely useful, work only if we define end-to-end -(e2e) tests for our VM. Since we don't focus on e2e tests in this tutorial, we -can just copy-and-paste a predefined implementation in. To do this, first run -the following: +A caveat of the scripts above is that we need to define end-to-end (e2e) tests +for our VM. To start, run the following: ```bash mkdir tests/e2e touch tests/e2e/e2e_test.go ``` -Then, in e2e_test.go, write the following: +Then, in `e2e_test.go`, write the following: ```go // Copyright (C) 2024, Ava Labs, Inc. All rights reserved. @@ -132,11 +126,8 @@ var _ = ginkgo.SynchronizedBeforeSuite(func() []byte { ## Adding a VM Binary Generator -Although the run/stop scripts we just created will take care of spinning up and -stopping our network, it still requires us to define a way for our VM binary to -be generated. - -To start, let's run the following commands: +We'll now write a simple CLI that allows us to generate the binary for +`TutorialVM`. To start, let's run the following commands: ```bash mkdir -p cmd/tutorialvm diff --git a/docs/tutorials/morpheusvm/5_cli.md b/docs/tutorials/morpheusvm/5_cli.md index 4fadfe69c3..e90c1e3448 100644 --- a/docs/tutorials/morpheusvm/5_cli.md +++ b/docs/tutorials/morpheusvm/5_cli.md @@ -1,9 +1,7 @@ # CLI -In the previous sections, we have gone over how to implement MorpheusVM from -scratch and how we can test our implementation. Now that we're confident that we -have a correct version of MorpheusVM, we can now utilize the HyperSDK-CLI tool -to actually interact with our VM! +In the previous section, we implemented our network scripts along with the +HyperSDK-CLI. Now, it's time to interact with `TutorialVM`! In this tutorial, we'll go over the following: @@ -12,8 +10,6 @@ In this tutorial, we'll go over the following: ## CLI Setup -In this section, we'll want to both start our network along with passing in the -necessary information that the HyperSDK-CLI needs to interact with our network. To start, run the following command from `./examples/tutorial`: ```bash @@ -47,7 +43,7 @@ AVL-0W5L7Y:tutorial rodrigo.villar$ ./hypersdk-cli key set --key=0x323b1d8f4eed5 Address: 0x00c4cb545f748a28770042f893784ce85b107389004d6a0e0d6d7518eeae1292d9 ``` -With this, we're now ready to interact with our implementation of MorpheusVM! +We're now ready to interact with our implementation of MorpheusVM! ## Interacting with MorpheusVM @@ -79,7 +75,7 @@ This should give us the following result: Since the account we are using is specified as a prefunded account in the genesis of our VM (via `DefaultGenesis`), our account balance is as expected. -Having read into the state of our VM, let's now try writing to our VM by sending +Let's now try writing to our VM by sending a transaction via the CLI. In particular, we want to send a transaction with the following action: From 8cca8e87dcc9fb3454962970357e0af0136b18d0 Mon Sep 17 00:00:00 2001 From: Rodrigo Villar Date: Tue, 5 Nov 2024 14:45:05 -0500 Subject: [PATCH 13/15] more reduce diff --- docs/tutorials/morpheusvm/3_testing.md | 19 +++++++------------ docs/tutorials/morpheusvm/4_interlude.md | 3 +-- docs/tutorials/morpheusvm/5_cli.md | 12 +++++------- 3 files changed, 13 insertions(+), 21 deletions(-) diff --git a/docs/tutorials/morpheusvm/3_testing.md b/docs/tutorials/morpheusvm/3_testing.md index 168a328c1b..85c7bf0399 100644 --- a/docs/tutorials/morpheusvm/3_testing.md +++ b/docs/tutorials/morpheusvm/3_testing.md @@ -75,7 +75,7 @@ files: - `generator.go` - `genesis.go` -In short, `generator.go` will be responsible for generating transactions that +`generator.go` will be responsible for generating transactions that contain the `Transfer` action while `genesis.go` will be responsible for providing the network configuration for our integration tests. @@ -123,7 +123,6 @@ func NewTxGenerator(key ed25519.PrivateKey) *TxGenerator { Next, we'll want to implement a method to our `TxGenerator` that will allow it to produce a valid transaction with `Transfer` on the fly. We have: - ```go func (g *TxGenerator) GenerateTx(ctx context.Context, uri string) (*chain.Transaction, workload.TxAssertion, error) { // TODO: no need to generate the clients every tx @@ -160,7 +159,7 @@ func (g *TxGenerator) GenerateTx(ctx context.Context, uri string) (*chain.Transa ``` In addition to generating a valid transaction, this method returns an anonymous -function which calls `confirmTX`. `confirmTX` sends the generated TX to the VM, +function containing `confirmTX`. `confirmTX` sends the generated TX to the VM, makes sure that it was accepted, and checks that the TX outputs are as expected. ```golang @@ -423,7 +422,7 @@ the following: Although we've defined the integration tests themselves, we still need to register them with the HyperSDK. To start, create a new folder named `integration` in -`tests/`. Inside `integration/`, create a new file `integration_test.go`. Here +`tests/`. Inside `integration/`, create a new file `integration_test.go`. Here, copy-paste the following: ```go @@ -477,8 +476,8 @@ var _ = ginkgo.BeforeSuite(func() { ``` In `integration_test.go`, we are feeding our workload tests along with various -other values to the HyperSDK integration test library. By using the HyperSDK integration -test framework, we can defer most tasks to it and solely focus on defining the +other values to the HyperSDK integration test library. Using this pattern allows +us to defer most tasks to it and solely focus on defining the workload tests. The setup mentioned above also implicitly sets up our procedural tests as well. @@ -504,16 +503,12 @@ Ginkgo ran 1 suite in 10.274886041s Test Suite Passed ``` -If you see this, then this means that your VM passed the workload tests! +If you see this, then your VM passed the integration tests! ## Conclusion Assuming the above went well, you've just built a VM which is functionally -equivalent to MorpheusVM. In particular, you started by building a base version -of MorpheusVM which introduced the concepts of actions and storage in the -context of token transfers. In the `options` section, you then extended your VM -by adding an option which allowed your VM to spin-up a JSON-RPC server. Finally, -in this section, we added integration tests to make sure our VM works as expected. +equivalent to MorpheusVM. Having built a base VM and extending it with options, we added integration tests to make sure our VM works as expected. In the final two sections, we'll explore the HyperSDK-CLI which will allow us to interact with our VM by reading from it and being able to send TXs in real time diff --git a/docs/tutorials/morpheusvm/4_interlude.md b/docs/tutorials/morpheusvm/4_interlude.md index 1c0742da59..22d4fceaaa 100644 --- a/docs/tutorials/morpheusvm/4_interlude.md +++ b/docs/tutorials/morpheusvm/4_interlude.md @@ -234,8 +234,7 @@ func versionFunc(*cobra.Command, []string) error { ## CLI Installation -To start, we'll want to compile the HyperSDK-CLI. If you're in -`examples/tutorial`, you can run the following: +To start, install the HyperSDK-CLI by running the following: ```bash go install github.com/ava-labs/hypersdk/cmd/hypersdk-cli@b2ad4d38aec5b2958a02b209b58eafc6891c51cd diff --git a/docs/tutorials/morpheusvm/5_cli.md b/docs/tutorials/morpheusvm/5_cli.md index e90c1e3448..0821b7465b 100644 --- a/docs/tutorials/morpheusvm/5_cli.md +++ b/docs/tutorials/morpheusvm/5_cli.md @@ -24,7 +24,7 @@ SUCCESS! -- 1 Passed | 0 Failed | 0 Pending | 7 Skipped PASS ``` -This means that our network is now running in the background. Focusing now on +This means that our network is now running in the background. Focusing on the HyperSDK-CLI, we now want to store the private key of our (test!) account and the RPC endpoint. We can do this by executing the following commands: @@ -33,7 +33,7 @@ and the RPC endpoint. We can do this by executing the following commands: ./hypersdk-cli key set --key=0x323b1d8f4eed5f0da9da93071b034f2dce9d2d22692c172f3cb252a64ddfafd01b057de320297c29ad0c1f589ea216869cf1938d88c9fbd70d6748323dbf2fa7 ``` -Your command line should look like the following: +Your command line should look as follows: ```bash AVL-0W5L7Y:tutorial rodrigo.villar$ ./hypersdk-cli endpoint set --endpoint=http://localhost:9650/ext/bc/morpheusvm/ @@ -47,8 +47,8 @@ We're now ready to interact with our implementation of MorpheusVM! ## Interacting with MorpheusVM -As a sanity test, let's first check that we can interact with our running VM - -to do this, run the following command: +As a sanity test, let's first check that we can interact with our running VM by +running the following: ```bash ./hypersdk-cli ping @@ -75,9 +75,7 @@ This should give us the following result: Since the account we are using is specified as a prefunded account in the genesis of our VM (via `DefaultGenesis`), our account balance is as expected. -Let's now try writing to our VM by sending -a transaction via the CLI. In particular, we want to send a transaction with the -following action: +Let's now write to our VM by sending a TX via the CLI with the following action: - Transfer - Recipient: the zero address From d8cf20e2d72790d3d52f577c2a4b8868f444aaa1 Mon Sep 17 00:00:00 2001 From: Rodrigo Villar Date: Tue, 5 Nov 2024 15:16:25 -0500 Subject: [PATCH 14/15] nit --- docs/tutorials/morpheusvm/3_testing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/morpheusvm/3_testing.md b/docs/tutorials/morpheusvm/3_testing.md index 85c7bf0399..3b89631b6c 100644 --- a/docs/tutorials/morpheusvm/3_testing.md +++ b/docs/tutorials/morpheusvm/3_testing.md @@ -311,7 +311,7 @@ We now move onto our procedural tests. ## Implementing our Procedural Tests -The benefit of using procedural tests is that we write an integration test in a +The benefit of using procedural tests is that we can write an integration tests in a unit-test-esque manner. To start, in the `tests` folder, run the following command: From cb2a01bf5e61c37159e5f554e77fefd80ebd23c2 Mon Sep 17 00:00:00 2001 From: Rodrigo Villar Date: Thu, 7 Nov 2024 09:24:10 -0500 Subject: [PATCH 15/15] address feedback --- docs/tutorials/morpheusvm/3_testing.md | 49 ++++++++++++-------------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/docs/tutorials/morpheusvm/3_testing.md b/docs/tutorials/morpheusvm/3_testing.md index 3b89631b6c..783c061af9 100644 --- a/docs/tutorials/morpheusvm/3_testing.md +++ b/docs/tutorials/morpheusvm/3_testing.md @@ -6,17 +6,15 @@ Let's quickly recap what we've done so far: - We've extended our implementation by adding a JSON-RPC server option With the above, our code should work exactly like the version of MorpheusVM -found in `examples/`. To verify this though, we're going to apply the same -integration tests used in MorpheusVM against our VM. +found in `examples/`. To verify this though, we're going to apply the same +workload tests used in MorpheusVM against our VM. This section will consist of the following: - Implementing a bash script to run our workload tests -- Implementing workload integration tests - - These tests generate a large quantity of generic transactions -- Implementing procedural integration tests - - These tests test for a specific transaction -- Registering our integration tests +- Implementing workload tests that generate a large quantity of generic transactions +- Implementing workload tests that test for a specific transaction +- Registering our workload tests ## Workload Scripts @@ -66,7 +64,7 @@ Let's make sure that our script can be executed: chmod +x ./scripts/tests.integration.sh ``` -## Implementing Our Workload Tests +## Testing via Transaction Generation Start by creating a subdirectory in `tutorial/` named `tests`. Within `tests/`, create a directory called `workload`. Within `workload`, create the following @@ -77,7 +75,7 @@ files: `generator.go` will be responsible for generating transactions that contain the `Transfer` action while `genesis.go` will be responsible for -providing the network configuration for our integration tests. +providing the network configuration for our tests. ### Implementing the Generator @@ -277,7 +275,7 @@ func newDefaultKeys() []ed25519.PrivateKey { } ``` -Finally, we implement the network configuration required for our VM integration +Finally, we implement the network configuration required for our VM tests: ```go @@ -307,13 +305,12 @@ func NewTestNetworkConfig(minBlockGap time.Duration) (*NetworkConfiguration, err } ``` -We now move onto our procedural tests. +We now move onto testing against a specific transaction. -## Implementing our Procedural Tests +## Testing via a Specific Transaction -The benefit of using procedural tests is that we can write an integration tests in a -unit-test-esque manner. To start, in the `tests` folder, run the -following command: +The benefit of this testing style is that it's similar to writing unit tests. +To start, in the `tests` folder, run the following command: ```bash touch transfer.go @@ -356,7 +353,7 @@ var _ = registry.Register(TestsRegistry, "Transfer Transaction", func(t ginkgo.F ``` In the code above, we have `TestsRegistry`: this is a -registry of all the procedural tests that we want to run against our VM. +registry of all the tests that we want to run against our VM. Afterwards, we have the following snippet: ```go @@ -365,7 +362,7 @@ registry.Register(TestsRegistry, "Transfer Transaction", func(t ginkgo.FullGinkg }) ``` -Here, we are adding a procedural test to `TestRegistry`. However, we're +Here, we are adding a test to `TestRegistry`. However, we're missing the test itself. In short, here's what we want to do in our testing logic: @@ -412,15 +409,15 @@ sent and that, if finalized, our transaction has the expected outputs. We have the following: ```go - timeoutCtx, timeoutCtxFnc := context.WithDeadline(context.Background(), time.Now().Add(2*time.Second)) + timeoutCtx, timeoutCtxFnc := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second)) defer timeoutCtxFnc() require.NoError(tn.ConfirmTxs(timeoutCtx, []*chain.Transaction{tx})) ``` -## Registering our Workload Tests +## Registering our Tests -Although we've defined the integration tests themselves, we still need to +Although we've defined the tests themselves, we still need to register them with the HyperSDK. To start, create a new folder named `integration` in `tests/`. Inside `integration/`, create a new file `integration_test.go`. Here, copy-paste the following: @@ -475,11 +472,9 @@ var _ = ginkgo.BeforeSuite(func() { }) ``` -In `integration_test.go`, we are feeding our workload tests along with various -other values to the HyperSDK integration test library. Using this pattern allows -us to defer most tasks to it and solely focus on defining the -workload tests. The setup mentioned above also implicitly sets up our procedural -tests as well. +In `integration_test.go`, we are feeding our tests along with various +other values to the HyperSDK test library. Using this pattern allows +us to defer most tasks to it and solely focus on defining the tests. ## Testing Our VM @@ -503,12 +498,12 @@ Ginkgo ran 1 suite in 10.274886041s Test Suite Passed ``` -If you see this, then your VM passed the integration tests! +If you see this, then your VM passed the tests! ## Conclusion Assuming the above went well, you've just built a VM which is functionally -equivalent to MorpheusVM. Having built a base VM and extending it with options, we added integration tests to make sure our VM works as expected. +equivalent to MorpheusVM. Having built a base VM and extending it with options, we added tests to make sure our VM works as expected. In the final two sections, we'll explore the HyperSDK-CLI which will allow us to interact with our VM by reading from it and being able to send TXs in real time