diff --git a/docs/tutorials/morpheusvm/morpheusvm.md b/docs/tutorials/morpheusvm/1_morpheusvm.md similarity index 82% rename from docs/tutorials/morpheusvm/morpheusvm.md rename to docs/tutorials/morpheusvm/1_morpheusvm.md index 1f9aae0f53..74701afe82 100644 --- a/docs/tutorials/morpheusvm/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 @@ -72,7 +72,7 @@ import ( ) const ( - Name = "tutorialvm" + Name = "morpheusvm" ) var ID ids.ID @@ -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 @@ -198,65 +197,46 @@ 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 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. -func TimestampKey() (k []byte) { - return timestampKey -} +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! -func FeeKey() (k []byte) { - return feeKey -} -``` +### Defining Account Balance Keys -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 +441,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 +460,43 @@ import ( "github.com/ava-labs/hypersdk/state" ) -var _ chain.StateManager = (*StateManager)(nil) - -type StateManager struct{} +var _ chain.BalanceHandler = (*BalanceHandler)(nil) -func (s *StateManager) FeeKey() []byte { - panic("unimplemented") -} +type BalanceHandler struct{} -func (s *StateManager) HeightKey() []byte { +func (*BalanceHandler) SponsorStateKeys(addr codec.Address) state.Keys { 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") } ``` @@ -516,28 +504,11 @@ func (s *StateManager) SponsorStateKeys(addr codec.Address) state.Keys { 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 -func (*StateManager) CanDeduct( +func (*BalanceHandler) CanDeduct( ctx context.Context, addr codec.Address, im state.Immutable, @@ -553,16 +524,17 @@ func (*StateManager) CanDeduct( return nil } -func (*StateManager) Deduct( +func (*BalanceHandler) Deduct( ctx context.Context, addr codec.Address, mu state.Mutable, amount uint64, ) error { - return SubBalance(ctx, mu, addr, amount) + _, err := SubBalance(ctx, mu, addr, amount) + return err } -func (*StateManager) AddBalance( +func (*BalanceHandler) AddBalance( ctx context.Context, addr codec.Address, mu state.Mutable, @@ -572,6 +544,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`. @@ -593,7 +569,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, } @@ -674,6 +650,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). 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: @@ -765,6 +776,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 ( @@ -781,7 +798,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), @@ -808,7 +825,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/options.md b/docs/tutorials/morpheusvm/2_options.md similarity index 97% rename from docs/tutorials/morpheusvm/options.md rename to docs/tutorials/morpheusvm/2_options.md index 7a38196f33..0fab372212 100644 --- a/docs/tutorials/morpheusvm/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,22 +262,18 @@ 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 } -func (*Parser) StateManager() chain.StateManager { - return &storage.StateManager{} -} - func NewParser(genesis *genesis.DefaultGenesis) chain.Parser { return &Parser{genesis: genesis} } diff --git a/docs/tutorials/morpheusvm/3_testing.md b/docs/tutorials/morpheusvm/3_testing.md new file mode 100644 index 0000000000..783c061af9 --- /dev/null +++ b/docs/tutorials/morpheusvm/3_testing.md @@ -0,0 +1,510 @@ +# Testing + +Let's quickly recap what we've done so far: + +- We've built a base implementation of MorpheusVM +- 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 +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 tests that generate a large quantity of generic transactions +- Implementing workload tests that test for a specific transaction +- Registering our workload tests + +## Workload Scripts + +We start by reusing the workload script from MorpheusVM. In `tutorial/`, create +a new directory named `scripts`. Within this scripts directory, create a file +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 morpheusvm root" + exit 255 +fi + +# shellcheck source=/scripts/common/utils.sh +source ../../scripts/common/utils.sh +# shellcheck source=/scripts/constants.sh +source ../../scripts/constants.sh + +rm_previous_cov_reports +prepare_ginkgo + +# run with 3 embedded VMs +ACK_GINKGO_RC=true ginkgo \ +run \ +-v \ +--fail-fast \ +-cover \ +-covermode=atomic \ +-coverpkg=github.com/ava-labs/hypersdk/... \ +-coverprofile=integration.coverage.out \ +./tests/integration \ +--vms 3 + +# output generate coverage html +go tool cover -html=integration.coverage.out -o=integration.coverage.html +``` + +Let's make sure that our script can be executed: + +```bash +chmod +x ./scripts/tests.integration.sh +``` + +## 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 +files: + +- `generator.go` +- `genesis.go` + +`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 tests. + +### Implementing the Generator + +In `generator.go`, we start by implementing the following: + +```go +package workload + +import ( + "context" + "time" + + "github.com/ava-labs/avalanchego/ids" + "github.com/stretchr/testify/require" + + "github.com/ava-labs/hypersdk/api/indexer" + "github.com/ava-labs/hypersdk/api/jsonrpc" + "github.com/ava-labs/hypersdk/auth" + "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/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" +) + +var _ workload.TxGenerator = (*TxGenerator)(nil) + +const txCheckInterval = 100 * time.Millisecond + +type TxGenerator struct { + factory *auth.ED25519Factory +} + +func NewTxGenerator(key ed25519.PrivateKey) *TxGenerator { + return &TxGenerator{ + factory: auth.NewED25519Factory(key), + } +} +``` + +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 + cli := jsonrpc.NewJSONRPCClient(uri) + lcli := vm.NewJSONRPCClient(uri) + + to, err := ed25519.GeneratePrivateKey() + if err != nil { + return nil, nil, err + } + + toAddress := auth.NewED25519Address(to.PublicKey()) + parser, err := lcli.Parser(ctx) + if err != nil { + return nil, nil, err + } + _, tx, _, err := cli.GenerateTransaction( + ctx, + parser, + []chain.Action{&actions.Transfer{ + To: toAddress, + Value: 1, + }}, + g.factory, + ) + if err != nil { + return nil, nil, err + } + + return tx, func(ctx context.Context, require *require.Assertions, uri string) { + confirmTx(ctx, require, uri, tx.ID(), toAddress, 1) + }, nil +} +``` + +In addition to generating a valid transaction, this method returns an anonymous +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 +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) +} +``` + +With our generator complete, we can now move onto implementing the network +configuration. + +### Implementing the Network Configuration + +In `genesis.go`, we first start by implementing a function which returns the +genesis of our VM: + +```go +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package workload + +import ( + "encoding/json" + "math" + "time" + + "github.com/ava-labs/avalanchego/ids" + + "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/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" +) + +const ( + // default initial balance for each address + InitialBalance uint64 = 10_000_000_000_000 +) + +var _ workload.TestNetworkConfiguration = &NetworkConfiguration{} + +// hardcoded initial set of ed25519 keys. Each will be initialized with InitialBalance +var ed25519HexKeys = []string{ + "323b1d8f4eed5f0da9da93071b034f2dce9d2d22692c172f3cb252a64ddfafd01b057de320297c29ad0c1f589ea216869cf1938d88c9fbd70d6748323dbf2fa7", //nolint:lll + "8a7be2e0c9a2d09ac2861c34326d6fe5a461d920ba9c2b345ae28e603d517df148735063f8d5d8ba79ea4668358943e5c80bc09e9b2b9a15b5b15db6c1862e88", //nolint:lll +} + +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, + }) + } + + 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.Milliseconds() + + genesis.Rules.NetworkID = uint32(1) + genesis.Rules.ChainID = ids.GenerateTestID() + + return genesis +} +``` + +Next, using the values in `ed25519HexKeys`, we'll implement a function that +returns our private test keys: + +```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) + } + + return testKeys +} +``` + +Finally, we implement the network configuration required for our VM +tests: + +```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 +} +``` + +We now move onto testing against a specific transaction. + +## Testing via a Specific Transaction + +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 +``` + +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. +// 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/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" + 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`: this is a +registry of all the 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 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 +- 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` 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(30*time.Second)) + defer timeoutCtxFnc() + + require.NoError(tn.ConfirmTxs(timeoutCtx, []*chain.Transaction{tx})) +``` + +## Registering our Tests + +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: + +```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/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/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/tutorial/consts" + ginkgo "github.com/onsi/ginkgo/v2" +) + +func TestIntegration(t *testing.T) { + ginkgo.RunSpecs(t, "tutorial 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 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 + +Putting everything together, it's now time to test our work! To do this, run the +following command: + +```bash +./scripts/tests.integration.sh +``` + +If all goes well, you should see the following message in your command line: + +```bash +Ran 12 of 12 Specs in 1.614 seconds +SUCCESS! -- 12 Passed | 0 Failed | 0 Pending | 0 Skipped +PASS +coverage: 61.9% of statements in github.com/ava-labs/hypersdk/... +composite coverage: 60.4% of statements + +Ginkgo ran 1 suite in 10.274886041s +Test Suite Passed +``` + +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 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! diff --git a/docs/tutorials/morpheusvm/4_interlude.md b/docs/tutorials/morpheusvm/4_interlude.md new file mode 100644 index 0000000000..22d4fceaaa --- /dev/null +++ b/docs/tutorials/morpheusvm/4_interlude.md @@ -0,0 +1,279 @@ +# Interlude + +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. + +In this section, we'll: + +- Setting up our run/stop scripts +- Adding end-to-end tests +- Setting up our VM binary generator +- Installing our CLI + +Let's get started! + +## Script Setup + +To get started, in `examples/tutorial`, run the following commands: + +```bash +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, along with giving them +execute permissions. + +Before moving forward, 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 + +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: + +```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 + +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 +mkdir cmd/tutorialvm/version +touch cmd/tutorialvm/version/version.go +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/tutorial/cmd/tutorialvm/version" + "github.com/ava-labs/hypersdk/examples/tutorial/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/tutorial/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, install the HyperSDK-CLI by running the following: + +```bash +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 +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/5_cli.md b/docs/tutorials/morpheusvm/5_cli.md new file mode 100644 index 0000000000..0821b7465b --- /dev/null +++ b/docs/tutorials/morpheusvm/5_cli.md @@ -0,0 +1,144 @@ +# CLI + +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: + +- Setting up the CLI +- Interacting with MorpheusVM via the CLI + +## CLI Setup + +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 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 as follows: + +```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 +``` + +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 by +running the following: + +```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. +Let's now write to our VM by sending a TX via the CLI 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. diff --git a/docs/tutorials/morpheusvm/testing.md b/docs/tutorials/morpheusvm/testing.md deleted file mode 100644 index edbb65ea9d..0000000000 --- a/docs/tutorials/morpheusvm/testing.md +++ /dev/null @@ -1,426 +0,0 @@ -# Testing - -Let's quickly recap what we've done so far: - -- We've built a base implementation of MorpheusVM -- 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 -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 the workload tests itself -- Implementing a way to feed our workload tests to the HyperSDK workload test - framework - -## Workload Scripts - -We start by reusing the workload script from MorpheusVM. In `tutorial/`, create -a new directory named `scripts`. Within this scripts directory, create a file -called `tests.integration.sh` and paste the following: - -```bash -#!/usr/bin/env bash - -set -e - -if ! [[ "$0" =~ scripts/tests.integration.sh ]]; then - echo "must be run from tutorial root" - exit 255 -fi - -# shellcheck source=/scripts/common/utils.sh -source ../../scripts/common/utils.sh -# shellcheck source=/scripts/constants.sh -source ../../scripts/constants.sh - -rm_previous_cov_reports -prepare_ginkgo - -# run with 3 embedded VMs -ACK_GINKGO_RC=true ginkgo \ -run \ --v \ ---fail-fast \ --cover \ --covermode=atomic \ --coverpkg=github.com/ava-labs/hypersdk/... \ --coverprofile=integration.coverage.out \ -./tests/integration \ ---vms 3 - -# output generate coverage html -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: - -```bash -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. - -## Workload Initialization - -We start by defining the values necesssary for the workload tests along with an -`init()` function: - -```golang -package workload - -import ( - "context" - "math" - "time" - - "github.com/ava-labs/avalanchego/ids" - "github.com/stretchr/testify/require" - - "github.com/ava-labs/hypersdk/api/indexer" - "github.com/ava-labs/hypersdk/api/jsonrpc" - "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/tests/workload" -) - -const ( - initialBalance uint64 = 10_000_000_000_000 - txCheckInterval = 100 * time.Millisecond -) - -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)) -) - -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 -} - -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, - }) - } - - 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 - - return genesis, &workloadFactory{ - factories: ed25519AuthFactories, - addrs: ed25519Addrs, - }, nil -} - -func (f *workloadFactory) NewSizedTxWorkload(uri string, size int) (workload.TxWorkloadIterator, error) { - 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() - if err != nil { - return nil, nil, err - } - - aother := auth.NewED25519Address(other.PublicKey()) - 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: aother, - Value: 1, - }}, - g.factory, - ) - if err != nil { - return nil, nil, err - } - - 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) - }, 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) - - cli := jsonrpc.NewJSONRPCClient(uri) - networkID, _, blockchainID, err := cli.Network(context.Background()) - if err != nil { - return nil, err - } - lcli := vm.NewJSONRPCClient(uri) - - 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, - } - - return []workload.TxWorkloadIterator{generator}, nil -} - -type addressAndFactory struct { - address codec.Address - authFactory chain.AuthFactory -} - -type mixedAuthWorkload struct { - addressAndFactories []addressAndFactory - balance uint64 - cli *jsonrpc.JSONRPCClient - lcli *vm.JSONRPCClient - networkID uint32 - chainID ids.ID - count int -} - -func (g *mixedAuthWorkload) Next() bool { - return g.count < len(g.addressAndFactories)-1 -} - -func (g *mixedAuthWorkload) GenerateTxWithAssertion(ctx context.Context) (*chain.Transaction, workload.TxAssertion, error) { - defer func() { g.count++ }() - - sender := g.addressAndFactories[g.count] - receiver := g.addressAndFactories[g.count+1] - expectedBalance := g.balance - 1_000_000 - - 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 - - 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 -} -``` - -## Using Our Workload Tests - -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: - -```golang -package integration_test - -import ( - "encoding/json" - "testing" - - "github.com/stretchr/testify/require" - - "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" - - 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" -) - -func TestIntegration(t *testing.T) { - ginkgo.RunSpecs(t, "tutorial integration test suites") -} - -var _ = ginkgo.BeforeSuite(func() { - require := require.New(ginkgo.GinkgoT()) - genesis, workloadFactory, err := tutorialWorkload.New(0 /* minBlockGap: 0ms */) - require.NoError(err) - - genesisBytes, err := json.Marshal(genesis) - require.NoError(err) - - randomEd25519Priv, err := ed25519.GeneratePrivateKey() - require.NoError(err) - - randomEd25519AuthFactory := auth.NewED25519Factory(randomEd25519Priv) - - // Setup imports the integration test coverage - integration.Setup( - vm.New, - genesisBytes, - lconsts.ID, - vm.CreateParser, - workloadFactory, - 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. - -## Testing Our VM - -Putting everything together, its now time to test our work! To do this, run the -following command: - -```bash -./scripts/tests.integration.sh -``` - -If all goes well, you should see the following message in your command line: - -```bash -Ran 12 of 12 Specs in 1.083 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 - -Ginkgo ran 1 suite in 9.091254458s -Test Suite Passed -``` - -If you see this, then this means that your VM passed the workload 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 workload tests to make sure our VM works as expected.