diff --git a/testing/e2e/README.md b/testing/e2e/README.md index 8387f0fd5..cc382def8 100644 --- a/testing/e2e/README.md +++ b/testing/e2e/README.md @@ -27,15 +27,31 @@ This will automatically build your beacond docker image from the local source code, and spin up a Kurtosis network based on the config file in `testing/e2e/config/defaults.go`. +Currently, the e2e tests runs in different kurtosis enclaves. Check the default configuration in `TestBeaconKitE2ESuite()`. +Play around with the configuration to see how it works. You need to pass the chain ID and chain spec name to the `suite.WithChain()` function. Ensure that the chain ID and chain spec name are valid. + + ## Configuration In case you want to configure(change) the validator set, consider doing changes in `defaultValidators`. The user can specify the number of replicas they want per type. All the default configuration are listed in `testing/e2e/config/defaults.go` -Note: Currently the chainID for this local network is 80087, which is our dev network configuration (this is fixed in the kurtosis env setup and will be made configurable in a future version). To make changes to the 80087 chain spec used, modify parameters [here](https://github.com/berachain/beacon-kit/blob/main/config/spec/devnet.go#L40). +Note: The default chainID for this local network is 80087, which is our dev network configuration. To make changes to the 80087 chain spec used, modify parameters [here](https://github.com/berachain/beacon-kit/blob/main/config/spec/devnet.go#L40). + +## Configure the default network configuration +To change the chainID, modify the `ChainID` field in the `NetworkConfiguration` struct in `defaultNetworkConfiguration` +function in `testing/e2e/config/defaults.go`. + +To change the chainSpec, modify the `ChainSpec` field in the `NetworkConfiguration` struct in `defaultNetworkConfiguration` +function in `testing/e2e/config/defaults.go`. ## Add your tests -Add your tests in here like how it is done in `TestBasicStartup()` +To add your tests, you need to do the following: +1. Create a new file in the `testing/e2e/` directory. +2. Add your tests in here like how it is done in `runBasicStartup()` +3. Register your tests in `TestRunE2E()`. +4. Then specify the chainID and chainSpec in `SetupSuiteWithOptions()` in `e2e_test.go`. + diff --git a/testing/e2e/config/config.go b/testing/e2e/config/config.go index 678443415..1319d873a 100644 --- a/testing/e2e/config/config.go +++ b/testing/e2e/config/config.go @@ -40,6 +40,10 @@ type E2ETestConfig struct { } type NetworkConfiguration struct { + // ChainID specifies the chain ID for the network. + ChainID int `json:"chain_id"` + // ChainSpec specifies the chain spec for the network. + ChainSpec string `json:"chain_spec"` // Validators lists the configurations for each validator in the test. Validators NodeSet `json:"validators"` // FullNodes specifies the number of full nodes to include in the test. diff --git a/testing/e2e/config/defaults.go b/testing/e2e/config/defaults.go index c112695a9..ea82662b7 100644 --- a/testing/e2e/config/defaults.go +++ b/testing/e2e/config/defaults.go @@ -36,6 +36,16 @@ const ( ClientValidator4 = "cl-validator-beaconkit-4" ) +// default network configuration. +// +//nolint:gochecknoglobals // it's a default value +var ( + // defaultChainID is the default chain ID for the network. + defaultChainID = 80087 + // defaultChainSpec is the default chain spec for the network. + defaultChainSpec = "devnet" +) + // DefaultE2ETestConfig provides a default configuration for end-to-end tests, // pre-populating with a standard set of validators and no additional // services. @@ -50,6 +60,8 @@ func DefaultE2ETestConfig() *E2ETestConfig { func defaultNetworkConfiguration() NetworkConfiguration { return NetworkConfiguration{ + ChainID: defaultChainID, + ChainSpec: defaultChainSpec, Validators: defaultValidators(), FullNodes: defaultFullNodes(), SeedNodes: defaultSeedNodes(), diff --git a/testing/e2e/e2e_api_test.go b/testing/e2e/e2e_api_test.go index babfda459..009b8da98 100644 --- a/testing/e2e/e2e_api_test.go +++ b/testing/e2e/e2e_api_test.go @@ -28,13 +28,18 @@ import ( // TestBeaconAPISuite tests that the api test suite is setup correctly with a // working beacon node-api client. -func (s *BeaconKitE2ESuite) TestBeaconAPIStartup() { +func (s *BeaconKitE2ESuite) runBeaconAPIStartup() { + s.Logger().Info("Running BeaconAPIStartup") + // Get the current network instance + network := s.GetCurrentNetwork() + s.Require().NotNil(network, "Network instance is nil") + // Wait for execution block 5. - err := s.WaitForFinalizedBlockNumber(5) + err := s.WaitForFinalizedBlockNumber(network, 5) s.Require().NoError(err) // Get the consensus client. - client := s.ConsensusClients()[config.ClientValidator0] + client := network.ConsensusClients()[config.ClientValidator0] s.Require().NotNil(client) // Ensure the state root is not nil. diff --git a/testing/e2e/e2e_blob_test.go b/testing/e2e/e2e_blob_test.go index 8efe16a0e..c1a686b28 100644 --- a/testing/e2e/e2e_blob_test.go +++ b/testing/e2e/e2e_blob_test.go @@ -48,26 +48,35 @@ const ( // Test4844Live tests sending a large number of blob carrying txs over the // network. -func (s *BeaconKitE2ESuite) Test4844Live() { +func (s *BeaconKitE2ESuite) run4844Live() { + s.Logger().Info("Running 4844 Live") ctx, cancel := context.WithTimeout(s.Ctx(), suite.DefaultE2ETestTimeout) defer cancel() + // Get the current network + network := s.GetCurrentNetwork() + s.Require().NotNil(network, "Network instance is nil") + // Connect the consensus client node-api - client0 := s.ConsensusClients()[config.ClientValidator0] + client0 := network.ConsensusClients()[config.ClientValidator0] s.Require().NotNil(client0) s.Require().NoError(client0.Connect(ctx)) // Grab values to plug into txs - sender := s.TestAccounts()[0] - chainID, err := s.JSONRPCBalancer().ChainID(ctx) + accounts := s.GetAccounts() + s.Require().NotNil(accounts, "Test accounts are nil") + s.Require().NotEmpty(accounts, "No test accounts available") + + sender := accounts[0] + chainID, err := network.JSONRPCBalancer().ChainID(ctx) s.Require().NoError(err) - tip, err := s.JSONRPCBalancer().SuggestGasTipCap(ctx) + tip, err := network.JSONRPCBalancer().SuggestGasTipCap(ctx) s.Require().NoError(err) - gasFee, err := s.JSONRPCBalancer().SuggestGasPrice(ctx) + gasFee, err := network.JSONRPCBalancer().SuggestGasPrice(ctx) s.Require().NoError(err) - blkNum, err := s.JSONRPCBalancer().BlockNumber(s.Ctx()) + blkNum, err := network.JSONRPCBalancer().BlockNumber(s.Ctx()) s.Require().NoError(err) - nonce, err := s.JSONRPCBalancer().NonceAt( + nonce, err := network.JSONRPCBalancer().NonceAt( s.Ctx(), sender.Address(), new(big.Int).SetUint64(blkNum), ) s.Require().NoError(err) @@ -99,7 +108,7 @@ func (s *BeaconKitE2ESuite) Test4844Live() { s.Logger().Info("submitting blob transaction", "blobTx", blobTx.Hash().Hex()) blobTxs = append(blobTxs, blobTx) - err = s.JSONRPCBalancer().SendTransaction(ctx, blobTx) + err = network.JSONRPCBalancer().SendTransaction(ctx, blobTx) // TODO: Figure out what is causing this to happen. // Also, `errors.Is(err, txpool.ErrAlreadyKnown)` doesn't catch it. if err != nil && err.Error() == txpool.ErrAlreadyKnown.Error() { @@ -119,7 +128,7 @@ func (s *BeaconKitE2ESuite) Test4844Live() { // Wait for the blob transaction to be mined before making request. s.Logger(). Info("waiting for blob transaction to be mined", "blobTx", blobTx.Hash().Hex()) - receipt, errWait := bind.WaitMined(ctx, s.JSONRPCBalancer(), blobTx) + receipt, errWait := bind.WaitMined(ctx, network.JSONRPCBalancer(), blobTx) s.Require().NoError(errWait) s.Require().Equal(coretypes.ReceiptStatusSuccessful, receipt.Status) @@ -130,7 +139,7 @@ func (s *BeaconKitE2ESuite) Test4844Live() { // just wait 1 block. // //nolint:contextcheck // uses the service context. - s.Require().NoError(s.WaitForNBlockNumbers(1)) + s.Require().NoError(s.WaitForNBlockNumbers(network, 1)) // Fetch blobs from node-api. response, errAPI := client0.BlobSidecars(ctx, &api.BlobSidecarsOpts{Block: receipt.BlockNumber.String()}) @@ -156,6 +165,6 @@ func (s *BeaconKitE2ESuite) Test4844Live() { wg.Wait() // Ensure Blob Tx doesn't cause liveliness issues. - err = s.WaitForNBlockNumbers(BlocksToWait4844) + err = s.WaitForNBlockNumbers(network, BlocksToWait4844) s.Require().NoError(err) } diff --git a/testing/e2e/e2e_inflation_test.go b/testing/e2e/e2e_inflation_test.go index be6d9ea5c..cdcae9cbb 100644 --- a/testing/e2e/e2e_inflation_test.go +++ b/testing/e2e/e2e_inflation_test.go @@ -29,9 +29,14 @@ import ( "github.com/ethereum/go-ethereum/params" ) -// TestEVMInflation checks that the EVM inflation address receives the correct +// runEVMInflation checks that the EVM inflation address receives the correct // amount of EVM inflation per block. -func (s *BeaconKitE2ESuite) TestEVMInflation() { +func (s *BeaconKitE2ESuite) runEVMInflation() { + s.Logger().Info("Running TestEVMInflation") + + // Get the current network + network := s.GetCurrentNetwork() + s.Require().NotNil(network, "Network instance is nil") // TODO: make test use configurable chain spec. chainspec, err := spec.DevnetChainSpec() s.Require().NoError(err) @@ -43,7 +48,7 @@ func (s *BeaconKitE2ESuite) TestEVMInflation() { preForkInflation := chainspec.EVMInflationPerBlock(math.Slot(0)) preForkAddress := chainspec.EVMInflationAddress(math.Slot(0)) for blkNum := range int64(deneb1ForkSlot) { - err = s.WaitForFinalizedBlockNumber(uint64(blkNum)) + err = s.WaitForFinalizedBlockNumber(network, uint64(blkNum)) s.Require().NoError(err) expectedBalance := new(big.Int).Mul( @@ -52,7 +57,7 @@ func (s *BeaconKitE2ESuite) TestEVMInflation() { ) var balance *big.Int - balance, err = s.JSONRPCBalancer().BalanceAt( + balance, err = network.JSONRPCBalancer().BalanceAt( s.Ctx(), gethcommon.Address(preForkAddress), big.NewInt(blkNum), @@ -75,13 +80,13 @@ func (s *BeaconKitE2ESuite) TestEVMInflation() { // take the snapshot of balance right before the fork and check it won't change anymore var preForkAddressFinalBalance *big.Int - preForkAddressFinalBalance, err = s.JSONRPCBalancer().BalanceAt( + preForkAddressFinalBalance, err = network.JSONRPCBalancer().BalanceAt( s.Ctx(), gethcommon.Address(preForkAddress), big.NewInt(int64(deneb1ForkSlot-1)), ) s.Require().NoError(err) for blkNum := deneb1ForkSlot; blkNum < deneb1ForkSlot+chainspec.SlotsPerEpoch(); blkNum++ { - err = s.WaitForFinalizedBlockNumber(blkNum) + err = s.WaitForFinalizedBlockNumber(network, blkNum) s.Require().NoError(err) expectedBalance := new(big.Int).Mul( @@ -90,7 +95,7 @@ func (s *BeaconKitE2ESuite) TestEVMInflation() { ) var balance *big.Int - balance, err = s.JSONRPCBalancer().BalanceAt( + balance, err = network.JSONRPCBalancer().BalanceAt( s.Ctx(), gethcommon.Address(postForkAddress), big.NewInt(int64(blkNum)), @@ -105,7 +110,7 @@ func (s *BeaconKitE2ESuite) TestEVMInflation() { // Enforce that the balance of the EVM inflation address // prior to the hardfork is the same as it is now. var preForkLatestBalance *big.Int - preForkLatestBalance, err = s.JSONRPCBalancer().BalanceAt( + preForkLatestBalance, err = network.JSONRPCBalancer().BalanceAt( s.Ctx(), gethcommon.Address(preForkAddress), nil, // at the current block ) s.Require().NoError(err) diff --git a/testing/e2e/e2e_staking_test.go b/testing/e2e/e2e_staking_test.go index 1ee4b936e..d89f3f1af 100644 --- a/testing/e2e/e2e_staking_test.go +++ b/testing/e2e/e2e_staking_test.go @@ -57,7 +57,11 @@ type ValidatorTestStruct struct { // TODO: // 1) Add staking tests for adding a new validator to the network. // 2) Add staking tests for hitting the validator set cap and eviction. -func (s *BeaconKitE2ESuite) TestDepositRobustness() { +func (s *BeaconKitE2ESuite) runDepositRobustness() { + s.Logger().Info("Running Deposit Robustness") + // Get the current network + network := s.GetCurrentNetwork() + s.Require().NotNil(network, "Network instance is nil") // TODO: make test use configurable chain spec. chainspec, err := spec.DevnetChainSpec() s.Require().NoError(err) @@ -80,7 +84,7 @@ func (s *BeaconKitE2ESuite) TestDepositRobustness() { ) // Get the chain ID. - chainID, err := s.JSONRPCBalancer().ChainID(s.Ctx()) + chainID, err := network.JSONRPCBalancer().ChainID(s.Ctx()) s.Require().NoError(err) // Get the chain spec used by the e2e nodes. TODO: make configurable. @@ -90,7 +94,7 @@ func (s *BeaconKitE2ESuite) TestDepositRobustness() { // Bind the deposit contract. depositContractAddress := gethcommon.Address(chainSpec.DepositContractAddress()) - dc, err := deposit.NewDepositContract(depositContractAddress, s.JSONRPCBalancer()) + dc, err := deposit.NewDepositContract(depositContractAddress, network.JSONRPCBalancer()) s.Require().NoError(err) // Enforce the deposit count at genesis is equal to the number of validators. @@ -139,7 +143,7 @@ func (s *BeaconKitE2ESuite) TestDepositRobustness() { s.Require().NotNil(val.Validator) creds := [32]byte(val.Validator.WithdrawalCredentials) withdrawalAddress := gethcommon.Address(creds[12:]) - withdrawalBalance, jErr := s.JSONRPCBalancer().BalanceAt(s.Ctx(), withdrawalAddress, nil) + withdrawalBalance, jErr := network.JSONRPCBalancer().BalanceAt(s.Ctx(), withdrawalAddress, nil) s.Require().NoError(jErr) // Populate the validators testing struct so we can keep track of the pre-state. @@ -155,24 +159,27 @@ func (s *BeaconKitE2ESuite) TestDepositRobustness() { } // Sender account - sender := s.TestAccounts()[0] + accounts := s.GetAccounts() + s.Require().NotNil(accounts, "Test accounts are nil") + s.Require().NotEmpty(accounts, "No test accounts available") + + sender := accounts[0] // Get the block num - blkNum, err := s.JSONRPCBalancer().BlockNumber(s.Ctx()) + blkNum, err := network.JSONRPCBalancer().BlockNumber(s.Ctx()) s.Require().NoError(err) // Get original evm balance - balance, err := s.JSONRPCBalancer().BalanceAt( + balance, err := network.JSONRPCBalancer().BalanceAt( s.Ctx(), sender.Address(), new(big.Int).SetUint64(blkNum), ) s.Require().NoError(err) // Get the nonce. - nonce, err := s.JSONRPCBalancer().NonceAt( + nonce, err := network.JSONRPCBalancer().NonceAt( s.Ctx(), sender.Address(), new(big.Int).SetUint64(blkNum), ) s.Require().NoError(err) - var ( tx *coretypes.Transaction clientPubkey []byte @@ -214,13 +221,13 @@ func (s *BeaconKitE2ESuite) TestDepositRobustness() { "Waiting for the final deposit tx to be mined", "num", NumDepositsLoad, "hash", tx.Hash().Hex(), ) - receipt, err := bind.WaitMined(s.Ctx(), s.JSONRPCBalancer(), tx) + receipt, err := bind.WaitMined(s.Ctx(), network.JSONRPCBalancer(), tx) s.Require().NoError(err) s.Require().Equal(coretypes.ReceiptStatusSuccessful, receipt.Status) s.Logger().Info("Final deposit tx mined successfully", "hash", receipt.TxHash.Hex()) // Give time for the nodes to catch up. - err = s.WaitForNBlockNumbers(NumDepositsLoad / chainspec.MaxDepositsPerBlock()) + err = s.WaitForNBlockNumbers(network, NumDepositsLoad/chainspec.MaxDepositsPerBlock()) s.Require().NoError(err) // Compare height of nodes 0 and 1 @@ -231,7 +238,7 @@ func (s *BeaconKitE2ESuite) TestDepositRobustness() { s.Require().InDelta(height.Response.LastBlockHeight, height2.Response.LastBlockHeight, 1) // Check to see if evm balance decreased. - postDepositBalance, err := s.JSONRPCBalancer().BalanceAt(s.Ctx(), sender.Address(), nil) + postDepositBalance, err := network.JSONRPCBalancer().BalanceAt(s.Ctx(), sender.Address(), nil) s.Require().NoError(err) // Check that the eth spent is somewhere~ (gas) between @@ -246,11 +253,11 @@ func (s *BeaconKitE2ESuite) TestDepositRobustness() { // Check that all validators' voting power have increased by // (NumDepositsLoad / NumValidators) * depositAmountWei // after the end of the epoch (next multiple of SlotsPerEpoch after receipt.BlockNumber). - blkNum, err = s.JSONRPCBalancer().BlockNumber(s.Ctx()) + blkNum, err = network.JSONRPCBalancer().BlockNumber(s.Ctx()) s.Require().NoError(err) nextEpoch := chainspec.SlotToEpoch(math.Slot(blkNum)) + 1 nextEpochBlockNum := nextEpoch.Unwrap() * chainspec.SlotsPerEpoch() - err = s.WaitForFinalizedBlockNumber(nextEpochBlockNum + 1) + err = s.WaitForFinalizedBlockNumber(network, nextEpochBlockNum+1) s.Require().NoError(err) increaseAmt := new(big.Int).Mul(depositAmountGwei, big.NewInt(int64(NumDepositsLoad/config.NumValidators))) @@ -264,7 +271,7 @@ func (s *BeaconKitE2ESuite) TestDepositRobustness() { // withdrawal balance is in Wei, so we'll convert it to Gwei. withdrawalAddress := gethcommon.Address(val.WithdrawalCredentials[12:]) - withdrawalBalanceAfter, jErr := s.JSONRPCBalancer().BalanceAt(s.Ctx(), withdrawalAddress, nil) + withdrawalBalanceAfter, jErr := network.JSONRPCBalancer().BalanceAt(s.Ctx(), withdrawalAddress, nil) s.Require().NoError(jErr) withdrawalDiff := new(big.Int).Sub(withdrawalBalanceAfter, val.WithdrawalBalance) withdrawalDiff.Div(withdrawalDiff, weiPerGwei) diff --git a/testing/e2e/e2e_startup_test.go b/testing/e2e/e2e_startup_test.go index 59905403d..274d6daa5 100644 --- a/testing/e2e/e2e_startup_test.go +++ b/testing/e2e/e2e_startup_test.go @@ -20,17 +20,81 @@ package e2e_test -import "github.com/berachain/beacon-kit/testing/e2e/suite" +import ( + "context" + "fmt" + "os" -// BeaconE2ESuite is a suite of tests simulating a fully function beacon-kit -// network. + "cosmossdk.io/log" + "github.com/berachain/beacon-kit/testing/e2e/config" + "github.com/berachain/beacon-kit/testing/e2e/suite" + "github.com/kurtosis-tech/kurtosis/api/golang/engine/lib/kurtosis_context" +) + +// BeaconKitE2ESuite is a suite of tests simulating a fully functional beacon-kit network. type BeaconKitE2ESuite struct { suite.KurtosisE2ESuite + opts []suite.Option +} + +func (s *BeaconKitE2ESuite) SetupSuiteWithOptions(opts ...suite.Option) { + s.opts = opts } -// TestBasicStartup tests the basic startup of the beacon-kit network. -// TODO: Should check all clients, opposed to just the load balancer. -func (s *BeaconKitE2ESuite) TestBasicStartup() { - err := s.WaitForFinalizedBlockNumber(10) +// SetupSuite executes before the test suite begins execution. +func (s *BeaconKitE2ESuite) SetupSuite() { + // Initialize basic configuration + s.SetContext(context.Background()) + logger := log.NewLogger(os.Stdout) + s.SetLogger(logger) + + var err error + kCtx, err := kurtosis_context.NewKurtosisContextFromLocalEngine() + s.Require().NoError(err) + s.SetKurtosisCtx(kCtx) + + s.SetNetworks(make(map[string]*suite.NetworkInstance)) + s.SetTestSpecs(make(map[string]suite.ChainSpec)) + + // Apply all chain options + for _, opt := range s.opts { + if err = opt(&s.KurtosisE2ESuite); err != nil { + s.Require().NoError(err) + } + } + + // Initialize networks + s.initializeNetworks() +} + +// initializeNetworks sets up networks for each unique chain spec. +func (s *BeaconKitE2ESuite) initializeNetworks() { + for _, spec := range s.GetTestSpecs() { + chainKey := fmt.Sprintf("%d-%s", spec.ChainID, spec.Network) + if networks := s.GetNetworks(); networks[chainKey] == nil { + network := suite.NewNetworkInstance(config.DefaultE2ETestConfig()) + network.Config.NetworkConfiguration.ChainID = int(spec.ChainID) + network.Config.NetworkConfiguration.ChainSpec = spec.Network + networks[chainKey] = network + s.SetNetworks(networks) + } + } +} + +func (s *BeaconKitE2ESuite) TestRunE2E() { + s.RegisterTestFunc("runBasicStartup", s.runBasicStartup) + s.RegisterTestFunc("runEVMInflation", s.runEVMInflation) + s.RegisterTestFunc("runBeaconAPIStartup", s.runBeaconAPIStartup) + s.RegisterTestFunc("run4844Live", s.run4844Live) + s.RegisterTestFunc("runDepositRobustness", s.runDepositRobustness) + s.RunTestsByChainSpec() +} + +func (s *BeaconKitE2ESuite) runBasicStartup() { + s.Logger().Info("Running Basic Startup") + network := s.GetCurrentNetwork() + s.Require().NotNil(network, "Network instance is nil") + + err := s.WaitForFinalizedBlockNumber(network, 10) s.Require().NoError(err) } diff --git a/testing/e2e/e2e_test.go b/testing/e2e/e2e_test.go index 0208d03ab..8d924ad3e 100644 --- a/testing/e2e/e2e_test.go +++ b/testing/e2e/e2e_test.go @@ -31,5 +31,16 @@ import ( // TestBeaconKitE2ESuite runs the test suite. func TestBeaconKitE2ESuite(t *testing.T) { - suite.Run(t, new(BeaconKitE2ESuite)) + s := new(BeaconKitE2ESuite) + + // Setup suite with chain configurations + s.SetupSuiteWithOptions( + suite.WithChain("runBasicStartup", 80087, "devnet"), + suite.WithChain("runEVMInflation", 80087, "devnet"), + suite.WithChain("runBeaconAPIStartup", 80087, "devnet"), + suite.WithChain("run4844Live", 80087, "devnet"), + suite.WithChain("runDepositRobustness", 80087, "devnet"), + ) + + suite.Run(t, s) } diff --git a/testing/e2e/suite/options.go b/testing/e2e/suite/options.go index 72a90ce5b..b57eea0f8 100644 --- a/testing/e2e/suite/options.go +++ b/testing/e2e/suite/options.go @@ -20,5 +20,21 @@ package suite -// Option is a functional option for the KurtosisE2ESuite. +type ChainSpec struct { + ChainID uint64 + Network string +} + +// Option is a function type that modifies suite configuration. type Option func(*KurtosisE2ESuite) error + +// WithChain adds a chain configuration for a specific test. +func WithChain(testName string, chainID uint64, network string) Option { + return func(s *KurtosisE2ESuite) error { + s.RegisterTest(testName, ChainSpec{ + ChainID: chainID, + Network: network, + }) + return nil + } +} diff --git a/testing/e2e/suite/setup.go b/testing/e2e/suite/setup.go index 9d6696500..1f08c9dd0 100644 --- a/testing/e2e/suite/setup.go +++ b/testing/e2e/suite/setup.go @@ -25,302 +25,375 @@ import ( "crypto/ecdsa" "fmt" "math/big" - "sync/atomic" + "strings" "time" "cosmossdk.io/log" "github.com/berachain/beacon-kit/errors" - "github.com/berachain/beacon-kit/testing/e2e/config" "github.com/berachain/beacon-kit/testing/e2e/suite/types" - "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/rpc" "github.com/kurtosis-tech/kurtosis/api/golang/core/lib/enclaves" - "github.com/kurtosis-tech/kurtosis/api/golang/core/lib/services" "github.com/kurtosis-tech/kurtosis/api/golang/core/lib/starlark_run_config" "github.com/kurtosis-tech/kurtosis/api/golang/engine/lib/kurtosis_context" - "github.com/sourcegraph/conc/iter" ) -// SetupSuite executes before the test suite begins execution. -func (s *KurtosisE2ESuite) SetupSuite() { - s.SetupSuiteWithOptions() -} +// Setup related functions -// SetupSuiteWithOptions sets up the test suite with the provided options. -func (s *KurtosisE2ESuite) SetupSuiteWithOptions(opts ...Option) { - var ( - key0, key1, key2 *ecdsa.PrivateKey - err error - ) +// setupEnclave creates and initializes the enclave for the network. +// It ensures any existing enclave with the same name is cleaned up first. +func (s *KurtosisE2ESuite) setupEnclave(network *NetworkInstance) error { + // Create unique enclave name for this chain spec + s.Logger().Info("Creating enclave", "chainSpec", network.Config.NetworkConfiguration.ChainSpec) + enclaveName := "e2e-test-enclave-" + network.Config.NetworkConfiguration.ChainSpec - // Setup some sane defaults. - s.cfg = config.DefaultE2ETestConfig() - s.ctx = context.Background() - s.logger = log.NewTestLogger(s.T()) - s.Require().NoError(err, "Error loading starlark helper file") - s.testAccounts = make([]*types.EthAccount, 3) //nolint:mnd // number of accounts. + // Try to destroy any existing enclave with the same name + if err := s.cleanupExistingEnclave(enclaveName); err != nil { + return err + } - s.genesisAccount = types.NewEthAccountFromHex( - "genesisAccount", "fffdbb37105441e14b0ee6330d855d8504ff39e705c3afa8f859ac9865f99306", + var err error + network.enclave, err = s.kCtx.CreateEnclave(s.ctx, enclaveName) + if err != nil { + return fmt.Errorf("failed to create enclave: %w", err) + } + + // Run Starlark package + result, err := network.enclave.RunStarlarkPackageBlocking( + s.ctx, + "../../kurtosis", + starlark_run_config.NewRunStarlarkConfig( + starlark_run_config.WithSerializedParams(network.Config.MustMarshalJSON()), + ), ) - key0, err = crypto.GenerateKey() - s.Require().NoError(err, "Error generating key") - key1, err = crypto.GenerateKey() - s.Require().NoError(err, "Error generating key") - key2, err = crypto.GenerateKey() - s.Require().NoError(err, "Error generating key") - - s.testAccounts[0] = types.NewEthAccount("testAccount0", key0) - s.testAccounts[1] = types.NewEthAccount("testAccount1", key1) - s.testAccounts[2] = types.NewEthAccount("testAccount2", key2) - - // Apply all the provided options, this allows - // the test suite to be configured in a flexible manner by - // the caller (i.e. overriding defaults). - for _, opt := range opts { - if err = opt(s); err != nil { - s.Require().NoError(err, "Error applying option") + if err != nil { + return fmt.Errorf("failed to run starlark package: %w", err) + } + if result.ExecutionError != nil { + return fmt.Errorf("starlark execution error: %s", result.ExecutionError) + } + + return nil +} + +// setupConsensusClients initializes and connects all consensus clients for the network. +// It creates clients based on the network's validator configuration. +func (s *KurtosisE2ESuite) setupConsensusClients(network *NetworkInstance) error { + s.Logger().Info("Setting up validator clients", "clients", network.Config.NetworkConfiguration.Validators.Nodes) + for i := range network.Config.NetworkConfiguration.Validators.Nodes { + if err := s.setupSingleConsensusClient(network, i); err != nil { + return err } } + s.Logger().Info("Set up consensus clients", "clients", network.consensusClients) + return nil +} + +// setupLoadBalancer initializes the network's load balancer and waits for RPC readiness. +func (s *KurtosisE2ESuite) setupLoadBalancer(network *NetworkInstance) error { + balancerType := network.Config.EthJSONRPCEndpoints[0].Type + sCtx, err := network.enclave.GetServiceContext(balancerType) + if err != nil { + return fmt.Errorf("failed to get balancer service context: %w", err) + } - s.kCtx, err = kurtosis_context.NewKurtosisContextFromLocalEngine() - s.Require().NoError(err) - s.logger.Info("Destroying any existing enclave...") - //#nosec:G703 // It's okay if this errors out. It will error out - // if there is no enclave to destroy. - _ = s.kCtx.DestroyEnclave(s.ctx, "e2e-test-enclave") + loadBalancer, err := types.NewLoadBalancer(sCtx) + if err != nil { + return fmt.Errorf("failed to create load balancer: %w", err) + } - s.logger.Info("Creating enclave...") - s.enclave, err = s.kCtx.CreateEnclave(s.ctx, "e2e-test-enclave") - s.Require().NoError(err) + // Verify the load balancer was created successfully + if loadBalancer == nil { + return errors.New("load balancer is nil after creation") + } + network.loadBalancer = loadBalancer - s.logger.Info( - "Spinning up enclave...", - "num_validators", - len(s.cfg.NetworkConfiguration.Validators.Nodes), - "num_full_nodes", - len(s.cfg.NetworkConfiguration.FullNodes.Nodes), - ) + return s.waitForRPCReady(network) +} - result, err := s.enclave.RunStarlarkPackageBlocking( - s.ctx, "../../kurtosis", - starlark_run_config.NewRunStarlarkConfig( - starlark_run_config.WithSerializedParams(s.cfg.MustMarshalJSON()), - ), +// setupAccounts initializes and funds the genesis and test accounts. +// It ensures the genesis account has sufficient funds before creating test accounts. +func (s *KurtosisE2ESuite) setupAccounts(network *NetworkInstance) error { + // Initialize genesis account + network.genesisAccount = types.NewEthAccountFromHex( + "genesisAccount", "fffdbb37105441e14b0ee6330d855d8504ff39e705c3afa8f859ac9865f99306", ) - s.Require().NoError(err, "Error running Starlark package") - s.Require().Nil(result.ExecutionError, "Error running Starlark package") - s.Require().Empty(result.ValidationErrors) - s.logger.Info("Enclave spun up successfully") - - s.logger.Info("Setting up consensus clients") - err = s.SetupConsensusClients() - s.Require().NoError(err, "Error setting up consensus clients") - - // Setup the JSON-RPC balancer. - s.logger.Info("Setting up JSON-RPC balancer") - err = s.SetupJSONRPCBalancer() - s.Require().NoError(err, "Error setting up JSON-RPC balancer") - - s.logger.Info("Waiting for nodes to get ready...") - //nolint:mnd // its okay. - time.Sleep(5 * time.Second) - // Wait for the finalized block number to increase a bit to - // ensure all clients are in sync. - //nolint:mnd // 3 blocks - err = s.WaitForFinalizedBlockNumber(3) - s.Require().NoError(err, "Error waiting for finalized block number") - - // Fund any requested accounts. - s.FundAccounts() -} -// SetupConsensusClients sets up the consensus clients for the validator nodes. -func (s *KurtosisE2ESuite) SetupConsensusClients() error { - s.consensusClients = make(map[string]*types.ConsensusClient, config.NumValidators) + // Wait for a few blocks to ensure the genesis account has funds + //nolint:mnd // 5 blocks + if err := s.WaitForNBlockNumbers(network, 5); err != nil { + return fmt.Errorf("failed waiting for blocks: %w", err) + } - var ( - sCtx *services.ServiceContext - res *enclaves.StarlarkRunResult - err error - ) - for i := range config.NumValidators { - var clientName string - //nolint:mnd // its okay. - switch i % config.NumValidators { - case 0: - clientName = config.ClientValidator0 - case 1: - clientName = config.ClientValidator1 - case 2: - clientName = config.ClientValidator2 - case 3: - clientName = config.ClientValidator3 - case 4: - clientName = config.ClientValidator4 + // Verify genesis account balance + balance, err := network.JSONRPCBalancer().BalanceAt(s.ctx, network.genesisAccount.Address(), nil) + if err != nil { + return fmt.Errorf("failed to get genesis balance: %w", err) + } + s.Logger().Info("Genesis account balance", "balance", balance) + if balance.Cmp(big.NewInt(0)) == 0 { + return errors.New("genesis account has no funds") + } + + if err = s.generateTestAccounts(network); err != nil { + return fmt.Errorf("failed to generate test accounts: %w", err) + } + + // Fund test accounts using the genesis account + for _, account := range network.testAccounts { + //nolint:mnd // 60000 ETH + amount, ok := new(big.Int).SetString("60000000000000000000000", 10) + if !ok { + return errors.New("failed to parse amount") } - sCtx, err = s.Enclave().GetServiceContext(clientName) - if err != nil { - return err + if err = s.fundAccount(network, account.Address(), amount); err != nil { + return fmt.Errorf("failed to fund test accounts: %w", err) } + } + return nil +} - s.consensusClients[clientName] = types.NewConsensusClient( - types.NewWrappedServiceContext(sCtx, s.Enclave().RunStarlarkScriptBlocking), - ) - if res, err = s.consensusClients[clientName].Start( - context.Background(), s.Enclave(), - ); err != nil { - return err +// Helper functions + +// cleanupExistingEnclave attempts to destroy any existing enclave with the given name. +// It logs any errors but continues execution to allow setup to proceed. +func (s *KurtosisE2ESuite) cleanupExistingEnclave(enclaveName string) error { + enclaves, err := s.kCtx.GetEnclaves(s.ctx) + if err != nil { + s.Logger().Error("Failed to get enclaves", "error", err) + return nil // Continue with setup even if we can't check enclaves + } + + for _, e := range enclaves.GetEnclavesByUuid() { + if e.GetName() == enclaveName { + s.Logger().Info("Destroying existing enclave", "name", enclaveName) + if err = s.kCtx.DestroyEnclave(s.ctx, e.GetEnclaveUuid()); err != nil { + s.Logger().Error("Failed to destroy existing enclave", "error", err) + } } - if res.ExecutionError != nil { - return errors.New(res.ExecutionError.String()) + } + return nil +} + +// setupSingleConsensusClient initializes a single consensus client with the given index. +// It creates the client, establishes connection, and adds it to the network's client map. +func (s *KurtosisE2ESuite) setupSingleConsensusClient(network *NetworkInstance, i int) error { + clientName := fmt.Sprintf("cl-validator-beaconkit-%d", i) + sCtx, err := network.enclave.GetServiceContext(clientName) + if err != nil { + return fmt.Errorf("failed to get service context: %w", err) + } + + client := types.NewConsensusClient( + types.NewWrappedServiceContext(sCtx, network.enclave.RunStarlarkScriptBlocking), + ) + if err = client.Connect(s.ctx); err != nil { + return fmt.Errorf("failed to connect consensus client %s: %w", clientName, err) + } + network.consensusClients[clientName] = client + s.Logger().Info("Created consensus client", "name", clientName) + return nil +} + +// waitForRPCReady polls the RPC endpoint until it responds successfully or times out. +// It attempts up to maxRetries times with a 2-second delay between attempts. +func (s *KurtosisE2ESuite) waitForRPCReady(network *NetworkInstance) error { + s.Logger().Info("Waiting for RPC to be ready", "url", network.loadBalancer.URL()) + maxRetries := 30 + for attempt := range maxRetries { + blockNum, err := network.loadBalancer.BlockNumber(s.ctx) + if err == nil { + s.Logger().Info("RPC is ready", "blockNum", blockNum) + return nil } - if len(res.ValidationErrors) > 0 { - return errors.New(res.ValidationErrors[0].String()) + s.Logger().Info("RPC not ready yet", + "attempt", attempt+1, + "url", network.loadBalancer.URL(), + "error", err, + ) + //nolint:mnd // 2 seconds + time.Sleep(2 * time.Second) + } + return fmt.Errorf("RPC not ready after %d retries", maxRetries) +} + +// generateTestAccounts creates three test accounts with freshly generated keys. +// It returns an error if key generation fails for any account. +func (s *KurtosisE2ESuite) generateTestAccounts(network *NetworkInstance) error { + // Generate keys for test accounts + //nolint:mnd // 3 accounts + keys := make([]*ecdsa.PrivateKey, 3) + for i := range keys { + var err error + keys[i], err = crypto.GenerateKey() + if err != nil { + return fmt.Errorf("error generating key%d: %w", i, err) } } + // Initialize test accounts with generated keys + network.testAccounts = []*types.EthAccount{ + types.NewEthAccount("testAccount0", keys[0]), + types.NewEthAccount("testAccount1", keys[1]), + types.NewEthAccount("testAccount2", keys[2]), + } + return nil } -// SetupJSONRPCBalancer sets up the load balancer for the test suite. -func (s *KurtosisE2ESuite) SetupJSONRPCBalancer() error { - // get the type for EthJSONRPCEndpoint - typeRPCEndpoint := s.JSONRPCBalancerType() +// fundAccount sends ETH to the given address. +func (s *KurtosisE2ESuite) fundAccount(network *NetworkInstance, to common.Address, amount *big.Int) error { + // Get initial balance + initialBalance, err := network.JSONRPCBalancer().BalanceAt(s.ctx, to, nil) + if err != nil { + return fmt.Errorf("failed to get initial balance: %w", err) + } - s.logger.Info("Setting up JSON-RPC balancer:", "type", typeRPCEndpoint) + nonce, err := network.JSONRPCBalancer().PendingNonceAt(s.ctx, network.genesisAccount.Address()) + if err != nil { + return err + } - sCtx, err := s.Enclave().GetServiceContext(typeRPCEndpoint) + chainID, err := network.JSONRPCBalancer().ChainID(s.ctx) if err != nil { return err } - if s.loadBalancer, err = types.NewLoadBalancer( - sCtx, - ); err != nil { + // Get the latest block for fee estimation + header, err := network.JSONRPCBalancer().HeaderByNumber(s.ctx, nil) + if err != nil { return err } + //nolint:mnd // 1 Gwei + gasFeeCap := new(big.Int).Add(header.BaseFee, big.NewInt(1e9)) + + //nolint:mnd // 21000 gas + tx := ethtypes.NewTx(ðtypes.DynamicFeeTx{ + ChainID: chainID, + Nonce: nonce, + To: &to, + Value: amount, + Gas: 21000, + GasFeeCap: gasFeeCap, + GasTipCap: big.NewInt(1e9), + }) + + signedTx, err := network.genesisAccount.SignTx(chainID, tx) + if err != nil { + return err + } + + if err = network.JSONRPCBalancer().SendTransaction(s.ctx, signedTx); err != nil { + return fmt.Errorf("failed to send transaction: %w", err) + } + + // Wait for transaction to be mined + receipt, err := s.WaitForTransactionReceipt(network, signedTx.Hash()) + if err != nil { + return fmt.Errorf("failed waiting for transaction: %w", err) + } + + // Verify transaction success + if receipt.Status != ethtypes.ReceiptStatusSuccessful { + return fmt.Errorf("transaction failed with status: %d", receipt.Status) + } + + // Verify balance increase + newBalance, err := network.JSONRPCBalancer().BalanceAt(s.ctx, to, nil) + if err != nil { + return fmt.Errorf("failed to get new balance: %w", err) + } + + if newBalance.Cmp(initialBalance) <= 0 { + return fmt.Errorf("balance did not increase: old=%s new=%s", initialBalance, newBalance) + } + s.Logger().Info("Successfully funded account", + "address", to.Hex(), + "amount", amount, + "oldBalance", initialBalance, + "newBalance", newBalance, + ) return nil } -// FundAccounts funds the accounts for the test suite. -// -//nolint:funlen -func (s *KurtosisE2ESuite) FundAccounts() { - ctx := context.Background() - nonce := atomic.Uint64{} - pendingNonce, err := s.JSONRPCBalancer().PendingNonceAt( - ctx, s.genesisAccount.Address()) - s.Require().NoError(err, "Failed to get nonce for genesis account") - nonce.Store(pendingNonce) - - var chainID *big.Int - chainID, err = s.JSONRPCBalancer().ChainID(ctx) - s.Require().NoError(err, "failed to get chain ID") - s.logger.Info("Chain-id is", "chain_id", chainID) - _, err = iter.MapErr( - s.testAccounts, - func(acc **types.EthAccount) (*ethtypes.Receipt, error) { - account := *acc - var gasTipCap *big.Int - - if gasTipCap, err = s.JSONRPCBalancer().SuggestGasTipCap(ctx); err != nil { - var rpcErr rpc.Error - if errors.As(err, &rpcErr) && rpcErr.ErrorCode() == -32601 { - // Besu does not support eth_maxPriorityFeePerGas - // so we use a default value of 10 Gwei. - gasTipCap = big.NewInt(0).SetUint64(TenGwei) - } else { - return nil, err - } - } +// stopSingleConsensusClient stops a single consensus client safely. +// It handles nil clients and logs any errors that occur during shutdown. +func (s *KurtosisE2ESuite) stopSingleConsensusClient(name string, client *types.ConsensusClient) error { + if client == nil || client.Client == nil { + s.Logger().Info("Client is nil, skipping", "name", name) + return nil + } - gasFeeCap := new(big.Int).Add( - gasTipCap, big.NewInt(0).SetUint64(TenGwei)) - nonceToSubmit := nonce.Add(1) - 1 - //nolint:mnd // 20000 Ether - value := new(big.Int).Mul(big.NewInt(200000), big.NewInt(Ether)) - dest := account.Address() - var signedTx *ethtypes.Transaction - if signedTx, err = s.genesisAccount.SignTx( - chainID, ethtypes.NewTx(ðtypes.DynamicFeeTx{ - ChainID: chainID, Nonce: nonceToSubmit, - GasTipCap: gasTipCap, GasFeeCap: gasFeeCap, - Gas: EtherTransferGasLimit, To: &dest, - Value: value, Data: nil, - }), - ); err != nil { - return nil, err - } + s.Logger().Info("Stopping consensus client", "name", name) + res, err := client.Stop(s.ctx) + if err != nil { + return fmt.Errorf("failed to stop client %s: %w", name, err) + } - cctx, cancel := context.WithTimeout(ctx, DefaultE2ETestTimeout) - defer cancel() - if err = s.JSONRPCBalancer().SendTransaction(cctx, signedTx); err != nil { - s.logger.Error( - "error submitting funding transaction", - "error", - err, - ) - return nil, err - } + if res != nil && res.ExecutionError != nil { + return fmt.Errorf("client %s stop returned error: %s", name, res.ExecutionError) + } + + return nil +} - s.logger.Info( - "Funding transaction submitted, waiting for confirmation...", - "tx_hash", signedTx.Hash().Hex(), "nonce", nonceToSubmit, - "account", account.Name(), "value", value, - ) +// stopConsensusClients stops all consensus clients in the network. +// It attempts to stop each client individually and returns on first error. +func (s *KurtosisE2ESuite) stopConsensusClients(network *NetworkInstance) error { + s.Logger().Info("Stopping consensus clients in cleanupNetwork", "count", len(network.consensusClients)) + for name, client := range network.consensusClients { + if err := s.stopSingleConsensusClient(name, client); err != nil { + return err + } + } + return nil +} - var receipt *ethtypes.Receipt +// checkEnclaveExists verifies if the enclave still exists in Kurtosis. +// It returns true if the enclave UUID is found in the list of active enclaves. +func (s *KurtosisE2ESuite) checkEnclaveExists(enclave *enclaves.EnclaveContext) (bool, error) { + enclaves, err := s.kCtx.GetEnclaves(s.ctx) + if err != nil { + return false, fmt.Errorf("failed to get enclaves: %w", err) + } - if receipt, err = bind.WaitMined( - cctx, s.JSONRPCBalancer(), signedTx, - ); err != nil { - return nil, err - } + for _, e := range enclaves.GetEnclavesByUuid() { + if e.GetEnclaveUuid() == string(enclave.GetEnclaveUuid()) { + return true, nil + } + } - s.logger.Info( - "Funding transaction confirmed", - "tx_hash", signedTx.Hash().Hex(), - "account", account.Name(), - ) + return false, nil +} - // Verify the receipt status. - if receipt.Status != ethtypes.ReceiptStatusSuccessful { - return nil, err - } +// destroyEnclave destroys the network's enclave if it exists. +// It checks for existence first to avoid errors on already destroyed enclaves. +func (s *KurtosisE2ESuite) destroyEnclave(network *NetworkInstance) error { + if network.enclave == nil { + s.Logger().Info("Enclave is nil, skipping destruction") + return nil + } - // Wait an extra block to ensure all clients are in sync. - //nolint:mnd,contextcheck // its okay. - if err = s.WaitForFinalizedBlockNumber( - receipt.BlockNumber.Uint64() + 2, - ); err != nil { - return nil, err - } + exists, err := s.checkEnclaveExists(network.enclave) + if err != nil { + s.Logger().Error("Failed to check enclave existence", "error", err) + return nil // Continue with cleanup even if we can't check existence + } - // Verify the balance of the account - var balance *big.Int - if balance, err = s.JSONRPCBalancer().BalanceAt(ctx, dest, nil); err != nil { - return nil, err - } else if balance.Cmp(value) != 0 { - return nil, errors.Wrap( - ErrUnexpectedBalance, fmt.Sprintf("expected: %v, got: %v", value, balance), - ) - } - return receipt, nil - }, - ) - s.Require().NoError(err, "Error funding accounts") + if !exists { + s.Logger().Info("Enclave already destroyed", "uuid", network.enclave.GetEnclaveUuid()) + return nil + } + + s.Logger().Info("Destroying enclave", "uuid", network.enclave.GetEnclaveUuid()) + return s.kCtx.DestroyEnclave(s.ctx, string(network.enclave.GetEnclaveUuid())) } -// WaitForFinalizedBlockNumber waits for the finalized block number -// to reach the target block number across all execution clients. -func (s *KurtosisE2ESuite) WaitForFinalizedBlockNumber( - target uint64, -) error { +// WaitForFinalizedBlockNumber waits until the chain reaches the target block number. +// It polls the chain state and returns an error if the context deadline is exceeded. +func (s *KurtosisE2ESuite) WaitForFinalizedBlockNumber(network *NetworkInstance, target uint64) error { cctx, cancel := context.WithTimeout(s.ctx, DefaultE2ETestTimeout) defer cancel() ticker := time.NewTicker(time.Second) @@ -335,7 +408,7 @@ func (s *KurtosisE2ESuite) WaitForFinalizedBlockNumber( } var err error - finalBlockNum, err = s.JSONRPCBalancer().BlockNumber(cctx) + finalBlockNum, err = network.JSONRPCBalancer().BlockNumber(cctx) if err != nil { s.logger.Error("error getting finalized block number", "error", err) continue @@ -367,41 +440,128 @@ func (s *KurtosisE2ESuite) WaitForFinalizedBlockNumber( return nil } -// WaitForNBlockNumbers waits for a specified amount of blocks into the future -// from now. -func (s *KurtosisE2ESuite) WaitForNBlockNumbers( - n uint64, -) error { - current, err := s.JSONRPCBalancer().BlockNumber(s.ctx) +// WaitForNBlockNumbers waits for a specified amount of blocks into the future from now. +// It gets the current block number and waits until target = current + n blocks. +func (s *KurtosisE2ESuite) WaitForNBlockNumbers(network *NetworkInstance, n uint64) error { + current, err := network.JSONRPCBalancer().BlockNumber(s.ctx) if err != nil { return err } - return s.WaitForFinalizedBlockNumber(current + n) + return s.WaitForFinalizedBlockNumber(network, current+n) } -// TearDownSuite cleans up resources after all tests have been executed. -// this function executes after all tests executed. -func (s *KurtosisE2ESuite) TearDownSuite() { - s.Logger().Info("Destroying enclave...") - for _, client := range s.consensusClients { - res, err := client.Stop(s.ctx) - s.Require().NoError(err, "Error stopping consensus client") - s.Require().Nil(res.ExecutionError, "Error stopping consensus client") - s.Require().Empty(res.ValidationErrors, "Error stopping consensus client") - } - s.Require().NoError(s.kCtx.DestroyEnclave(s.ctx, "e2e-test-enclave")) +// GetAccounts returns all test accounts created for the test suite. +func (s *KurtosisE2ESuite) GetAccounts() []*types.EthAccount { + network := s.GetCurrentNetwork() + if network == nil { + s.Logger().Error("No network found for current test") + return nil + } + return network.testAccounts } -// CheckForSuccessfulTx returns true if the transaction was successful. -func (s *KurtosisE2ESuite) CheckForSuccessfulTx( - tx common.Hash, -) bool { - ctx, cancel := context.WithTimeout(s.Ctx(), DefaultE2ETestTimeout) - defer cancel() - receipt, err := s.JSONRPCBalancer().TransactionReceipt(ctx, tx) - if err != nil { - s.Logger().Error("Error getting transaction receipt", "error", err) - return false +// RegisterTest associates a test with its chain specification. +func (s *KurtosisE2ESuite) RegisterTest(testName string, spec ChainSpec) { + s.mu.Lock() + defer s.mu.Unlock() + s.testSpecs[testName] = spec +} + +// SetNetworks sets the networks for the test suite. +func (s *KurtosisE2ESuite) SetNetworks(networks map[string]*NetworkInstance) { + s.networks = networks +} + +// Networks returns the networks for the test suite. +func (s *KurtosisE2ESuite) GetNetworks() map[string]*NetworkInstance { + return s.networks +} + +// SetTestSpecs sets the test specs for the test suite. +func (s *KurtosisE2ESuite) SetTestSpecs(specs map[string]ChainSpec) { + s.testSpecs = specs +} + +// TestSpecs returns the test specs for the test suite. +func (s *KurtosisE2ESuite) GetTestSpecs() map[string]ChainSpec { + return s.testSpecs +} + +func (s *KurtosisE2ESuite) SetKurtosisCtx(ctx *kurtosis_context.KurtosisContext) { + s.kCtx = ctx +} + +// SetContext sets the main context for the test suite. +func (s *KurtosisE2ESuite) SetContext(ctx context.Context) { + s.ctx = ctx +} + +// RegisterTestFunc registers a test function with a name. +func (s *KurtosisE2ESuite) RegisterTestFunc(name string, fn func()) { + if s.testFuncs == nil { + s.testFuncs = make(map[string]func()) } - return receipt.Status == ethtypes.ReceiptStatusSuccessful + s.testFuncs[name] = fn +} + +// ConsensusClients returns all consensus clients associated with the test suite. +func (s *KurtosisE2ESuite) ConsensusClients() map[string]*types.ConsensusClient { + network := s.GetCurrentNetwork() + if network == nil { + s.Logger().Error("No network found for current test") + return nil + } + return network.consensusClients +} + +// WaitForTransactionReceipt waits for a transaction to be mined and returns the receipt. +// It attempts to get the receipt for up to 30 seconds before timing out. +func (s *KurtosisE2ESuite) WaitForTransactionReceipt(network *NetworkInstance, tx common.Hash) (*ethtypes.Receipt, error) { + for range 30 { + receipt, err := network.JSONRPCBalancer().TransactionReceipt(s.ctx, tx) + if err == nil { + return receipt, nil + } + time.Sleep(time.Second) + } + return nil, fmt.Errorf("transaction not mined within timeout: %s", tx.Hex()) +} + +// GetCurrentNetwork returns the network instance for the current running test. +// It extracts the test name from the full path and looks up the corresponding network. +func (s *KurtosisE2ESuite) GetCurrentNetwork() *NetworkInstance { + s.mu.RLock() + defer s.mu.RUnlock() + + testName := s.T().Name() + // Extract the actual test name from the full path + if idx := strings.LastIndex(testName, "/"); idx != -1 { + testName = testName[idx+1:] + } + + spec := s.testSpecs[testName] + chainKey := fmt.Sprintf("%d-%s", spec.ChainID, spec.Network) + return s.networks[chainKey] +} + +// Ctx returns the context used throughout the test suite. +func (s *KurtosisE2ESuite) Ctx() context.Context { + return s.ctx +} + +// SetLogger sets the logger instance for the test suite. +func (s *KurtosisE2ESuite) SetLogger(l log.Logger) { + s.logger = l +} + +// Network Instance Methods + +// ConsensusClients returns the consensus clients for a specific network instance. +func (n *NetworkInstance) ConsensusClients() map[string]*types.ConsensusClient { + return n.consensusClients +} + +// JSONRPCBalancer returns the JSON-RPC load balancer for the network instance. +func (n *NetworkInstance) JSONRPCBalancer() *types.LoadBalancer { + return n.loadBalancer } diff --git a/testing/e2e/suite/suite.go b/testing/e2e/suite/suite.go index 63dd7ffea..ec5a971a7 100644 --- a/testing/e2e/suite/suite.go +++ b/testing/e2e/suite/suite.go @@ -22,7 +22,10 @@ package suite import ( "context" + "fmt" + "sync" + "github.com/berachain/beacon-kit/errors" "github.com/berachain/beacon-kit/log" "github.com/berachain/beacon-kit/testing/e2e/config" "github.com/berachain/beacon-kit/testing/e2e/suite/types" @@ -40,83 +43,121 @@ var Run = suite.Run // KurtosisE2ESuite. type KurtosisE2ESuite struct { suite.Suite - cfg *config.E2ETestConfig - logger log.Logger - ctx context.Context - kCtx *kurtosis_context.KurtosisContext - enclave *enclaves.EnclaveContext + logger log.Logger + ctx context.Context + kCtx *kurtosis_context.KurtosisContext + + // Network management + networks map[string]*NetworkInstance // maps chainSpec to network + testSpecs map[string]ChainSpec // maps testName to chainSpec + testFuncs map[string]func() // maps test names to test functions + mu sync.RWMutex +} - // TODO: Figure out what these may be useful for. +// NetworkInstance represents a single network configuration. +type NetworkInstance struct { + Config *config.E2ETestConfig consensusClients map[string]*types.ConsensusClient - // executionClients map[string]*types.ExecutionClient - loadBalancer *types.LoadBalancer - - genesisAccount *types.EthAccount - testAccounts []*types.EthAccount + loadBalancer *types.LoadBalancer + genesisAccount *types.EthAccount + testAccounts []*types.EthAccount + enclave *enclaves.EnclaveContext } -// ConsensusClients returns the consensus clients associated with the -// KurtosisE2ESuite. -func ( - s *KurtosisE2ESuite, -) ConsensusClients() map[string]*types.ConsensusClient { - return s.consensusClients +// NewNetworkInstance creates a new network instance. +func NewNetworkInstance(cfg *config.E2ETestConfig) *NetworkInstance { + return &NetworkInstance{ + Config: cfg, + consensusClients: make(map[string]*types.ConsensusClient), + } } -// Ctx returns the context associated with the KurtosisE2ESuite. -// This context is used throughout the suite to control the flow of operations, -// including timeouts and cancellations. -func (s *KurtosisE2ESuite) Ctx() context.Context { - return s.ctx +// Logger returns the logger for the test suite. +func (s *KurtosisE2ESuite) Logger() log.Logger { + return s.logger } -// Enclave returns the enclave running the beacon-kit network. -func (s *KurtosisE2ESuite) Enclave() *enclaves.EnclaveContext { - return s.enclave +// RunTestsByChainSpec runs all tests for each chain spec. +func (s *KurtosisE2ESuite) RunTestsByChainSpec() { + s.Logger().Info("RunTestsByChainSpec", "testSpecs", s.testSpecs) + // Group tests by chain spec + testsBySpec := make(map[string][]string) + for testName, spec := range s.testSpecs { + chainKey := fmt.Sprintf("%d-%s", spec.ChainID, spec.Network) + testsBySpec[chainKey] = append(testsBySpec[chainKey], testName) + } + + // For each chain spec + for chainKey, tests := range testsBySpec { + s.Logger().Info("Setting up network for chain spec", "chainKey", chainKey) + + // Initialize network for this chain spec + network := s.networks[chainKey] + if err := s.InitializeNetwork(network); err != nil { + s.T().Fatalf("Failed to initialize network for %s: %v", chainKey, err) + } + + // Run all tests for this chain spec + for _, testName := range tests { + s.Logger().Info("Running test", "test", testName) + s.Run(testName, func() { + fn, ok := s.testFuncs[testName] + if !ok { + s.T().Errorf("Test method %s not found", testName) + return + } + fn() + }) + } + + // Clean up network after all tests for this chain spec are done + if err := s.CleanupNetwork(network); err != nil { + s.Logger().Error("Failed to cleanup network", "error", err) + } + } } -// Config returns the E2ETestConfig associated with the KurtosisE2ESuite. -func (s *KurtosisE2ESuite) Config() *config.E2ETestConfig { - return s.cfg -} +// InitializeNetwork sets up a network using the provided configuration. +func (s *KurtosisE2ESuite) InitializeNetwork(network *NetworkInstance) error { + if network == nil { + return errors.New("network instance cannot be nil") + } -// KurtosisCtx returns the KurtosisContext associated with the KurtosisE2ESuite. -// The KurtosisContext is a critical component that facilitates interaction with -// the Kurtosis testnet, including creating and managing enclaves. -func (s *KurtosisE2ESuite) KurtosisCtx() *kurtosis_context.KurtosisContext { - return s.kCtx -} + if err := s.setupEnclave(network); err != nil { + return fmt.Errorf("failed to setup enclave: %w", err) + } -// ExecutionClients returns the execution clients associated with the -// KurtosisE2ESuite. -func ( - s *KurtosisE2ESuite, -) ExecutionClients() map[string]*types.ExecutionClient { - return nil -} + if err := s.setupConsensusClients(network); err != nil { + return fmt.Errorf("failed to setup consensus clients: %w", err) + } -// JSONRPCBalancer returns the JSON-RPC balancer for the test suite. -func (s *KurtosisE2ESuite) JSONRPCBalancer() *types.LoadBalancer { - return s.loadBalancer -} + if err := s.setupLoadBalancer(network); err != nil { + return fmt.Errorf("failed to setup load balancer: %w", err) + } -// JSONRPCBalancerType returns the type of the JSON-RPC balancer -// for the test suite. -func (s *KurtosisE2ESuite) JSONRPCBalancerType() string { - return s.cfg.EthJSONRPCEndpoints[0].Type -} + if err := s.setupAccounts(network); err != nil { + return fmt.Errorf("failed to setup accounts: %w", err) + } -// Logger returns the logger for the test suite. -func (s *KurtosisE2ESuite) Logger() log.Logger { - return s.logger + return nil } -// GenesisAccount returns the genesis account for the test suite. -func (s *KurtosisE2ESuite) GenesisAccount() *types.EthAccount { - return s.genesisAccount -} +// CleanupNetwork cleans up the network resources. +func (s *KurtosisE2ESuite) CleanupNetwork(network *NetworkInstance) error { + if network == nil || len(network.consensusClients) == 0 { + // Network already cleaned up + s.Logger().Info("Network is nil, skipping cleanup") + return nil + } + + if err := s.stopConsensusClients(network); err != nil { + s.Logger().Error("Failed to stop consensus clients", "error", err) + // Continue with cleanup even if consensus clients fail to stop + } -// TestAccounts returns the test accounts for the test suite. -func (s *KurtosisE2ESuite) TestAccounts() []*types.EthAccount { - return s.testAccounts + if err := s.destroyEnclave(network); err != nil { + return fmt.Errorf("failed to destroy enclave: %w", err) + } + + return nil }