diff --git a/examples/morpheusvm/tests/e2e/e2e_test.go b/examples/morpheusvm/tests/e2e/e2e_test.go index 31ce1b9f9d..a962aa2161 100644 --- a/examples/morpheusvm/tests/e2e/e2e_test.go +++ b/examples/morpheusvm/tests/e2e/e2e_test.go @@ -4,13 +4,14 @@ package e2e_test import ( - "encoding/json" "testing" "time" "github.com/ava-labs/avalanchego/tests/fixture/e2e" "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/abi" "github.com/ava-labs/hypersdk/auth" "github.com/ava-labs/hypersdk/examples/morpheusvm/consts" @@ -39,14 +40,10 @@ func init() { var _ = ginkgo.SynchronizedBeforeSuite(func() []byte { require := require.New(ginkgo.GinkgoT()) - keys := workload.NewDefaultKeys() - genesis := workload.NewGenesis(keys, 100*time.Millisecond) - genesisBytes, err := json.Marshal(genesis) - require.NoError(err) - expectedABI, err := abi.NewABI(vm.ActionParser.GetRegisteredTypes(), vm.OutputParser.GetRegisteredTypes()) + testingNetworkConfig, err := workload.NewTestNetworkConfig(100 * time.Millisecond) require.NoError(err) - parser, err := vm.CreateParser(genesisBytes) + expectedABI, err := abi.NewABI(vm.ActionParser.GetRegisteredTypes(), vm.OutputParser.GetRegisteredTypes()) require.NoError(err) // Import HyperSDK e2e test coverage and inject MorpheusVM name @@ -55,15 +52,16 @@ var _ = ginkgo.SynchronizedBeforeSuite(func() []byte { KeyType: auth.ED25519Key, } - generator := workload.NewTxGenerator(keys[0]) + firstKey := testingNetworkConfig.Keys()[0] + generator := workload.NewTxGenerator(firstKey) spamKey := &auth.PrivateKey{ - Address: auth.NewED25519Address(keys[0].PublicKey()), - Bytes: keys[0][:], + Address: auth.NewED25519Address(firstKey.PublicKey()), + Bytes: firstKey[:], } tc := e2e.NewTestContext() - he2e.SetWorkload(consts.Name, generator, expectedABI, parser, &spamHelper, spamKey) + he2e.SetWorkload(testingNetworkConfig, generator, expectedABI, &spamHelper, spamKey) - return fixture.NewTestEnvironment(tc, flagVars, owner, consts.Name, consts.ID, genesisBytes).Marshal() + return fixture.NewTestEnvironment(tc, flagVars, owner, testingNetworkConfig, consts.ID).Marshal() }, func(envBytes []byte) { // Run in every ginkgo process diff --git a/examples/morpheusvm/tests/integration/integration_test.go b/examples/morpheusvm/tests/integration/integration_test.go index 62c751ef8f..65e44ac0d5 100644 --- a/examples/morpheusvm/tests/integration/integration_test.go +++ b/examples/morpheusvm/tests/integration/integration_test.go @@ -4,11 +4,12 @@ package integration_test import ( - "encoding/json" "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" @@ -26,9 +27,7 @@ func TestIntegration(t *testing.T) { var _ = ginkgo.BeforeSuite(func() { require := require.New(ginkgo.GinkgoT()) - keys := workload.NewDefaultKeys() - genesis := workload.NewGenesis(keys, 0) - genesisBytes, err := json.Marshal(genesis) + testingNetworkConfig, err := workload.NewTestNetworkConfig(0) require.NoError(err) randomEd25519Priv, err := ed25519.GeneratePrivateKey() @@ -36,13 +35,12 @@ var _ = ginkgo.BeforeSuite(func() { randomEd25519AuthFactory := auth.NewED25519Factory(randomEd25519Priv) - generator := workload.NewTxGenerator(keys[0]) + generator := workload.NewTxGenerator(testingNetworkConfig.Keys()[0]) // Setup imports the integration test coverage integration.Setup( vm.New, - genesisBytes, + testingNetworkConfig, lconsts.ID, - vm.CreateParser, generator, randomEd25519AuthFactory, ) diff --git a/examples/morpheusvm/tests/transfer.go b/examples/morpheusvm/tests/transfer.go new file mode 100644 index 0000000000..ebf8fa5d73 --- /dev/null +++ b/examples/morpheusvm/tests/transfer.go @@ -0,0 +1,49 @@ +// 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) { + 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] + + tx, err := tn.GenerateTx(context.Background(), []chain.Action{&actions.Transfer{ + To: toAddress, + Value: 1, + }}, + auth.NewED25519Factory(spendingKey), + ) + require.NoError(err) + + timeoutCtx, timeoutCtxFnc := context.WithDeadline(context.Background(), time.Now().Add(2*time.Second)) + defer timeoutCtxFnc() + + require.NoError(tn.ConfirmTxs(timeoutCtx, []*chain.Transaction{tx})) +}) diff --git a/examples/morpheusvm/tests/workload/genesis.go b/examples/morpheusvm/tests/workload/genesis.go index f3e874abd0..8d3f8fd49e 100644 --- a/examples/morpheusvm/tests/workload/genesis.go +++ b/examples/morpheusvm/tests/workload/genesis.go @@ -4,14 +4,20 @@ 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/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" ) const ( @@ -19,13 +25,15 @@ const ( 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 { +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 { @@ -45,10 +53,13 @@ func NewGenesis(keys []ed25519.PrivateKey, minBlockGap time.Duration) *genesis.D 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 } -func NewDefaultKeys() []ed25519.PrivateKey { +func newDefaultKeys() []ed25519.PrivateKey { testKeys := make([]ed25519.PrivateKey, len(ed25519HexKeys)) for i, keyHex := range ed25519HexKeys { bytes, err := codec.LoadHex(keyHex, ed25519.PrivateKeyLen) @@ -60,3 +71,28 @@ func NewDefaultKeys() []ed25519.PrivateKey { return testKeys } + +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 +} diff --git a/tests/e2e/e2e.go b/tests/e2e/e2e.go index 43fe02b915..7a96c5b8e5 100644 --- a/tests/e2e/e2e.go +++ b/tests/e2e/e2e.go @@ -19,7 +19,7 @@ import ( "github.com/ava-labs/hypersdk/api/jsonrpc" "github.com/ava-labs/hypersdk/api/state" "github.com/ava-labs/hypersdk/auth" - "github.com/ava-labs/hypersdk/chain" + "github.com/ava-labs/hypersdk/tests/registry" "github.com/ava-labs/hypersdk/tests/workload" "github.com/ava-labs/hypersdk/throughput" "github.com/ava-labs/hypersdk/utils" @@ -28,20 +28,18 @@ import ( ) var ( - vmName string - txWorkload workload.TxWorkload - parser chain.Parser - expectedABI abi.ABI - spamKey *auth.PrivateKey - spamHelper throughput.SpamHelper + networkConfig workload.TestNetworkConfiguration + txWorkload workload.TxWorkload + expectedABI abi.ABI + spamKey *auth.PrivateKey + spamHelper throughput.SpamHelper ) -func SetWorkload(name string, generator workload.TxGenerator, abi abi.ABI, chainParser chain.Parser, sh throughput.SpamHelper, key *auth.PrivateKey) { - vmName = name +func SetWorkload(networkConfigImpl workload.TestNetworkConfiguration, generator workload.TxGenerator, abi abi.ABI, sh throughput.SpamHelper, key *auth.PrivateKey) { + networkConfig = networkConfigImpl txWorkload = workload.TxWorkload{ Generator: generator, } - parser = chainParser expectedABI = abi spamHelper = sh spamKey = key @@ -52,23 +50,23 @@ var _ = ginkgo.Describe("[HyperSDK APIs]", func() { require := require.New(tc) ginkgo.It("Ping", func() { - expectedBlockchainID := e2e.GetEnv(tc).GetNetwork().GetSubnet(vmName).Chains[0].ChainID + expectedBlockchainID := e2e.GetEnv(tc).GetNetwork().GetSubnet(networkConfig.Name()).Chains[0].ChainID workload.Ping(tc.DefaultContext(), require, getE2EURIs(tc, expectedBlockchainID)) }) ginkgo.It("StableNetworkIdentity", func() { hardcodedHostPort := "http://localhost:9650" - fixedNodeURL := hardcodedHostPort + "/ext/bc/" + vmName + fixedNodeURL := hardcodedHostPort + "/ext/bc/" + networkConfig.Name() c := jsonrpc.NewJSONRPCClient(fixedNodeURL) _, _, chainIDFromRPC, err := c.Network(tc.DefaultContext()) require.NoError(err) - expectedBlockchainID := e2e.GetEnv(tc).GetNetwork().GetSubnet(vmName).Chains[0].ChainID + expectedBlockchainID := e2e.GetEnv(tc).GetNetwork().GetSubnet(networkConfig.Name()).Chains[0].ChainID require.Equal(expectedBlockchainID, chainIDFromRPC) }) ginkgo.It("GetNetwork", func() { - expectedBlockchainID := e2e.GetEnv(tc).GetNetwork().GetSubnet(vmName).Chains[0].ChainID + expectedBlockchainID := e2e.GetEnv(tc).GetNetwork().GetSubnet(networkConfig.Name()).Chains[0].ChainID baseURIs := getE2EBaseURIs(tc) baseURI := baseURIs[0] client := info.NewClient(baseURI) @@ -78,12 +76,12 @@ var _ = ginkgo.Describe("[HyperSDK APIs]", func() { }) ginkgo.It("GetABI", func() { - expectedBlockchainID := e2e.GetEnv(tc).GetNetwork().GetSubnet(vmName).Chains[0].ChainID + expectedBlockchainID := e2e.GetEnv(tc).GetNetwork().GetSubnet(networkConfig.Name()).Chains[0].ChainID workload.GetABI(tc.DefaultContext(), require, getE2EURIs(tc, expectedBlockchainID), expectedABI) }) ginkgo.It("ReadState", func() { - blockchainID := e2e.GetEnv(tc).GetNetwork().GetSubnet(vmName).Chains[0].ChainID + blockchainID := e2e.GetEnv(tc).GetNetwork().GetSubnet(networkConfig.Name()).Chains[0].ChainID ctx := tc.DefaultContext() for _, uri := range getE2EURIs(tc, blockchainID) { client := state.NewJSONRPCStateClient(uri) @@ -97,23 +95,23 @@ var _ = ginkgo.Describe("[HyperSDK APIs]", func() { }) }) -var _ = ginkgo.Describe("[HyperSDK Tx Workloads]", func() { +var _ = ginkgo.Describe("[HyperSDK Tx Workloads]", ginkgo.Serial, func() { ginkgo.It("Basic Tx Workload", func() { tc := e2e.NewTestContext() require := require.New(tc) - blockchainID := e2e.GetEnv(tc).GetNetwork().GetSubnet(vmName).Chains[0].ChainID + blockchainID := e2e.GetEnv(tc).GetNetwork().GetSubnet(networkConfig.Name()).Chains[0].ChainID ginkgo.By("Tx workloads", func() { txWorkload.GenerateBlocks(tc.DefaultContext(), require, getE2EURIs(tc, blockchainID), 1) }) ginkgo.By("Confirm accepted blocks indexed", func() { - workload.GetBlocks(tc.DefaultContext(), require, parser, getE2EURIs(tc, blockchainID)) + workload.GetBlocks(tc.DefaultContext(), require, networkConfig.Parser(), getE2EURIs(tc, blockchainID)) }) }) }) -var _ = ginkgo.Describe("[HyperSDK Spam Workloads]", func() { +var _ = ginkgo.Describe("[HyperSDK Spam Workloads]", ginkgo.Serial, func() { ginkgo.It("Spam Workload", func() { if spamKey == nil || spamHelper == nil { return @@ -121,7 +119,7 @@ var _ = ginkgo.Describe("[HyperSDK Spam Workloads]", func() { tc := e2e.NewTestContext() require := require.New(tc) - blockchainID := e2e.GetEnv(tc).GetNetwork().GetSubnet(vmName).Chains[0].ChainID + blockchainID := e2e.GetEnv(tc).GetNetwork().GetSubnet(networkConfig.Name()).Chains[0].ChainID uris := getE2EURIs(tc, blockchainID) key := spamKey @@ -137,11 +135,11 @@ var _ = ginkgo.Describe("[HyperSDK Spam Workloads]", func() { }) }) -var _ = ginkgo.Describe("[HyperSDK Syncing]", func() { +var _ = ginkgo.Describe("[HyperSDK Syncing]", ginkgo.Serial, func() { ginkgo.It("[Sync]", func() { tc := e2e.NewTestContext() require := require.New(tc) - blockchainID := e2e.GetEnv(tc).GetNetwork().GetSubnet(vmName).Chains[0].ChainID + blockchainID := e2e.GetEnv(tc).GetNetwork().GetSubnet(networkConfig.Name()).Chains[0].ChainID uris := getE2EURIs(tc, blockchainID) ginkgo.By("Generate 128 blocks", func() { @@ -244,6 +242,19 @@ var _ = ginkgo.Describe("[HyperSDK Syncing]", func() { }) }) +var _ = ginkgo.Describe("[Custom VM Tests]", ginkgo.Serial, func() { + tc := e2e.NewTestContext() + + for testRegistry := range registry.GetTestsRegistries() { + for _, test := range testRegistry.List() { + ginkgo.It(test.Name, func() { + testNetwork := NewNetwork(tc) + test.Fnc(ginkgo.GinkgoT(), testNetwork) + }) + } + } +}) + func getE2EURIs(tc tests.TestContext, blockchainID ids.ID) []string { nodeURIs := e2e.GetEnv(tc).GetNetwork().GetNodeURIs() uris := make([]string, 0, len(nodeURIs)) diff --git a/tests/e2e/network.go b/tests/e2e/network.go new file mode 100644 index 0000000000..4e79f553cb --- /dev/null +++ b/tests/e2e/network.go @@ -0,0 +1,129 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package e2e + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/tests/fixture/e2e" + + "github.com/ava-labs/hypersdk/api/indexer" + "github.com/ava-labs/hypersdk/api/jsonrpc" + "github.com/ava-labs/hypersdk/chain" + "github.com/ava-labs/hypersdk/tests/workload" +) + +var ( + ErrUnableToConfirmTx = errors.New("unable to confirm transaction") + ErrInvalidURI = errors.New("invalid uri") +) + +const ( + txCheckInterval = 100 * time.Millisecond +) + +type Network struct { + uris []string + // The parser here is the original parser provided by the vm, with the chain ID populated by + // the newly created network. On e2e networks, we can't tell in advance what the ChainID would be, + // and therefore need to update it from the network. + parser *parser +} + +func NewNetwork(tc *e2e.GinkgoTestContext) *Network { + blockchainID := e2e.GetEnv(tc).GetNetwork().GetSubnet(networkConfig.Name()).Chains[0].ChainID + testNetwork := &Network{ + uris: getE2EURIs(tc, blockchainID), + parser: &parser{ + Parser: networkConfig.Parser(), + rules: &rules{ + Rules: networkConfig.Parser().Rules(0), + chainID: blockchainID, + }, + }, + } + return testNetwork +} + +func (n *Network) URIs() []string { + return n.uris +} + +func (n *Network) ConfirmTxs(ctx context.Context, txs []*chain.Transaction) error { + c := jsonrpc.NewJSONRPCClient(n.uris[0]) + txIDs := []ids.ID{} + for _, tx := range txs { + txID, err := c.SubmitTx(ctx, tx.Bytes()) + if err != nil { + return fmt.Errorf("unable to submit transaction : %w", err) + } + txIDs = append(txIDs, txID) + } + + indexerCli := indexer.NewClient(n.uris[0]) + for _, txID := range txIDs { + success, _, err := indexerCli.WaitForTransaction(ctx, txCheckInterval, txID) + if err != nil { + return fmt.Errorf("error while waiting for transaction : %w", err) + } + if !success { + return ErrUnableToConfirmTx + } + } + + _, targetHeight, _, err := c.Accepted(ctx) + if err != nil { + return err + } + for _, uri := range n.uris[1:] { + if err := jsonrpc.Wait(ctx, txCheckInterval, func(ctx context.Context) (bool, error) { + c := jsonrpc.NewJSONRPCClient(uri) + _, nodeHeight, _, err := c.Accepted(ctx) + if err != nil { + return false, err + } + return nodeHeight >= targetHeight, nil + }); err != nil { + return err + } + } + return nil +} + +func (n *Network) GenerateTx(ctx context.Context, actions []chain.Action, auth chain.AuthFactory) (*chain.Transaction, error) { + c := jsonrpc.NewJSONRPCClient(n.uris[0]) + _, tx, _, err := c.GenerateTransaction( + ctx, + n.parser, + actions, + auth, + ) + return tx, err +} + +func (*Network) Configuration() workload.TestNetworkConfiguration { + return networkConfig +} + +type rules struct { + chain.Rules + chainID ids.ID +} + +func (r *rules) GetChainID() ids.ID { + return r.chainID +} + +type parser struct { + chain.Parser + rules *rules +} + +func (p *parser) Rules(int64) chain.Rules { + return p.rules +} diff --git a/tests/fixture/environment.go b/tests/fixture/environment.go index c8db18eb11..5ae56de093 100644 --- a/tests/fixture/environment.go +++ b/tests/fixture/environment.go @@ -13,6 +13,8 @@ import ( "github.com/ava-labs/avalanchego/tests/fixture/e2e" "github.com/ava-labs/avalanchego/tests/fixture/tmpnet" "github.com/stretchr/testify/require" + + "github.com/ava-labs/hypersdk/tests/workload" ) var StableNodeURI = fmt.Sprintf("http://localhost:%d", config.DefaultHTTPPort) @@ -21,18 +23,17 @@ func NewTestEnvironment( testContext tests.TestContext, flagVars *e2e.FlagVars, owner string, - vmName string, + networkConfig workload.TestNetworkConfiguration, vmID ids.ID, - genesisBytes []byte, ) *e2e.TestEnvironment { // Run only once in the first ginkgo process nodes := tmpnet.NewNodesOrPanic(flagVars.NodeCount()) nodes[0].Flags[config.HTTPPortKey] = config.DefaultHTTPPort subnet := NewHyperVMSubnet( - vmName, + networkConfig.Name(), vmID, - genesisBytes, + networkConfig.GenesisBytes(), nodes..., ) network := NewTmpnetNetwork(owner, nodes, subnet) @@ -43,8 +44,8 @@ func NewTestEnvironment( network, ) - chainID := testEnv.GetNetwork().GetSubnet(vmName).Chains[0].ChainID - setupDefaultChainAlias(testContext, chainID, vmName) + chainID := testEnv.GetNetwork().GetSubnet(networkConfig.Name()).Chains[0].ChainID + setupDefaultChainAlias(testContext, chainID, networkConfig.Name()) return testEnv } diff --git a/tests/integration/integration.go b/tests/integration/integration.go index a8e4d34376..c152a66a26 100644 --- a/tests/integration/integration.go +++ b/tests/integration/integration.go @@ -6,6 +6,7 @@ package integration import ( "context" "encoding/json" + "fmt" "net/http" "net/http/httptest" "os" @@ -35,6 +36,7 @@ import ( "github.com/ava-labs/hypersdk/extension/externalsubscriber" "github.com/ava-labs/hypersdk/fees" "github.com/ava-labs/hypersdk/pubsub" + "github.com/ava-labs/hypersdk/tests/registry" "github.com/ava-labs/hypersdk/tests/workload" "github.com/ava-labs/hypersdk/vm" @@ -54,19 +56,18 @@ var ( log logging.Logger // when used with embedded VMs - instances []instance + instances []*instance sendAppGossipCounter int uris []string blocks []snowman.Block + testNetwork *Network networkID uint32 // Injected values populated by Setup - createVM func(...vm.Option) (*vm.VM, error) - genesisBytes []byte - vmID ids.ID - createParserFromBytes func(genesisBytes []byte) (chain.Parser, error) - parser chain.Parser + createVM func(...vm.Option) (*vm.VM, error) + networkConfig workload.TestNetworkConfiguration + vmID ids.ID txWorkload workload.TxWorkload authFactory chain.AuthFactory @@ -99,26 +100,19 @@ func init() { func Setup( newVM func(...vm.Option) (*vm.VM, error), - genesis []byte, + networkConfigImpl workload.TestNetworkConfiguration, id ids.ID, - createParser func(genesisBytes []byte) (chain.Parser, error), generator workload.TxGenerator, authF chain.AuthFactory, ) { - require := require.New(ginkgo.GinkgoT()) createVM = newVM - genesisBytes = genesis + networkConfig = networkConfigImpl vmID = id - createParserFromBytes = createParser txWorkload = workload.TxWorkload{ Generator: generator, } authFactory = authF - createdParser, err := createParserFromBytes(genesisBytes) - require.NoError(err) - parser = createdParser - setInstances() } @@ -128,7 +122,11 @@ func setInstances() { log.Info("VMID", zap.Stringer("id", vmID)) // create embedded VMs - instances = make([]instance, numVMs) + instances = make([]*instance, numVMs) + + createParserFromBytes := func(_ []byte) (chain.Parser, error) { + return networkConfig.Parser(), nil + } externalSubscriberAcceptedBlocksCh = make(chan ids.ID, 1) externalSubscriber0 := externalsubscriber.NewExternalSubscriberServer(log, createParserFromBytes, []event.Subscription[*chain.ExecutedBlock]{ @@ -172,9 +170,9 @@ func setInstances() { configs := make([][]byte, numVMs) configs[0] = externalSubscriberConfigBytes - networkID = uint32(1) + networkID = networkConfig.Parser().Rules(0).GetNetworkID() subnetID := ids.GenerateTestID() - chainID := ids.GenerateTestID() + chainID := networkConfig.Parser().Rules(0).GetChainID() app := &enginetest.Sender{ SendAppGossipF: func(ctx context.Context, _ common.SendConfig, appGossipBytes []byte) error { @@ -219,7 +217,7 @@ func setInstances() { context.TODO(), snowCtx, db, - genesisBytes, + networkConfig.GenesisBytes(), nil, configs[i], toEngine, @@ -238,7 +236,7 @@ func setInstances() { routerServer := httptest.NewServer(router) jsonRPCServer := httptest.NewServer(hd[jsonrpc.Endpoint]) webSocketServer := httptest.NewServer(hd[ws.Endpoint]) - instances[i] = instance{ + instances[i] = &instance{ chainID: snowCtx.ChainID, nodeID: snowCtx.NodeID, vm: v, @@ -267,6 +265,7 @@ func setInstances() { for i, inst := range instances { uris[i] = inst.routerServer.URL } + testNetwork = &Network{uris: uris} blocks = []snowman.Block{} @@ -487,7 +486,7 @@ var _ = ginkgo.Describe("[Tx Processing]", ginkgo.Serial, func() { }) ginkgo.By("Confirm accepted blocks indexed", func() { - workload.GetBlocks(ctx, require, parser, []string{uris[1]}) + workload.GetBlocks(ctx, require, networkConfig.Parser(), []string{uris[1]}) }) }) @@ -541,9 +540,11 @@ var _ = ginkgo.Describe("[Tx Processing]", ginkgo.Serial, func() { }) ginkgo.It("processes valid index transactions (w/block listening)", func() { - // Clear previous txs on instance 0 - accept := expectBlk(instances[0]) - accept(false) // don't care about results + // synchronize the nodes on the network. + // this would clear previous txs on instance 0 + // and ignore the transaction results while + // syncronizing the nodes on the network. + require.NoError(testNetwork.SynchronizeNetwork(context.Background())) // Subscribe to blocks cli, err := ws.NewWebSocketClient(instances[0].WebSocketServer.URL, ws.DefaultHandshakeTimeout, pubsub.MaxPendingMessages, pubsub.MaxReadMessageSize) @@ -559,7 +560,7 @@ var _ = ginkgo.Describe("[Tx Processing]", ginkgo.Serial, func() { _, err = instances[0].cli.SubmitTx(ctx, tx.Bytes()) require.NoError(err) - accept = expectBlk(instances[0]) + accept := expectBlk(instances[0]) results := accept(false) require.Len(results, 1) require.True(results[0].Success) @@ -569,7 +570,7 @@ var _ = ginkgo.Describe("[Tx Processing]", ginkgo.Serial, func() { txAssertion(cctx, require, uris[0]) // Read item from connection - blk, lresults, prices, err := cli.ListenBlock(context.TODO(), parser) + blk, lresults, prices, err := cli.ListenBlock(context.TODO(), networkConfig.Parser()) require.NoError(err) require.Len(blk.Txs, 1) require.Equal(lresults, results) @@ -620,9 +621,18 @@ var _ = ginkgo.Describe("[Tx Processing]", ginkgo.Serial, func() { // Close connection when done require.NoError(cli.Close()) }) + + for testRegistry := range registry.GetTestsRegistries() { + for _, test := range testRegistry.List() { + ginkgo.It(fmt.Sprintf("Custom VM Test '%s'", test.Name), func() { + require.NoError(testNetwork.SynchronizeNetwork(context.Background())) + test.Fnc(ginkgo.GinkgoT(), testNetwork) + }) + } + } }) -func expectBlk(i instance) func(add bool) []*chain.Result { +func expectBlk(i *instance) func(add bool) []*chain.Result { require := require.New(ginkgo.GinkgoT()) ctx := context.TODO() diff --git a/tests/integration/network.go b/tests/integration/network.go new file mode 100644 index 0000000000..e16db47bfa --- /dev/null +++ b/tests/integration/network.go @@ -0,0 +1,184 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package integration + +import ( + "context" + "errors" + "fmt" + + "github.com/ava-labs/avalanchego/ids" + + "github.com/ava-labs/hypersdk/api/jsonrpc" + "github.com/ava-labs/hypersdk/chain" + "github.com/ava-labs/hypersdk/tests/workload" +) + +var ( + ErrUnableToConfirmTx = errors.New("unable to confirm transaction") + ErrInvalidURI = errors.New("invalid uri") + ErrTxNotFound = errors.New("tx not found") +) + +type Network struct { + uris []string +} + +func (*Network) ConfirmTxs(ctx context.Context, txs []*chain.Transaction) error { + err := instances[0].confirmTxs(ctx, txs) + if err != nil { + return err + } + lastAcceptedBlock := instances[0].vm.LastAcceptedBlock() + for i := 1; i < len(instances); i++ { + err = instances[i].applyBlk(ctx, lastAcceptedBlock) + if err != nil { + return err + } + } + return nil +} + +func (*Network) GenerateTx(ctx context.Context, actions []chain.Action, auth chain.AuthFactory) (*chain.Transaction, error) { + return instances[0].GenerateTx(ctx, actions, auth) +} + +// SynchronizeNetwork ensures that all the nodes on the network are at the same block height. +// this method should be called at the beginning of each test to ensure good starting point. +func (*Network) SynchronizeNetwork(ctx context.Context) error { + // find the latest block height across the network + var biggestHeight uint64 + var biggestHeightInstanceIndex int + for i, instance := range instances { + lastHeight, err := instance.vm.GetLastAcceptedHeight() + if err != nil { + return err + } + if lastHeight >= biggestHeight { + biggestHeightInstanceIndex = i + biggestHeight = lastHeight + } + } + for i := 0; i < len(instances); i++ { + if i == biggestHeightInstanceIndex { + continue + } + instance := instances[i] + for { + height, err := instance.vm.GetLastAcceptedHeight() + if err != nil { + return err + } + if height == biggestHeight { + break + } + statefulBlock, err := instances[biggestHeightInstanceIndex].vm.GetDiskBlock(ctx, height+1) + if err != nil { + return err + } + if err := instance.applyBlk(ctx, statefulBlock); err != nil { + return err + } + } + } + return nil +} + +func (i *instance) applyBlk(ctx context.Context, lastAcceptedBlock *chain.StatefulBlock) error { + err := i.vm.SetPreference(ctx, lastAcceptedBlock.ID()) + if err != nil { + return fmt.Errorf("applyBlk failed to set preference : %w", err) + } + blk, err := i.vm.ParseBlock(ctx, lastAcceptedBlock.Bytes()) + if err != nil { + return fmt.Errorf("applyBlk failed to parse block : %w", err) + } + err = blk.Verify(ctx) + if err != nil { + return fmt.Errorf("applyBlk failed to verify block : %w", err) + } + err = blk.Accept(ctx) + if err != nil { + return fmt.Errorf("applyBlk failed to accept block : %w", err) + } + if i.onAccept != nil { + i.onAccept(blk) + } + return nil +} + +func (i *instance) confirmTxs(ctx context.Context, txs []*chain.Transaction) error { + errs := i.vm.Submit(ctx, true, txs) + if len(errs) != 0 && errs[0] != nil { + return errs[0] + } + + expectBlk(i)(false) + + for _, tx := range txs { + err := i.confirmTx(ctx, tx.ID()) + if err != nil { + return err + } + } + + return nil +} + +func (i *instance) URI() string { + return i.routerServer.URL +} + +func (i *instance) GenerateTx(ctx context.Context, actions []chain.Action, auth chain.AuthFactory) (*chain.Transaction, error) { + // TODO: support generating tx without using jsonRPC client + c := jsonrpc.NewJSONRPCClient(i.URI()) + _, tx, _, err := c.GenerateTransaction( + ctx, + networkConfig.Parser(), + actions, + auth, + ) + return tx, err +} + +func (i *instance) confirmTx(ctx context.Context, txid ids.ID) error { + lastAcceptedHeight, err := i.vm.GetLastAcceptedHeight() + if err != nil { + return err + } + lastAcceptedBlockID, err := i.vm.GetBlockHeightID(lastAcceptedHeight) + if err != nil { + return err + } + blk, err := i.vm.GetBlock(ctx, lastAcceptedBlockID) + if err != nil { + return err + } + for { + stflBlk, ok := blk.(*chain.StatefulBlock) + if !ok { + return ErrTxNotFound + } + for _, tx := range stflBlk.StatelessBlock.Txs { + if tx.ID() == txid { + // found. + return nil + } + } + // keep iterating backward. + lastAcceptedBlockID = blk.Parent() + blk, err = i.vm.GetBlock(ctx, lastAcceptedBlockID) + if err != nil { + return ErrTxNotFound + } + } +} + +func (n *Network) URIs() []string { + return n.uris +} + +func (*Network) Configuration() workload.TestNetworkConfiguration { + return networkConfig +} diff --git a/tests/registry/registry.go b/tests/registry/registry.go new file mode 100644 index 0000000000..55c6e9cae2 --- /dev/null +++ b/tests/registry/registry.go @@ -0,0 +1,46 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package registry + +import ( + "github.com/onsi/ginkgo/v2" + + "github.com/ava-labs/hypersdk/tests/workload" +) + +type TestFunc func(t ginkgo.FullGinkgoTInterface, tn workload.TestNetwork) + +type namedTest struct { + Fnc TestFunc + Name string +} +type Registry struct { + tests []namedTest +} + +func (r *Registry) Add(name string, f TestFunc) { + r.tests = append(r.tests, namedTest{Fnc: f, Name: name}) +} + +func (r *Registry) List() []namedTest { + if r == nil { + return []namedTest{} + } + return r.tests +} + +// we need to pre-register all the test registries that are created externally in order to comply with the ginko execution order. +// i.e. the global `var _ = ginkgo.Describe` used in the integration/e2e tests need to have this field populated before the iteration +// over the top level nodes. +var testRegistries = map[*Registry]bool{} + +func Register(registry *Registry, name string, f TestFunc) bool { + registry.Add(name, f) + testRegistries[registry] = true + return true +} + +func GetTestsRegistries() map[*Registry]bool { + return testRegistries +} diff --git a/tests/workload/network.go b/tests/workload/network.go new file mode 100644 index 0000000000..b62de6bf18 --- /dev/null +++ b/tests/workload/network.go @@ -0,0 +1,55 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package workload + +import ( + "context" + + "github.com/ava-labs/hypersdk/chain" +) + +type TestNetwork interface { + ConfirmTxs(context.Context, []*chain.Transaction) error + GenerateTx(context.Context, []chain.Action, chain.AuthFactory) (*chain.Transaction, error) + URIs() []string + Configuration() TestNetworkConfiguration +} + +// TestNetworkConfiguration is an interface, implemented by VM-specific tests +// to store information regarding the test network prior to it's invocation, and +// retrieve it during execution. All implementations must be thread-safe. +type TestNetworkConfiguration interface { + GenesisBytes() []byte + Name() string + Parser() chain.Parser +} + +// DefaultTestNetworkConfiguration struct is the common test configuration that a test framework would need to provide +// in order to deploy a network. A test would typically embed this as part of it's network configuration structure. +type DefaultTestNetworkConfiguration struct { + genesisBytes []byte + name string + parser chain.Parser +} + +func (d DefaultTestNetworkConfiguration) GenesisBytes() []byte { + return d.genesisBytes +} + +func (d DefaultTestNetworkConfiguration) Name() string { + return d.name +} + +func (d DefaultTestNetworkConfiguration) Parser() chain.Parser { + return d.parser +} + +// NewDefaultTestNetworkConfiguration creates a new DefaultTestNetworkConfiguration object. +func NewDefaultTestNetworkConfiguration(genesisBytes []byte, name string, parser chain.Parser) DefaultTestNetworkConfiguration { + return DefaultTestNetworkConfiguration{ + genesisBytes: genesisBytes, + name: name, + parser: parser, + } +} diff --git a/x/contracts/vm/tests/workload/workload.go b/x/contracts/vm/tests/workload/workload.go index 6f7ee8fce9..d5d8a9f8e1 100644 --- a/x/contracts/vm/tests/workload/workload.go +++ b/x/contracts/vm/tests/workload/workload.go @@ -85,6 +85,13 @@ func New(minBlockGap int64) (*genesis.DefaultGenesis, workload.TxWorkloadFactory }, nil } +func (*workloadFactory) GetSpendingKey() (*auth.PrivateKey, error) { + return &auth.PrivateKey{ + Address: ed25519Addrs[0], + Bytes: ed25519PrivKeys[0][:], + }, nil +} + func (f *workloadFactory) NewSizedTxWorkload(uri string, size int) (workload.TxWorkloadIterator, error) { cli := jsonrpc.NewJSONRPCClient(uri) lcli := vm.NewJSONRPCClient(uri)