package builder import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "strconv" "testing" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/gorilla/mux" mock "github.com/prysmaticlabs/prysm/v4/beacon-chain/blockchain/testing" "github.com/prysmaticlabs/prysm/v4/beacon-chain/rpc/testutil" "github.com/prysmaticlabs/prysm/v4/beacon-chain/state" "github.com/prysmaticlabs/prysm/v4/config/params" "github.com/prysmaticlabs/prysm/v4/consensus-types/primitives" "github.com/prysmaticlabs/prysm/v4/crypto/bls" http2 "github.com/prysmaticlabs/prysm/v4/network/http" eth "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1" "github.com/prysmaticlabs/prysm/v4/testing/assert" "github.com/prysmaticlabs/prysm/v4/testing/require" "github.com/prysmaticlabs/prysm/v4/testing/util" "github.com/prysmaticlabs/prysm/v4/time/slots" ) func TestExpectedWithdrawals_BadRequest(t *testing.T) { st, err := util.NewBeaconStateCapella() slotsAhead := 5000 require.NoError(t, err) capellaSlot, err := slots.EpochStart(params.BeaconConfig().CapellaForkEpoch) require.NoError(t, err) currentSlot := capellaSlot + primitives.Slot(slotsAhead) require.NoError(t, st.SetSlot(currentSlot)) mockChainService := &mock.ChainService{Optimistic: true} testCases := []struct { name string path string urlParams map[string]string state state.BeaconState errorMessage string }{ { name: "no state_id url params", path: "/eth/v1/builder/states/{state_id}/expected_withdrawals?proposal_slot" + strconv.FormatUint(uint64(currentSlot), 10), urlParams: map[string]string{}, state: nil, errorMessage: "state_id is required in URL params", }, { name: "invalid proposal slot value", path: "/eth/v1/builder/states/{state_id}/expected_withdrawals?proposal_slot=aaa", urlParams: map[string]string{"state_id": "head"}, state: st, errorMessage: "invalid proposal slot value", }, { name: "proposal slot < Capella start slot", path: "/eth/v1/builder/states/{state_id}/expected_withdrawals?proposal_slot=" + strconv.FormatUint(uint64(capellaSlot)-1, 10), urlParams: map[string]string{"state_id": "head"}, state: st, errorMessage: "expected withdrawals are not supported before Capella fork", }, { name: "proposal slot == Capella start slot", path: "/eth/v1/builder/states/{state_id}/expected_withdrawals?proposal_slot=" + strconv.FormatUint(uint64(capellaSlot), 10), urlParams: map[string]string{"state_id": "head"}, state: st, errorMessage: "proposal slot must be bigger than state slot", }, { name: "Proposal slot >= 128 slots ahead of state slot", path: "/eth/v1/builder/states/{state_id}/expected_withdrawals?proposal_slot=" + strconv.FormatUint(uint64(currentSlot+128), 10), urlParams: map[string]string{"state_id": "head"}, state: st, errorMessage: "proposal slot cannot be >= 128 slots ahead of state slot", }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { s := &Server{ FinalizationFetcher: mockChainService, OptimisticModeFetcher: mockChainService, Stater: &testutil.MockStater{BeaconState: testCase.state}, } request := httptest.NewRequest("GET", testCase.path, nil) request = mux.SetURLVars(request, testCase.urlParams) writer := httptest.NewRecorder() writer.Body = &bytes.Buffer{} s.ExpectedWithdrawals(writer, request) assert.Equal(t, http.StatusBadRequest, writer.Code) e := &http2.DefaultErrorJson{} require.NoError(t, json.Unmarshal(writer.Body.Bytes(), e)) assert.Equal(t, http.StatusBadRequest, e.Code) assert.StringContains(t, testCase.errorMessage, e.Message) }) } } func TestExpectedWithdrawals(t *testing.T) { st, err := util.NewBeaconStateCapella() slotsAhead := 5000 require.NoError(t, err) capellaSlot, err := slots.EpochStart(params.BeaconConfig().CapellaForkEpoch) require.NoError(t, err) currentSlot := capellaSlot + primitives.Slot(slotsAhead) require.NoError(t, st.SetSlot(currentSlot)) mockChainService := &mock.ChainService{Optimistic: true} t.Run("get correct expected withdrawals", func(t *testing.T) { params.SetupTestConfigCleanup(t) cfg := params.BeaconConfig().Copy() cfg.MaxValidatorsPerWithdrawalsSweep = 16 params.OverrideBeaconConfig(cfg) // Update state with updated validator fields valCount := 17 validators := make([]*eth.Validator, 0, valCount) balances := make([]uint64, 0, valCount) for i := 0; i < valCount; i++ { blsKey, err := bls.RandKey() require.NoError(t, err) val := ð.Validator{ PublicKey: blsKey.PublicKey().Marshal(), WithdrawalCredentials: make([]byte, 32), ExitEpoch: params.BeaconConfig().FarFutureEpoch, WithdrawableEpoch: params.BeaconConfig().FarFutureEpoch, EffectiveBalance: params.BeaconConfig().MaxEffectiveBalance, } val.WithdrawalCredentials[0] = params.BeaconConfig().ETH1AddressWithdrawalPrefixByte validators = append(validators, val) balances = append(balances, params.BeaconConfig().MaxEffectiveBalance) } epoch := slots.ToEpoch(st.Slot()) // Fully withdrawable now with more than 0 balance validators[5].WithdrawableEpoch = epoch // Fully withdrawable now but 0 balance validators[10].WithdrawableEpoch = epoch balances[10] = 0 // Partially withdrawable now but fully withdrawable after 1 epoch validators[14].WithdrawableEpoch = epoch + 1 balances[14] += params.BeaconConfig().MinDepositAmount // Partially withdrawable validators[15].WithdrawableEpoch = epoch + 2 balances[15] += params.BeaconConfig().MinDepositAmount // Above sweep bound validators[16].WithdrawableEpoch = epoch + 1 balances[16] += params.BeaconConfig().MinDepositAmount require.NoError(t, st.SetValidators(validators)) require.NoError(t, st.SetBalances(balances)) inactivityScores := make([]uint64, valCount) for i := range inactivityScores { inactivityScores[i] = 10 } require.NoError(t, st.SetInactivityScores(inactivityScores)) s := &Server{ FinalizationFetcher: mockChainService, OptimisticModeFetcher: mockChainService, Stater: &testutil.MockStater{BeaconState: st}, } request := httptest.NewRequest( "GET", "/eth/v1/builder/states/{state_id}/expected_withdrawals?proposal_slot="+ strconv.FormatUint(uint64(currentSlot+params.BeaconConfig().SlotsPerEpoch), 10), nil) request = mux.SetURLVars(request, map[string]string{"state_id": "head"}) writer := httptest.NewRecorder() writer.Body = &bytes.Buffer{} s.ExpectedWithdrawals(writer, request) assert.Equal(t, http.StatusOK, writer.Code) resp := &ExpectedWithdrawalsResponse{} require.NoError(t, json.Unmarshal(writer.Body.Bytes(), resp)) assert.Equal(t, true, resp.ExecutionOptimistic) assert.Equal(t, false, resp.Finalized) assert.Equal(t, 3, len(resp.Data)) expectedWithdrawal1 := &ExpectedWithdrawal{ Index: strconv.FormatUint(0, 10), ValidatorIndex: strconv.FormatUint(5, 10), Address: hexutil.Encode(validators[5].WithdrawalCredentials[12:]), // Decreased due to epoch processing when state advanced forward Amount: strconv.FormatUint(31998257885, 10), } expectedWithdrawal2 := &ExpectedWithdrawal{ Index: strconv.FormatUint(1, 10), ValidatorIndex: strconv.FormatUint(14, 10), Address: hexutil.Encode(validators[14].WithdrawalCredentials[12:]), // MaxEffectiveBalance + MinDepositAmount + decrease after epoch processing Amount: strconv.FormatUint(32998257885, 10), } expectedWithdrawal3 := &ExpectedWithdrawal{ Index: strconv.FormatUint(2, 10), ValidatorIndex: strconv.FormatUint(15, 10), Address: hexutil.Encode(validators[15].WithdrawalCredentials[12:]), // MinDepositAmount + decrease after epoch processing Amount: strconv.FormatUint(998257885, 10), } require.DeepEqual(t, expectedWithdrawal1, resp.Data[0]) require.DeepEqual(t, expectedWithdrawal2, resp.Data[1]) require.DeepEqual(t, expectedWithdrawal3, resp.Data[2]) }) }