diff --git a/changelog/nisdas_e2e_execution_requests.md b/changelog/nisdas_e2e_execution_requests.md new file mode 100644 index 0000000000..a18f4936f3 --- /dev/null +++ b/changelog/nisdas_e2e_execution_requests.md @@ -0,0 +1,3 @@ +### Added + +- Added the ability for execution requests to be tested in e2e with electra. \ No newline at end of file diff --git a/config/params/testnet_e2e_config.go b/config/params/testnet_e2e_config.go index a3e829fd52..bd2a4f0dbe 100644 --- a/config/params/testnet_e2e_config.go +++ b/config/params/testnet_e2e_config.go @@ -24,6 +24,7 @@ func E2ETestConfig() *BeaconChainConfig { e2eConfig.GenesisDelay = 10 // 10 seconds so E2E has enough time to process deposits and get started. e2eConfig.ChurnLimitQuotient = 65536 e2eConfig.MaxValidatorsPerWithdrawalsSweep = 128 + e2eConfig.MinPerEpochChurnLimitElectra = 256000000000 // Time parameters. e2eConfig.SecondsPerSlot = 10 @@ -73,6 +74,7 @@ func E2EMainnetTestConfig() *BeaconChainConfig { e2eConfig.MinGenesisActiveValidatorCount = 256 e2eConfig.GenesisDelay = 25 // 25 seconds so E2E has enough time to process deposits and get started. e2eConfig.ChurnLimitQuotient = 65536 + e2eConfig.MinPerEpochChurnLimitElectra = 256000000000 // Time parameters. e2eConfig.SecondsPerSlot = 6 diff --git a/runtime/interop/generate_genesis_state.go b/runtime/interop/generate_genesis_state.go index 3dd02cd1b9..dbf6ec9dca 100644 --- a/runtime/interop/generate_genesis_state.go +++ b/runtime/interop/generate_genesis_state.go @@ -6,6 +6,7 @@ import ( "context" "sync" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/pkg/errors" "github.com/prysmaticlabs/prysm/v5/async" "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/signing" @@ -171,7 +172,7 @@ func createDepositData(privKey bls.SecretKey, pubKey bls.PublicKey, withExecCred if withExecCreds { newCredentials := make([]byte, 12) newCredentials[0] = params.BeaconConfig().ETH1AddressWithdrawalPrefixByte - execAddr := bytesutil.ToBytes20(pubKey.Marshal()) + execAddr := bytesutil.ToBytes20(hexutil.MustDecode(executionAddress)) depositMessage.WithdrawalCredentials = append(newCredentials, execAddr[:]...) } sr, err := depositMessage.HashTreeRoot() diff --git a/runtime/interop/genesis.go b/runtime/interop/genesis.go index 71cbd0084b..5aac961d2e 100644 --- a/runtime/interop/genesis.go +++ b/runtime/interop/genesis.go @@ -184,8 +184,10 @@ func GethTestnetGenesis(genesisTime uint64, cfg *clparams.BeaconChainConfig) *co Mixhash: common.HexToHash(defaultMixhash), Coinbase: common.HexToAddress(defaultCoinbase), Alloc: types.GenesisAlloc{ - da.Address: da.Account, - ma.Address: ma.Account, + da.Address: da.Account, + ma.Address: ma.Account, + params.WithdrawalQueueAddress: {Nonce: 1, Code: params.WithdrawalQueueCode, Balance: common.Big0}, + params.ConsolidationQueueAddress: {Nonce: 1, Code: params.ConsolidationQueueCode, Balance: common.Big0}, }, ParentHash: common.HexToHash(defaultParenthash), } diff --git a/runtime/interop/premine-state.go b/runtime/interop/premine-state.go index 58802b4966..0a99e7ff0b 100644 --- a/runtime/interop/premine-state.go +++ b/runtime/interop/premine-state.go @@ -23,6 +23,8 @@ import ( var errUnsupportedVersion = errors.New("schema version not supported by PremineGenesisConfig") +const executionAddress = "0x878705ba3f8bc32fcf7f4caa1a35e72af65cf766" + type PremineGenesisConfig struct { GenesisTime uint64 NVals uint64 diff --git a/testing/endtoend/component_handler_test.go b/testing/endtoend/component_handler_test.go index 88e1ff430b..41ba3552b5 100644 --- a/testing/endtoend/component_handler_test.go +++ b/testing/endtoend/component_handler_test.go @@ -28,7 +28,7 @@ type componentHandler struct { web3Signer e2etypes.ComponentRunner bootnode e2etypes.ComponentRunner eth1Miner e2etypes.ComponentRunner - txGen e2etypes.ComponentRunner + txGen *eth1.TransactionGenerator builders e2etypes.MultipleComponentRunners eth1Proxy e2etypes.MultipleComponentRunners eth1Nodes e2etypes.MultipleComponentRunners diff --git a/testing/endtoend/components/eth1/BUILD.bazel b/testing/endtoend/components/eth1/BUILD.bazel index 93facd8b3b..8c89bae5d1 100644 --- a/testing/endtoend/components/eth1/BUILD.bazel +++ b/testing/endtoend/components/eth1/BUILD.bazel @@ -33,9 +33,11 @@ go_library( "@com_github_ethereum_go_ethereum//accounts/keystore:go_default_library", "@com_github_ethereum_go_ethereum//common:go_default_library", "@com_github_ethereum_go_ethereum//core/types:go_default_library", + "@com_github_ethereum_go_ethereum//crypto:go_default_library", "@com_github_ethereum_go_ethereum//crypto/kzg4844:go_default_library", "@com_github_ethereum_go_ethereum//ethclient:go_default_library", "@com_github_ethereum_go_ethereum//ethclient/gethclient:go_default_library", + "@com_github_ethereum_go_ethereum//params:go_default_library", "@com_github_ethereum_go_ethereum//rpc:go_default_library", "@com_github_holiman_uint256//:go_default_library", "@com_github_mariusvanderwijden_fuzzyvm//filler:go_default_library", diff --git a/testing/endtoend/components/eth1/transactions.go b/testing/endtoend/components/eth1/transactions.go index eb461d7f62..1f95da7446 100644 --- a/testing/endtoend/components/eth1/transactions.go +++ b/testing/endtoend/components/eth1/transactions.go @@ -16,15 +16,19 @@ import ( "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + gethCrypto "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto/kzg4844" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/ethclient/gethclient" + gethparams "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rpc" "github.com/holiman/uint256" "github.com/pkg/errors" fieldparams "github.com/prysmaticlabs/prysm/v5/config/fieldparams" "github.com/prysmaticlabs/prysm/v5/config/params" "github.com/prysmaticlabs/prysm/v5/crypto/rand" + "github.com/prysmaticlabs/prysm/v5/encoding/bytesutil" + "github.com/prysmaticlabs/prysm/v5/runtime/interop" e2e "github.com/prysmaticlabs/prysm/v5/testing/endtoend/params" "github.com/sirupsen/logrus" "golang.org/x/sync/errgroup" @@ -32,14 +36,23 @@ import ( const txCount = 20 +type txType int + +const ( + RandomTx txType = iota + ConsolidationTx + WithdrawalTx +) + var fundedAccount *keystore.Key type TransactionGenerator struct { - keystore string - seed int64 - started chan struct{} - cancel context.CancelFunc - paused bool + keystore string + seed int64 + started chan struct{} + cancel context.CancelFunc + paused bool + txGenType txType } func (t *TransactionGenerator) UnderlyingProcess() *os.Process { @@ -49,7 +62,7 @@ func (t *TransactionGenerator) UnderlyingProcess() *os.Process { } func NewTransactionGenerator(keystore string, seed int64) *TransactionGenerator { - return &TransactionGenerator{keystore: keystore, seed: seed} + return &TransactionGenerator{keystore: keystore, seed: seed, txGenType: RandomTx} } func (t *TransactionGenerator) Start(ctx context.Context) error { @@ -105,10 +118,26 @@ func (t *TransactionGenerator) Start(ctx context.Context) error { continue } backend := ethclient.NewClient(client) - err = SendTransaction(client, mineKey.PrivateKey, f, gasPrice, mineKey.Address.String(), txCount, backend, false) - if err != nil { - return err + switch t.txGenType { + case ConsolidationTx: + err = SendConsolidationTransaction(mineKey.PrivateKey, gasPrice, backend) + if err != nil { + return err + } + case WithdrawalTx: + err = SendWithdrawalTransaction(mineKey.PrivateKey, newKey.PrivateKey, gasPrice, backend) + if err != nil { + return err + } + case RandomTx: + err = SendTransaction(client, mineKey.PrivateKey, f, gasPrice, mineKey.Address.String(), txCount, backend, false) + if err != nil { + return err + } + default: + logrus.Warnf("Unknown transaction type: %v", t.txGenType) } + backend.Close() } } @@ -219,6 +248,195 @@ func SendTransaction(client *rpc.Client, key *ecdsa.PrivateKey, f *filler.Filler return nil } +func SendConsolidationTransaction(key *ecdsa.PrivateKey, gasPrice *big.Int, backend *ethclient.Client) error { + totalCreds := e2e.TestParams.NumberOfExecutionCreds + _, pKeys, err := interop.DeterministicallyGenerateKeys(0, totalCreds) + if err != nil { + return err + } + compoundedKey := pKeys[len(pKeys)-1].Marshal() + + // Create compounding credentials + if err := createAndSendConsolidation(compoundedKey, compoundedKey, key, gasPrice, backend); err != nil { + return err + } + for _, k := range pKeys { + if err := createAndSendConsolidation(k.Marshal(), compoundedKey, key, gasPrice, backend); err != nil { + return err + } + } + + // Junk Requests + for i := 0; i < 2; i++ { + sourcePubkey := [48]byte{byte(i), 0xFF, 0x34, 0xEE} + targetPubkey := compoundedKey + if err := createAndSendConsolidation(sourcePubkey[:], targetPubkey, key, gasPrice, backend); err != nil { + return err + } + } + return nil +} + +func createAndSendConsolidation(sourceKey, targetKey []byte, key *ecdsa.PrivateKey, gasPrice *big.Int, backend *ethclient.Client) error { + publicKey := key.Public().(*ecdsa.PublicKey) + fromAddress := gethCrypto.PubkeyToAddress(*publicKey) + // Get nonce + nonce, err := backend.PendingNonceAt(context.Background(), fromAddress) + if err != nil { + return err + } + chainid, err := backend.ChainID(context.Background()) + if err != nil { + return err + } + gasLimit := uint64(200000) + + sourcePubkey := sourceKey + targetPubkey := targetKey + + consolidationData := []byte{} + consolidationData = append(consolidationData, sourcePubkey...) + consolidationData = append(consolidationData, targetPubkey...) + + ret, err := backend.CallContract(context.Background(), ethereum.CallMsg{To: &gethparams.ConsolidationQueueAddress}, nil) + if err != nil { + return errors.Wrapf(err, "%s", string(ret)) + } + fee := new(big.Int).SetBytes(ret) + fee = fee.Mul(fee, big.NewInt(2)) + + // Create transaction + tx := types.NewTx(&types.LegacyTx{ + Nonce: nonce, + To: &gethparams.ConsolidationQueueAddress, + Value: fee, + Gas: gasLimit, + GasPrice: gasPrice, + Data: consolidationData, + }) + + // Sign transaction + signedTx, err := types.SignTx(tx, types.NewCancunSigner(chainid), key) + if err != nil { + return err + } + return backend.SendTransaction(context.Background(), signedTx) +} + +func SendWithdrawalTransaction(key, newKey *ecdsa.PrivateKey, gasPrice *big.Int, backend *ethclient.Client) error { + totalCreds := e2e.TestParams.NumberOfExecutionCreds + _, pKeys, err := interop.DeterministicallyGenerateKeys(0, totalCreds) + if err != nil { + return err + } + compoundedKey := pKeys[len(pKeys)-1].Marshal() + + _, invalidWithdrawalKeys, err := interop.DeterministicallyGenerateKeys(totalCreds, totalCreds+4) + if err != nil { + return err + } + + publicKey := key.Public().(*ecdsa.PublicKey) + fromAddress := gethCrypto.PubkeyToAddress(*publicKey) + nonce, err := backend.PendingNonceAt(context.Background(), fromAddress) + if err != nil { + return err + } + + var withdrawalTxs []*types.Transaction + // Create Withdrawal for compounded key. + tx, err := createWithdrawal(compoundedKey, 0, nonce, key, gasPrice, backend) + if err != nil { + return err + } + withdrawalTxs = append(withdrawalTxs, tx) + nonce++ + + rGen := rand.NewDeterministicGenerator() + for _, k := range pKeys { + tx, err := createWithdrawal(k.Marshal(), uint64(rGen.Int63n(32000000000)), nonce, key, gasPrice, backend) + if err != nil { + return err + } + withdrawalTxs = append(withdrawalTxs, tx) + nonce++ + } + + // Junk Requests + for _, k := range invalidWithdrawalKeys { + tx, err := createWithdrawal(k.Marshal(), uint64(rGen.Int63n(32000000000)), nonce, newKey, gasPrice, backend) + if err != nil { + return err + } + withdrawalTxs = append(withdrawalTxs, tx) + nonce++ + } + currExecHead := uint64(0) + // Batch And Send Withdrawals + for len(withdrawalTxs) > 0 { + currBlock, err := backend.BlockNumber(context.Background()) + if err != nil { + return err + } + if currBlock > currExecHead { + currExecHead = currBlock + maxWithdrawalPerPayload := params.BeaconConfig().MaxWithdrawalRequestsPerPayload + if maxWithdrawalPerPayload > uint64(len(withdrawalTxs)) { + maxWithdrawalPerPayload = uint64(len(withdrawalTxs)) + } + for _, tx := range withdrawalTxs[:maxWithdrawalPerPayload] { + if err := backend.SendTransaction(context.Background(), tx); err != nil { + return err + } + } + // Shift slice to only have unsent transactions + withdrawalTxs = withdrawalTxs[maxWithdrawalPerPayload:] + time.Sleep(2 * time.Second) + } + } + return nil +} + +func createWithdrawal(sourceKey []byte, amount, nonce uint64, key *ecdsa.PrivateKey, gasPrice *big.Int, backend *ethclient.Client) (*types.Transaction, error) { + chainid, err := backend.ChainID(context.Background()) + if err != nil { + return nil, err + } + gasLimit := uint64(200000) + + withdrawalData := []byte{} + withdrawalData = append(withdrawalData, sourceKey...) + withdrawalData = append(withdrawalData, bytesutil.Uint64ToBytesBigEndian(amount)...) + + ret, err := backend.CallContract(context.Background(), ethereum.CallMsg{To: &gethparams.WithdrawalQueueAddress}, nil) + if err != nil { + return nil, errors.Wrapf(err, "%s", string(ret)) + } + fee := new(big.Int).SetBytes(ret) + fee = fee.Mul(fee, big.NewInt(2)) + + // Create transaction + tx := types.NewTx(&types.LegacyTx{ + Nonce: nonce, + To: &gethparams.WithdrawalQueueAddress, + Value: fee, + Gas: gasLimit, + GasPrice: gasPrice, + Data: withdrawalData, + }) + + // Sign transaction + signedTx, err := types.SignTx(tx, types.NewCancunSigner(chainid), key) + if err != nil { + return nil, err + } + return signedTx, nil +} + +func (t *TransactionGenerator) SetTxType(typ txType) { + t.txGenType = typ +} + // Pause pauses the component and its underlying process. func (t *TransactionGenerator) Pause() error { t.paused = true diff --git a/testing/endtoend/endtoend_setup_test.go b/testing/endtoend/endtoend_setup_test.go index c54d6fb6dd..56c3bbad52 100644 --- a/testing/endtoend/endtoend_setup_test.go +++ b/testing/endtoend/endtoend_setup_test.go @@ -73,17 +73,18 @@ func e2eMinimal(t *testing.T, cfg *params.BeaconChainConfig, cfgo ...types.E2ECo "--enable-tracing", "--trace-sample-fraction=1.0", }, - ValidatorFlags: []string{}, - EpochsToRun: uint64(epochsToRun), - TestSync: true, - TestFeature: true, - TestDeposits: true, - UsePrysmShValidator: false, - UsePprof: true, - TracingSinkEndpoint: tracingEndpoint, - Evaluators: evals, - EvalInterceptor: defaultInterceptor, - Seed: int64(seed), + ValidatorFlags: []string{}, + EpochsToRun: uint64(epochsToRun), + TestSync: true, + TestFeature: true, + TestDeposits: true, + UsePrysmShValidator: false, + UsePprof: true, + TestExecutionRequests: true, + TracingSinkEndpoint: tracingEndpoint, + Evaluators: evals, + EvalInterceptor: defaultInterceptor, + Seed: int64(seed), } for _, o := range cfgo { o(testConfig) @@ -149,18 +150,19 @@ func e2eMainnet(t *testing.T, usePrysmSh, useMultiClient bool, cfg *params.Beaco "--enable-tracing", "--trace-sample-fraction=1.0", }, - ValidatorFlags: []string{}, - EpochsToRun: uint64(epochsToRun), - TestSync: true, - TestFeature: true, - TestDeposits: true, - UseFixedPeerIDs: true, - UsePrysmShValidator: usePrysmSh, - UsePprof: true, - TracingSinkEndpoint: tracingEndpoint, - Evaluators: evals, - EvalInterceptor: defaultInterceptor, - Seed: int64(seed), + ValidatorFlags: []string{}, + EpochsToRun: uint64(epochsToRun), + TestSync: true, + TestFeature: true, + TestDeposits: true, + UseFixedPeerIDs: true, + UsePrysmShValidator: usePrysmSh, + TestExecutionRequests: true, + UsePprof: true, + TracingSinkEndpoint: tracingEndpoint, + Evaluators: evals, + EvalInterceptor: defaultInterceptor, + Seed: int64(seed), } for _, o := range cfgo { o(testConfig) diff --git a/testing/endtoend/endtoend_test.go b/testing/endtoend/endtoend_test.go index 2529ef1353..29c8a256b3 100644 --- a/testing/endtoend/endtoend_test.go +++ b/testing/endtoend/endtoend_test.go @@ -497,13 +497,15 @@ func (r *testRunner) defaultEndToEndRun() error { require.NoError(t, err) tickingStartTime := helpers.EpochTickerStartTime(genesis) - ec := e2etypes.NewEvaluationContext(r.depositor.History()) + ec, err := e2etypes.NewEvaluationContext(r.depositor.History(), e2e.TestParams.NumberOfExecutionCreds) + require.NoError(t, err) + // Run assigned evaluators. if err := r.runEvaluators(ec, conns, tickingStartTime); err != nil { return errors.Wrap(err, "one or more evaluators failed") } // Test execution request processing in electra. - if r.config.TestDeposits && params.ElectraEnabled() { + if r.config.TestDeposits && r.config.TestExecutionRequests && params.ElectraEnabled() { if err := r.comHandler.txGen.Pause(); err != nil { r.t.Error(err) } @@ -516,6 +518,33 @@ func (r *testRunner) defaultEndToEndRun() error { } } + if r.config.TestExecutionRequests && params.ElectraEnabled() { + // Test Consolidation Transactions + r.comHandler.txGen.SetTxType(eth1.ConsolidationTx) + // Wait For an epoch before running evaluator + secondsPerEpoch := time.Duration(params.BeaconConfig().SlotsPerEpoch.Mul(params.BeaconConfig().SecondsPerSlot)) + waitForSync := secondsPerEpoch * time.Second + time.Sleep(waitForSync) + + for _, evaluator := range []e2etypes.Evaluator{ev.ValidatorsHaveConsolidated} { + t.Run(evaluator.Name, func(t *testing.T) { + assert.NoError(t, evaluator.Evaluation(ec, conns...), "Evaluation failed for sync node") + }) + } + + // Test Withdrawal Transactions + r.comHandler.txGen.SetTxType(eth1.WithdrawalTx) + // Wait For an epoch before running evaluator + time.Sleep(waitForSync) + + for _, evaluator := range []e2etypes.Evaluator{ev.ValidatorsHaveWithdrawnViaExecution} { + t.Run(evaluator.Name, func(t *testing.T) { + assert.NoError(t, evaluator.Evaluation(ec, conns...), "Evaluation failed for sync node") + }) + } + r.comHandler.txGen.SetTxType(eth1.RandomTx) + } + index := e2e.TestParams.BeaconNodeCount + e2e.TestParams.LighthouseBeaconNodeCount if config.TestSync { if err := r.testBeaconChainSync(ctx, g, conns, tickingStartTime, bootNode.ENR(), eth1Miner.ENR()); err != nil { @@ -596,7 +625,8 @@ func (r *testRunner) scenarioRun() error { require.NoError(t, err) tickingStartTime := helpers.EpochTickerStartTime(genesis) - ec := e2etypes.NewEvaluationContext(r.depositor.History()) + ec, err := e2etypes.NewEvaluationContext(r.depositor.History(), e2e.TestParams.NumberOfExecutionCreds) + require.NoError(t, err) // Run assigned evaluators. return r.runEvaluators(ec, conns, tickingStartTime) } diff --git a/testing/endtoend/evaluators/operations.go b/testing/endtoend/evaluators/operations.go index ec19c89adc..8a021f5a95 100644 --- a/testing/endtoend/evaluators/operations.go +++ b/testing/endtoend/evaluators/operations.go @@ -110,6 +110,26 @@ var ValidatorsHaveWithdrawn = e2etypes.Evaluator{ Evaluation: validatorsAreWithdrawn, } +// ValidatorsHaveConsolidated checks the beacon state for the consolidated validator and ensures it is exited. +var ValidatorsHaveConsolidated = e2etypes.Evaluator{ + Name: "validators_have_consolidated_%d", + Policy: func(currentEpoch primitives.Epoch) bool { + fEpoch := params.BeaconConfig().ElectraForkEpoch + return policies.OnwardsNthEpoch(fEpoch)(currentEpoch) + }, + Evaluation: validatorsHaveBeenConsolidated, +} + +// ValidatorsHaveWithdrawnViaExecution checks the beacon state for the compounded validator and makes sure it is withdrawn. +var ValidatorsHaveWithdrawnViaExecution = e2etypes.Evaluator{ + Name: "validators_have_withdrawn_with_execution_%d", + Policy: func(currentEpoch primitives.Epoch) bool { + fEpoch := params.BeaconConfig().ElectraForkEpoch + return policies.OnwardsNthEpoch(fEpoch)(currentEpoch) + }, + Evaluation: validatorsHaveBeenWithdrawnWithExecution, +} + // ValidatorsVoteWithTheMajority verifies whether validator vote for eth1data using the majority algorithm. var ValidatorsVoteWithTheMajority = e2etypes.Evaluator{ Name: "validators_vote_with_the_majority_%d", @@ -438,7 +458,7 @@ func proposeVoluntaryExit(ec *e2etypes.EvaluationContext, conns ...*grpc.ClientC // Send an exit for a non-exited validator. for i := 0; i < numOfExits; { randIndex := primitives.ValidatorIndex(rand.Uint64() % params.BeaconConfig().MinGenesisActiveValidatorCount) - if ec.ExitedVals[bytesutil.ToBytes48(privKeys[randIndex].PublicKey().Marshal())] { + if ec.ExitedVals[bytesutil.ToBytes48(privKeys[randIndex].PublicKey().Marshal())] || ec.ValidExecutionCredentials[[48]byte(privKeys[randIndex].PublicKey().Marshal())] { continue } if err := sendExit(randIndex); err != nil { @@ -697,3 +717,98 @@ func validatorsAreWithdrawn(ec *e2etypes.EvaluationContext, conns ...*grpc.Clien } return nil } + +func validatorsHaveBeenConsolidated(ec *e2etypes.EvaluationContext, conns ...*grpc.ClientConn) error { + conn := conns[0] + beaconClient := ethpb.NewBeaconChainClient(conn) + debugClient := ethpb.NewDebugClient(conn) + + ctx := context.Background() + chainHead, err := beaconClient.GetChainHead(ctx, &emptypb.Empty{}) + if err != nil { + return errors.Wrap(err, "could not get chain head") + } + stObj, err := debugClient.GetBeaconState(ctx, ðpb.BeaconStateRequest{QueryFilter: ðpb.BeaconStateRequest_Slot{Slot: chainHead.HeadSlot}}) + if err != nil { + return errors.Wrap(err, "could not get state object") + } + versionedMarshaler, err := detect.FromState(stObj.Encoded) + if err != nil { + return errors.Wrap(err, "could not get state marshaler") + } + st, err := versionedMarshaler.UnmarshalBeaconState(stObj.Encoded) + if err != nil { + return errors.Wrap(err, "could not get state") + } + + var compoundingVal int + + for pubkey := range ec.ValidExecutionCredentials { + if ec.ExitedVals[pubkey] { + continue + } + valIdx, ok := st.ValidatorIndexByPubkey(pubkey) + if !ok { + return errors.Errorf("pubkey %#x does not exist in our state", pubkey) + } + val, err := st.ValidatorAtIndexReadOnly(valIdx) + if err != nil { + return err + } + if val.HasCompoundingWithdrawalCredentials() { + compoundingVal++ + continue + } + if val.ExitEpoch() == params.BeaconConfig().FarFutureEpoch { + return errors.Errorf("validator was not exited after consolidation, its exit epoch is %d", val.ExitEpoch()) + } + } + if compoundingVal != 1 { + return errors.Errorf("no compounding validators observed, wanted 1 but got %d", compoundingVal) + } + return nil +} + +func validatorsHaveBeenWithdrawnWithExecution(ec *e2etypes.EvaluationContext, conns ...*grpc.ClientConn) error { + conn := conns[0] + beaconClient := ethpb.NewBeaconChainClient(conn) + debugClient := ethpb.NewDebugClient(conn) + + ctx := context.Background() + chainHead, err := beaconClient.GetChainHead(ctx, &emptypb.Empty{}) + if err != nil { + return errors.Wrap(err, "could not get chain head") + } + stObj, err := debugClient.GetBeaconState(ctx, ðpb.BeaconStateRequest{QueryFilter: ðpb.BeaconStateRequest_Slot{Slot: chainHead.HeadSlot}}) + if err != nil { + return errors.Wrap(err, "could not get state object") + } + versionedMarshaler, err := detect.FromState(stObj.Encoded) + if err != nil { + return errors.Wrap(err, "could not get state marshaler") + } + st, err := versionedMarshaler.UnmarshalBeaconState(stObj.Encoded) + if err != nil { + return errors.Wrap(err, "could not get state") + } + + for pubkey := range ec.ValidExecutionCredentials { + if ec.ExitedVals[pubkey] { + continue + } + valIdx, ok := st.ValidatorIndexByPubkey(pubkey) + if !ok { + return errors.Errorf("pubkey %#x does not exist in our state", pubkey) + } + val, err := st.ValidatorAtIndexReadOnly(valIdx) + if err != nil { + return err + } + if val.HasCompoundingWithdrawalCredentials() { + if val.ExitEpoch() == params.BeaconConfig().FarFutureEpoch { + return errors.Errorf("validator was not exited after withdrawal, its exit epoch is %d", val.ExitEpoch()) + } + } + } + return nil +} diff --git a/testing/endtoend/types/BUILD.bazel b/testing/endtoend/types/BUILD.bazel index 1d167e5c3b..3aebf1f5e2 100644 --- a/testing/endtoend/types/BUILD.bazel +++ b/testing/endtoend/types/BUILD.bazel @@ -13,6 +13,7 @@ go_library( deps = [ "//config/params:go_default_library", "//consensus-types/primitives:go_default_library", + "//runtime/interop:go_default_library", "//runtime/version:go_default_library", "@org_golang_google_grpc//:go_default_library", ], diff --git a/testing/endtoend/types/types.go b/testing/endtoend/types/types.go index 62fdff879e..a2bf580ccf 100644 --- a/testing/endtoend/types/types.go +++ b/testing/endtoend/types/types.go @@ -8,6 +8,7 @@ import ( "github.com/prysmaticlabs/prysm/v5/config/params" "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" + "github.com/prysmaticlabs/prysm/v5/runtime/interop" "github.com/prysmaticlabs/prysm/v5/runtime/version" "google.golang.org/grpc" ) @@ -71,6 +72,7 @@ type E2EConfig struct { UseValidatorCrossClient bool UseBeaconRestApi bool UseBuilder bool + TestExecutionRequests bool EpochsToRun uint64 Seed int64 TracingSinkEndpoint string @@ -129,18 +131,28 @@ type DepositBalancer interface { // EvaluationContext allows for additional data to be provided to evaluators that need extra state. type EvaluationContext struct { DepositBalancer - ExitedVals map[[48]byte]bool - SeenVotes map[primitives.Slot][]byte - ExpectedEth1DataVote []byte + ExitedVals map[[48]byte]bool + SeenVotes map[primitives.Slot][]byte + ExpectedEth1DataVote []byte + ValidExecutionCredentials map[[48]byte]bool } // NewEvaluationContext handles initializing internal datastructures (like maps) provided by the EvaluationContext. -func NewEvaluationContext(d DepositBalancer) *EvaluationContext { - return &EvaluationContext{ - DepositBalancer: d, - ExitedVals: make(map[[48]byte]bool), - SeenVotes: make(map[primitives.Slot][]byte), +func NewEvaluationContext(d DepositBalancer, numExecCreds uint64) (*EvaluationContext, error) { + _, pkeys, err := interop.DeterministicallyGenerateKeys(0, numExecCreds) + if err != nil { + return nil, err } + execMap := make(map[[48]byte]bool) + for _, k := range pkeys { + execMap[[48]byte(k.Marshal())] = true + } + return &EvaluationContext{ + DepositBalancer: d, + ExitedVals: make(map[[48]byte]bool), + SeenVotes: make(map[primitives.Slot][]byte), + ValidExecutionCredentials: execMap, + }, nil } // ComponentRunner defines an interface via which E2E component's configuration, execution and termination is managed.