From 13da86859125005fb880862d52a2bd85a67d2779 Mon Sep 17 00:00:00 2001 From: Giulio rebuffo Date: Mon, 1 Jan 2024 22:18:11 +0100 Subject: [PATCH] Added RANDAO Api (#9108) --- cl/beacon/handler/committees.go | 8 +- cl/beacon/handler/duties_attester.go | 163 ++++++++++++++++++ cl/beacon/handler/duties_attester_test.go | 151 ++++++++++++++++ cl/beacon/handler/duties_sync.go | 146 ++++++++++++++++ cl/beacon/handler/duties_sync_test.go | 85 +++++++++ cl/beacon/handler/format.go | 8 + cl/beacon/handler/handler.go | 14 +- cl/beacon/handler/liveness.go | 153 ++++++++++++++++ cl/beacon/handler/liveness_test.go | 62 +++++++ cl/beacon/handler/rewards.go | 7 +- cl/beacon/handler/states.go | 60 +++++++ cl/beacon/handler/states_test.go | 57 ++++++ cl/cltypes/solid/bitlist.go | 7 + cl/phase1/core/state/raw/getters.go | 4 + cl/phase1/forkchoice/fork_choice_test.go | 23 +++ .../forkchoice/fork_graph/fork_graph_disk.go | 14 +- cl/phase1/forkchoice/fork_graph/interface.go | 1 + cl/phase1/forkchoice/forkchoice.go | 82 ++++++++- cl/phase1/forkchoice/forkchoice_mock.go | 14 ++ cl/phase1/forkchoice/interface.go | 3 + cl/phase1/forkchoice/on_block.go | 12 ++ 21 files changed, 1058 insertions(+), 16 deletions(-) create mode 100644 cl/beacon/handler/duties_attester.go create mode 100644 cl/beacon/handler/duties_attester_test.go create mode 100644 cl/beacon/handler/duties_sync.go create mode 100644 cl/beacon/handler/duties_sync_test.go create mode 100644 cl/beacon/handler/liveness.go create mode 100644 cl/beacon/handler/liveness_test.go diff --git a/cl/beacon/handler/committees.go b/cl/beacon/handler/committees.go index c506e9be396..0ab70fc29cd 100644 --- a/cl/beacon/handler/committees.go +++ b/cl/beacon/handler/committees.go @@ -67,8 +67,8 @@ func (a *ApiHandler) getCommittees(w http.ResponseWriter, r *http.Request) (*bea return nil, beaconhttp.NewEndpointError(http.StatusBadRequest, fmt.Sprintf("slot %d is not in epoch %d", *slotFilter, epoch)) } resp := make([]*committeeResponse, 0, a.beaconChainCfg.SlotsPerEpoch*a.beaconChainCfg.MaxCommitteesPerSlot) - - if a.forkchoiceStore.FinalizedSlot() <= slot { + isFinalized := slot <= a.forkchoiceStore.FinalizedSlot() + if a.forkchoiceStore.LowestAvaiableSlot() <= slot { // non-finality case s, cn := a.syncedData.HeadState() defer cn() @@ -100,7 +100,7 @@ func (a *ApiHandler) getCommittees(w http.ResponseWriter, r *http.Request) (*bea resp = append(resp, data) } } - return newBeaconResponse(resp).withFinalized(false), nil + return newBeaconResponse(resp).withFinalized(isFinalized), nil } // finality case activeIdxs, err := state_accessors.ReadActiveIndicies(tx, epoch*a.beaconChainCfg.SlotsPerEpoch) @@ -143,5 +143,5 @@ func (a *ApiHandler) getCommittees(w http.ResponseWriter, r *http.Request) (*bea resp = append(resp, data) } } - return newBeaconResponse(resp).withFinalized(true), nil + return newBeaconResponse(resp).withFinalized(isFinalized), nil } diff --git a/cl/beacon/handler/duties_attester.go b/cl/beacon/handler/duties_attester.go new file mode 100644 index 00000000000..a9f1045d0f9 --- /dev/null +++ b/cl/beacon/handler/duties_attester.go @@ -0,0 +1,163 @@ +package handler + +import ( + "encoding/json" + "fmt" + "net/http" + "strconv" + + libcommon "github.com/ledgerwatch/erigon-lib/common" + "github.com/ledgerwatch/erigon/cl/beacon/beaconhttp" + state_accessors "github.com/ledgerwatch/erigon/cl/persistence/state" + "github.com/ledgerwatch/erigon/cl/phase1/core/state" +) + +type attesterDutyResponse struct { + Pubkey libcommon.Bytes48 `json:"pubkey"` + ValidatorIndex uint64 `json:"validator_index,string"` + CommitteeIndex uint64 `json:"committee_index,string"` + CommitteeLength uint64 `json:"committee_length,string"` + ValidatorCommitteeIndex uint64 `json:"validator_committee_index,string"` + CommitteesAtSlot uint64 `json:"committees_at_slot,string"` + Slot uint64 `json:"slot,string"` +} + +func (a *ApiHandler) getAttesterDuties(w http.ResponseWriter, r *http.Request) (*beaconResponse, error) { + epoch, err := epochFromRequest(r) + if err != nil { + return nil, err + } + + var idxsStr []string + if err := json.NewDecoder(r.Body).Decode(&idxsStr); err != nil { + return nil, beaconhttp.NewEndpointError(http.StatusBadRequest, fmt.Errorf("could not decode request body: %w. request body is required", err).Error()) + } + if len(idxsStr) == 0 { + return newBeaconResponse([]string{}).withOptimistic(false), nil + } + idxSet := map[int]struct{}{} + // convert the request to uint64 + for _, idxStr := range idxsStr { + + idx, err := strconv.ParseUint(idxStr, 10, 64) + if err != nil { + return nil, beaconhttp.NewEndpointError(http.StatusBadRequest, fmt.Errorf("could not parse validator index: %w", err).Error()) + } + if _, ok := idxSet[int(idx)]; ok { + continue + } + idxSet[int(idx)] = struct{}{} + } + + tx, err := a.indiciesDB.BeginRo(r.Context()) + if err != nil { + return nil, err + } + defer tx.Rollback() + + resp := []attesterDutyResponse{} + + // get the duties + if a.forkchoiceStore.LowestAvaiableSlot() <= epoch*a.beaconChainCfg.SlotsPerEpoch { + // non-finality case + s, cn := a.syncedData.HeadState() + defer cn() + if s == nil { + return nil, beaconhttp.NewEndpointError(http.StatusServiceUnavailable, "node is syncing") + } + + if epoch > state.Epoch(s)+1 { + return nil, beaconhttp.NewEndpointError(http.StatusBadRequest, fmt.Sprintf("epoch %d is too far in the future", epoch)) + } + + // get active validator indicies + committeeCount := s.CommitteeCount(epoch) + // now start obtaining the committees from the head state + for currSlot := epoch * a.beaconChainCfg.SlotsPerEpoch; currSlot < (epoch+1)*a.beaconChainCfg.SlotsPerEpoch; currSlot++ { + for committeeIndex := uint64(0); committeeIndex < committeeCount; committeeIndex++ { + idxs, err := s.GetBeaconCommitee(currSlot, committeeIndex) + if err != nil { + return nil, err + } + for vIdx, idx := range idxs { + if _, ok := idxSet[int(idx)]; !ok { + continue + } + publicKey, err := s.ValidatorPublicKey(int(idx)) + if err != nil { + return nil, err + } + duty := attesterDutyResponse{ + Pubkey: publicKey, + ValidatorIndex: idx, + CommitteeIndex: committeeIndex, + CommitteeLength: uint64(len(idxs)), + ValidatorCommitteeIndex: uint64(vIdx), + CommitteesAtSlot: committeeCount, + Slot: currSlot, + } + resp = append(resp, duty) + } + } + } + return newBeaconResponse(resp).withOptimistic(false), nil + } + + stageStateProgress, err := state_accessors.GetStateProcessingProgress(tx) + if err != nil { + return nil, err + } + if (epoch)*a.beaconChainCfg.SlotsPerEpoch >= stageStateProgress { + return nil, beaconhttp.NewEndpointError(http.StatusBadRequest, fmt.Sprintf("epoch %d is too far in the future", epoch)) + } + // finality case + activeIdxs, err := state_accessors.ReadActiveIndicies(tx, epoch*a.beaconChainCfg.SlotsPerEpoch) + if err != nil { + return nil, err + } + + committeesPerSlot := uint64(len(activeIdxs)) / a.beaconChainCfg.SlotsPerEpoch / a.beaconChainCfg.TargetCommitteeSize + if a.beaconChainCfg.MaxCommitteesPerSlot < committeesPerSlot { + committeesPerSlot = a.beaconChainCfg.MaxCommitteesPerSlot + } + if committeesPerSlot < 1 { + committeesPerSlot = 1 + } + + mixPosition := (epoch + a.beaconChainCfg.EpochsPerHistoricalVector - a.beaconChainCfg.MinSeedLookahead - 1) % a.beaconChainCfg.EpochsPerHistoricalVector + mix, err := a.stateReader.ReadRandaoMixBySlotAndIndex(tx, epoch*a.beaconChainCfg.SlotsPerEpoch, mixPosition) + if err != nil { + return nil, beaconhttp.NewEndpointError(http.StatusNotFound, fmt.Sprintf("could not read randao mix: %v", err)) + } + + for currSlot := epoch * a.beaconChainCfg.SlotsPerEpoch; currSlot < (epoch+1)*a.beaconChainCfg.SlotsPerEpoch; currSlot++ { + for committeeIndex := uint64(0); committeeIndex < committeesPerSlot; committeeIndex++ { + index := (currSlot%a.beaconChainCfg.SlotsPerEpoch)*committeesPerSlot + committeeIndex + committeeCount := committeesPerSlot * a.beaconChainCfg.SlotsPerEpoch + idxs, err := a.stateReader.ComputeCommittee(mix, activeIdxs, currSlot, committeeCount, index) + if err != nil { + return nil, err + } + for vIdx, idx := range idxs { + if _, ok := idxSet[int(idx)]; !ok { + continue + } + publicKey, err := state_accessors.ReadPublicKeyByIndex(tx, idx) + if err != nil { + return nil, err + } + duty := attesterDutyResponse{ + Pubkey: publicKey, + ValidatorIndex: idx, + CommitteeIndex: committeeIndex, + CommitteeLength: uint64(len(idxs)), + ValidatorCommitteeIndex: uint64(vIdx), + CommitteesAtSlot: committeesPerSlot, + Slot: currSlot, + } + resp = append(resp, duty) + } + } + } + return newBeaconResponse(resp).withOptimistic(false), nil +} diff --git a/cl/beacon/handler/duties_attester_test.go b/cl/beacon/handler/duties_attester_test.go new file mode 100644 index 00000000000..6014096cc2a --- /dev/null +++ b/cl/beacon/handler/duties_attester_test.go @@ -0,0 +1,151 @@ +package handler + +import ( + "bytes" + "io" + "math" + "net/http" + "net/http/httptest" + "strconv" + "testing" + + "github.com/ledgerwatch/erigon/cl/clparams" + "github.com/ledgerwatch/erigon/cl/cltypes/solid" + "github.com/stretchr/testify/require" +) + +func TestDutiesAttesterAntiquated(t *testing.T) { + + // setupTestingHandler(t, clparams.Phase0Version) + _, blocks, _, _, postState, handler, _, _, fcu := setupTestingHandler(t, clparams.Phase0Version) + + fcu.HeadSlotVal = blocks[len(blocks)-1].Block.Slot + + fcu.FinalizedCheckpointVal = solid.NewCheckpointFromParameters(fcu.HeadVal, fcu.HeadSlotVal/32) + fcu.FinalizedSlotVal = math.MaxUint64 + + fcu.StateAtBlockRootVal[fcu.HeadVal] = postState + + cases := []struct { + name string + epoch string + code int + reqBody string + expected string + }{ + { + name: "non-empty-indicies", + epoch: strconv.FormatUint(fcu.HeadSlotVal/32, 10), + code: http.StatusOK, + reqBody: `["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]`, + expected: `{"data":[{"pubkey":"0x97f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb","validator_index":"0","committee_index":"0","committee_length":"14","validator_committee_index":"0","committees_at_slot":"1","slot":"8322"},{"pubkey":"0xb0e7791fb972fe014159aa33a98622da3cdc98ff707965e536d8636b5fcc5ac7a91a8c46e59a00dca575af0f18fb13dc","validator_index":"4","committee_index":"0","committee_length":"13","validator_committee_index":"5","committees_at_slot":"1","slot":"8327"},{"pubkey":"0xb928f3beb93519eecf0145da903b40a4c97dca00b21f12ac0df3be9116ef2ef27b2ae6bcd4c5bc2d54ef5a70627efcb7","validator_index":"6","committee_index":"0","committee_length":"13","validator_committee_index":"10","committees_at_slot":"1","slot":"8327"},{"pubkey":"0xa6e82f6da4520f85c5d27d8f329eccfa05944fd1096b20734c894966d12a9e2a9a9744529d7212d33883113a0cadb909","validator_index":"5","committee_index":"0","committee_length":"14","validator_committee_index":"10","committees_at_slot":"1","slot":"8329"},{"pubkey":"0x89ece308f9d1f0131765212deca99697b112d61f9be9a5f1f3780a51335b3ff981747a0b2ca2179b96d2c0c9024e5224","validator_index":"2","committee_index":"0","committee_length":"14","validator_committee_index":"11","committees_at_slot":"1","slot":"8331"},{"pubkey":"0xaf81da25ecf1c84b577fefbedd61077a81dc43b00304015b2b596ab67f00e41c86bb00ebd0f90d4b125eb0539891aeed","validator_index":"9","committee_index":"0","committee_length":"14","validator_committee_index":"8","committees_at_slot":"1","slot":"8342"},{"pubkey":"0xac9b60d5afcbd5663a8a44b7c5a02f19e9a77ab0a35bd65809bb5c67ec582c897feb04decc694b13e08587f3ff9b5b60","validator_index":"3","committee_index":"0","committee_length":"13","validator_committee_index":"6","committees_at_slot":"1","slot":"8348"}],"execution_optimistic":false}` + "\n", + }, + { + name: "empty-index", + epoch: strconv.FormatUint(fcu.HeadSlotVal/32, 10), + code: http.StatusOK, + reqBody: `[]`, + expected: `{"data":[],"execution_optimistic":false}` + "\n", + }, + { + name: "404", + reqBody: `["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]`, + epoch: `999999999`, + code: http.StatusBadRequest, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + server := httptest.NewServer(handler.mux) + defer server.Close() + // + body := bytes.Buffer{} + body.WriteString(c.reqBody) + // Query the block in the handler with /eth/v2/beacon/states/{block_id} with content-type octet-stream + req, err := http.NewRequest("POST", server.URL+"/eth/v1/validator/duties/attester/"+c.epoch, &body) + require.NoError(t, err) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + + defer resp.Body.Close() + require.Equal(t, c.code, resp.StatusCode) + if resp.StatusCode != http.StatusOK { + return + } + // read the all of the octect + out, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, c.expected, string(out)) + }) + } +} + +func TestDutiesAttesterNonAntiquated(t *testing.T) { + + // setupTestingHandler(t, clparams.Phase0Version) + _, blocks, _, _, postState, handler, _, sm, fcu := setupTestingHandler(t, clparams.Phase0Version) + + fcu.HeadSlotVal = blocks[len(blocks)-1].Block.Slot + + fcu.FinalizedCheckpointVal = solid.NewCheckpointFromParameters(fcu.HeadVal, fcu.HeadSlotVal/32) + fcu.FinalizedSlotVal = 0 + + fcu.StateAtBlockRootVal[fcu.HeadVal] = postState + require.NoError(t, sm.OnHeadState(postState)) + cases := []struct { + name string + epoch string + code int + reqBody string + expected string + }{ + { + name: "non-empty-indicies", + epoch: strconv.FormatUint(fcu.HeadSlotVal/32, 10), + code: http.StatusOK, + reqBody: `["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]`, + expected: `{"data":[{"pubkey":"0x97f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb","validator_index":"0","committee_index":"0","committee_length":"14","validator_committee_index":"0","committees_at_slot":"1","slot":"8322"},{"pubkey":"0xb0e7791fb972fe014159aa33a98622da3cdc98ff707965e536d8636b5fcc5ac7a91a8c46e59a00dca575af0f18fb13dc","validator_index":"4","committee_index":"0","committee_length":"13","validator_committee_index":"5","committees_at_slot":"1","slot":"8327"},{"pubkey":"0xb928f3beb93519eecf0145da903b40a4c97dca00b21f12ac0df3be9116ef2ef27b2ae6bcd4c5bc2d54ef5a70627efcb7","validator_index":"6","committee_index":"0","committee_length":"13","validator_committee_index":"10","committees_at_slot":"1","slot":"8327"},{"pubkey":"0xa6e82f6da4520f85c5d27d8f329eccfa05944fd1096b20734c894966d12a9e2a9a9744529d7212d33883113a0cadb909","validator_index":"5","committee_index":"0","committee_length":"14","validator_committee_index":"10","committees_at_slot":"1","slot":"8329"},{"pubkey":"0x89ece308f9d1f0131765212deca99697b112d61f9be9a5f1f3780a51335b3ff981747a0b2ca2179b96d2c0c9024e5224","validator_index":"2","committee_index":"0","committee_length":"14","validator_committee_index":"11","committees_at_slot":"1","slot":"8331"},{"pubkey":"0xaf81da25ecf1c84b577fefbedd61077a81dc43b00304015b2b596ab67f00e41c86bb00ebd0f90d4b125eb0539891aeed","validator_index":"9","committee_index":"0","committee_length":"14","validator_committee_index":"8","committees_at_slot":"1","slot":"8342"},{"pubkey":"0xac9b60d5afcbd5663a8a44b7c5a02f19e9a77ab0a35bd65809bb5c67ec582c897feb04decc694b13e08587f3ff9b5b60","validator_index":"3","committee_index":"0","committee_length":"13","validator_committee_index":"6","committees_at_slot":"1","slot":"8348"}],"execution_optimistic":false}` + "\n", + }, + { + name: "empty-index", + epoch: strconv.FormatUint(fcu.HeadSlotVal/32, 10), + code: http.StatusOK, + reqBody: `[]`, + expected: `{"data":[],"execution_optimistic":false}` + "\n", + }, + { + name: "404", + reqBody: `["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]`, + epoch: `999999999`, + code: http.StatusBadRequest, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + server := httptest.NewServer(handler.mux) + defer server.Close() + // + body := bytes.Buffer{} + body.WriteString(c.reqBody) + // Query the block in the handler with /eth/v2/beacon/states/{block_id} with content-type octet-stream + req, err := http.NewRequest("POST", server.URL+"/eth/v1/validator/duties/attester/"+c.epoch, &body) + require.NoError(t, err) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + + defer resp.Body.Close() + require.Equal(t, c.code, resp.StatusCode) + if resp.StatusCode != http.StatusOK { + return + } + // read the all of the octect + out, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, c.expected, string(out)) + }) + } +} diff --git a/cl/beacon/handler/duties_sync.go b/cl/beacon/handler/duties_sync.go new file mode 100644 index 00000000000..e52a8a23246 --- /dev/null +++ b/cl/beacon/handler/duties_sync.go @@ -0,0 +1,146 @@ +package handler + +import ( + "encoding/json" + "fmt" + "net/http" + "sort" + "strconv" + + libcommon "github.com/ledgerwatch/erigon-lib/common" + "github.com/ledgerwatch/erigon/cl/beacon/beaconhttp" + "github.com/ledgerwatch/erigon/cl/cltypes/solid" + "github.com/ledgerwatch/erigon/cl/persistence/beacon_indicies" + state_accessors "github.com/ledgerwatch/erigon/cl/persistence/state" +) + +type syncDutyResponse struct { + Pubkey libcommon.Bytes48 `json:"pubkey"` + ValidatorIndex uint64 `json:"validator_index,string"` + ValidatorSyncCommitteeIndicies []string `json:"validator_sync_committee_indicies"` +} + +func (a *ApiHandler) getSyncDuties(w http.ResponseWriter, r *http.Request) (*beaconResponse, error) { + epoch, err := epochFromRequest(r) + if err != nil { + return nil, err + } + + // compute the sync committee period + period := epoch / a.beaconChainCfg.EpochsPerSyncCommitteePeriod + + var idxsStr []string + if err := json.NewDecoder(r.Body).Decode(&idxsStr); err != nil { + return nil, beaconhttp.NewEndpointError(http.StatusBadRequest, fmt.Errorf("could not decode request body: %w. request body is required.", err).Error()) + } + if len(idxsStr) == 0 { + return newBeaconResponse([]string{}).withOptimistic(false), nil + } + duplicates := map[int]struct{}{} + // convert the request to uint64 + idxs := make([]uint64, 0, len(idxsStr)) + for _, idxStr := range idxsStr { + + idx, err := strconv.ParseUint(idxStr, 10, 64) + if err != nil { + return nil, beaconhttp.NewEndpointError(http.StatusBadRequest, fmt.Errorf("could not parse validator index: %w", err).Error()) + } + if _, ok := duplicates[int(idx)]; ok { + continue + } + idxs = append(idxs, idx) + duplicates[int(idx)] = struct{}{} + } + + tx, err := a.indiciesDB.BeginRo(r.Context()) + if err != nil { + return nil, err + } + defer tx.Rollback() + + // Try to find a slot in the epoch or close to it + referenceSlot := ((epoch + 1) * a.beaconChainCfg.SlotsPerEpoch) - 1 + + // Find the first slot in the epoch (or close enough that have a sync committee) + var referenceRoot libcommon.Hash + for ; referenceRoot != (libcommon.Hash{}); referenceSlot-- { + referenceRoot, err = beacon_indicies.ReadCanonicalBlockRoot(tx, referenceSlot) + if err != nil { + return nil, err + } + } + referencePeriod := (referenceSlot / a.beaconChainCfg.SlotsPerEpoch) / a.beaconChainCfg.EpochsPerSyncCommitteePeriod + // Now try reading the sync committee + currentSyncCommittee, nextSyncCommittee, ok := a.forkchoiceStore.GetSyncCommittees(referenceRoot) + if !ok { + roundedSlotToPeriod := a.beaconChainCfg.RoundSlotToSyncCommitteePeriod(referenceSlot) + switch { + case referencePeriod == period: + currentSyncCommittee, err = state_accessors.ReadCurrentSyncCommittee(tx, roundedSlotToPeriod) + case referencePeriod+1 == period: + nextSyncCommittee, err = state_accessors.ReadNextSyncCommittee(tx, roundedSlotToPeriod) + default: + return nil, beaconhttp.NewEndpointError(http.StatusNotFound, fmt.Sprintf("could not find sync committee for epoch %d", epoch)) + } + if err != nil { + return nil, err + } + } + var syncCommittee *solid.SyncCommittee + // Determine which one to use. TODO(Giulio2002): Make this less rendundant. + switch { + case referencePeriod == period: + syncCommittee = currentSyncCommittee + case referencePeriod+1 == period: + syncCommittee = nextSyncCommittee + default: + return nil, beaconhttp.NewEndpointError(http.StatusNotFound, fmt.Sprintf("could not find sync committee for epoch %d", epoch)) + } + if syncCommittee == nil { + return nil, beaconhttp.NewEndpointError(http.StatusNotFound, fmt.Sprintf("could not find sync committee for epoch %d", epoch)) + } + // Now we have the sync committee, we can initialize our response set + dutiesSet := map[uint64]*syncDutyResponse{} + for _, idx := range idxs { + publicKey, err := state_accessors.ReadPublicKeyByIndex(tx, idx) + if err != nil { + return nil, err + } + if publicKey == (libcommon.Bytes48{}) { + return nil, beaconhttp.NewEndpointError(http.StatusNotFound, fmt.Sprintf("could not find validator with index %d", idx)) + } + dutiesSet[idx] = &syncDutyResponse{ + Pubkey: publicKey, + ValidatorIndex: idx, + } + } + // Now we can iterate over the sync committee and fill the response + for idx, committeePartecipantPublicKey := range syncCommittee.GetCommittee() { + committeePartecipantIndex, ok, err := state_accessors.ReadValidatorIndexByPublicKey(tx, committeePartecipantPublicKey) + if err != nil { + return nil, err + } + if !ok { + return nil, beaconhttp.NewEndpointError(http.StatusNotFound, fmt.Sprintf("could not find validator with public key %x", committeePartecipantPublicKey)) + } + if _, ok := dutiesSet[committeePartecipantIndex]; !ok { + continue + } + dutiesSet[committeePartecipantIndex].ValidatorSyncCommitteeIndicies = append( + dutiesSet[committeePartecipantIndex].ValidatorSyncCommitteeIndicies, + strconv.FormatUint(uint64(idx), 10)) + } + // Now we can convert the map to a slice + duties := make([]*syncDutyResponse, 0, len(dutiesSet)) + for _, duty := range dutiesSet { + if len(duty.ValidatorSyncCommitteeIndicies) == 0 { + continue + } + duties = append(duties, duty) + } + sort.Slice(duties, func(i, j int) bool { + return duties[i].ValidatorIndex < duties[j].ValidatorIndex + }) + + return newBeaconResponse(duties).withOptimistic(false), nil +} diff --git a/cl/beacon/handler/duties_sync_test.go b/cl/beacon/handler/duties_sync_test.go new file mode 100644 index 00000000000..eca554c668d --- /dev/null +++ b/cl/beacon/handler/duties_sync_test.go @@ -0,0 +1,85 @@ +package handler + +import ( + "bytes" + "io" + "math" + "net/http" + "net/http/httptest" + "strconv" + "testing" + + "github.com/ledgerwatch/erigon/cl/clparams" + "github.com/ledgerwatch/erigon/cl/cltypes/solid" + "github.com/stretchr/testify/require" +) + +func TestDutiesSync(t *testing.T) { + // setupTestingHandler(t, clparams.Phase0Version) + _, blocks, _, _, postState, handler, _, _, fcu := setupTestingHandler(t, clparams.BellatrixVersion) + + fcu.HeadSlotVal = blocks[len(blocks)-1].Block.Slot + + fcu.FinalizedCheckpointVal = solid.NewCheckpointFromParameters(fcu.HeadVal, fcu.HeadSlotVal/32) + fcu.FinalizedSlotVal = math.MaxUint64 + + fcu.StateAtBlockRootVal[fcu.HeadVal] = postState + + cases := []struct { + name string + epoch string + code int + reqBody string + expected string + }{ + { + name: "non-empty-indicies", + epoch: strconv.FormatUint(fcu.HeadSlotVal/32, 10), + code: http.StatusOK, + reqBody: `["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]`, + expected: `{"data":[{"pubkey":"0x97f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb","validator_index":"0","validator_sync_committee_indicies":["30","286"]},{"pubkey":"0xa572cbea904d67468808c8eb50a9450c9721db309128012543902d0ac358a62ae28f75bb8f1c7c42c39a8c5529bf0f4e","validator_index":"1","validator_sync_committee_indicies":["120","376"]},{"pubkey":"0x89ece308f9d1f0131765212deca99697b112d61f9be9a5f1f3780a51335b3ff981747a0b2ca2179b96d2c0c9024e5224","validator_index":"2","validator_sync_committee_indicies":["138","394"]},{"pubkey":"0xac9b60d5afcbd5663a8a44b7c5a02f19e9a77ab0a35bd65809bb5c67ec582c897feb04decc694b13e08587f3ff9b5b60","validator_index":"3","validator_sync_committee_indicies":["10","266"]},{"pubkey":"0xb0e7791fb972fe014159aa33a98622da3cdc98ff707965e536d8636b5fcc5ac7a91a8c46e59a00dca575af0f18fb13dc","validator_index":"4","validator_sync_committee_indicies":["114","370"]},{"pubkey":"0xa6e82f6da4520f85c5d27d8f329eccfa05944fd1096b20734c894966d12a9e2a9a9744529d7212d33883113a0cadb909","validator_index":"5","validator_sync_committee_indicies":["103","359"]},{"pubkey":"0xb928f3beb93519eecf0145da903b40a4c97dca00b21f12ac0df3be9116ef2ef27b2ae6bcd4c5bc2d54ef5a70627efcb7","validator_index":"6","validator_sync_committee_indicies":["163","419"]},{"pubkey":"0xa85ae765588126f5e860d019c0e26235f567a9c0c0b2d8ff30f3e8d436b1082596e5e7462d20f5be3764fd473e57f9cf","validator_index":"7","validator_sync_committee_indicies":["197","453"]},{"pubkey":"0x99cdf3807146e68e041314ca93e1fee0991224ec2a74beb2866816fd0826ce7b6263ee31e953a86d1b72cc2215a57793","validator_index":"8","validator_sync_committee_indicies":["175","431"]},{"pubkey":"0xaf81da25ecf1c84b577fefbedd61077a81dc43b00304015b2b596ab67f00e41c86bb00ebd0f90d4b125eb0539891aeed","validator_index":"9","validator_sync_committee_indicies":["53","309"]}],"execution_optimistic":false}` + "\n", + }, + { + name: "empty-index", + epoch: strconv.FormatUint(fcu.HeadSlotVal/32, 10), + code: http.StatusOK, + reqBody: `[]`, + expected: `{"data":[],"execution_optimistic":false}` + "\n", + }, + { + name: "404", + reqBody: `["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]`, + epoch: `999999999`, + code: http.StatusNotFound, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + server := httptest.NewServer(handler.mux) + defer server.Close() + // + body := bytes.Buffer{} + body.WriteString(c.reqBody) + // Query the block in the handler with /eth/v2/beacon/states/{block_id} with content-type octet-stream + req, err := http.NewRequest("POST", server.URL+"/eth/v1/validator/duties/sync/"+c.epoch, &body) + require.NoError(t, err) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + + defer resp.Body.Close() + require.Equal(t, c.code, resp.StatusCode) + if resp.StatusCode != http.StatusOK { + return + } + // read the all of the octect + out, err := io.ReadAll(resp.Body) + require.NoError(t, err) + if string(out) != c.expected { + panic(string(out)) + } + require.Equal(t, c.expected, string(out)) + }) + } +} diff --git a/cl/beacon/handler/format.go b/cl/beacon/handler/format.go index ba734568b48..7baa88b42f2 100644 --- a/cl/beacon/handler/format.go +++ b/cl/beacon/handler/format.go @@ -60,6 +60,14 @@ func (r *beaconResponse) withFinalized(finalized bool) (out *beaconResponse) { return out } +func (r *beaconResponse) withOptimistic(optimistic bool) (out *beaconResponse) { + out = new(beaconResponse) + *out = *r + out.ExecutionOptimistic = new(bool) + out.ExecutionOptimistic = &optimistic + return out +} + func (r *beaconResponse) withVersion(version clparams.StateVersion) (out *beaconResponse) { out = new(beaconResponse) *out = *r diff --git a/cl/beacon/handler/handler.go b/cl/beacon/handler/handler.go index 92407d84b52..72a926c9fa0 100644 --- a/cl/beacon/handler/handler.go +++ b/cl/beacon/handler/handler.go @@ -9,6 +9,7 @@ import ( "github.com/ledgerwatch/erigon/cl/beacon/beaconhttp" "github.com/ledgerwatch/erigon/cl/beacon/synced_data" "github.com/ledgerwatch/erigon/cl/clparams" + "github.com/ledgerwatch/erigon/cl/cltypes/solid" "github.com/ledgerwatch/erigon/cl/persistence" "github.com/ledgerwatch/erigon/cl/persistence/state/historical_states_reader" "github.com/ledgerwatch/erigon/cl/phase1/forkchoice" @@ -28,10 +29,15 @@ type ApiHandler struct { operationsPool pool.OperationsPool syncedData *synced_data.SyncedDataManager stateReader *historical_states_reader.HistoricalStatesReader + + // pools + randaoMixesPool sync.Pool } func NewApiHandler(genesisConfig *clparams.GenesisConfig, beaconChainConfig *clparams.BeaconChainConfig, source persistence.RawBeaconBlockChain, indiciesDB kv.RoDB, forkchoiceStore forkchoice.ForkChoiceStorage, operationsPool pool.OperationsPool, rcsn freezeblocks.BeaconSnapshotReader, syncedData *synced_data.SyncedDataManager, stateReader *historical_states_reader.HistoricalStatesReader) *ApiHandler { - return &ApiHandler{o: sync.Once{}, genesisCfg: genesisConfig, beaconChainCfg: beaconChainConfig, indiciesDB: indiciesDB, forkchoiceStore: forkchoiceStore, operationsPool: operationsPool, blockReader: rcsn, syncedData: syncedData, stateReader: stateReader} + return &ApiHandler{o: sync.Once{}, genesisCfg: genesisConfig, beaconChainCfg: beaconChainConfig, indiciesDB: indiciesDB, forkchoiceStore: forkchoiceStore, operationsPool: operationsPool, blockReader: rcsn, syncedData: syncedData, stateReader: stateReader, randaoMixesPool: sync.Pool{New: func() interface{} { + return solid.NewHashVector(int(beaconChainConfig.EpochsPerHistoricalVector)) + }}} } func (a *ApiHandler) init() { @@ -79,6 +85,7 @@ func (a *ApiHandler) init() { r.Route("/states", func(r chi.Router) { r.Get("/head/validators/{index}", http.NotFound) // otterscan r.Route("/{state_id}", func(r chi.Router) { + r.Get("/randao", beaconhttp.HandleEndpointFunc(a.getRandao)) r.Get("/committees", beaconhttp.HandleEndpointFunc(a.getCommittees)) r.Get("/sync_committees", beaconhttp.HandleEndpointFunc(a.getSyncCommittees)) // otterscan r.Get("/finality_checkpoints", beaconhttp.HandleEndpointFunc(a.getFinalityCheckpoints)) @@ -93,9 +100,9 @@ func (a *ApiHandler) init() { }) r.Route("/validator", func(r chi.Router) { r.Route("/duties", func(r chi.Router) { - r.Post("/attester/{epoch}", http.NotFound) + r.Post("/attester/{epoch}", beaconhttp.HandleEndpointFunc(a.getAttesterDuties)) r.Get("/proposer/{epoch}", beaconhttp.HandleEndpointFunc(a.getDutiesProposer)) - r.Post("/sync/{epoch}", http.NotFound) + r.Post("/sync/{epoch}", beaconhttp.HandleEndpointFunc(a.getSyncDuties)) }) r.Get("/blinded_blocks/{slot}", http.NotFound) r.Get("/attestation_data", http.NotFound) @@ -106,6 +113,7 @@ func (a *ApiHandler) init() { r.Get("/sync_committee_contribution", http.NotFound) r.Post("/contribution_and_proofs", http.NotFound) r.Post("/prepare_beacon_proposer", http.NotFound) + r.Post("/liveness/{epoch}", beaconhttp.HandleEndpointFunc(a.liveness)) }) }) r.Route("/v2", func(r chi.Router) { diff --git a/cl/beacon/handler/liveness.go b/cl/beacon/handler/liveness.go new file mode 100644 index 00000000000..f6b8bd4b519 --- /dev/null +++ b/cl/beacon/handler/liveness.go @@ -0,0 +1,153 @@ +package handler + +import ( + "encoding/json" + "fmt" + "net/http" + "sort" + "strconv" + + libcommon "github.com/ledgerwatch/erigon-lib/common" + "github.com/ledgerwatch/erigon-lib/kv" + "github.com/ledgerwatch/erigon/cl/beacon/beaconhttp" + "github.com/ledgerwatch/erigon/cl/cltypes" + "github.com/ledgerwatch/erigon/cl/cltypes/solid" + "github.com/ledgerwatch/erigon/cl/utils" +) + +type live struct { + Index int `json:"index,string"` + IsLive bool `json:"is_live"` +} + +func (a *ApiHandler) liveness(w http.ResponseWriter, r *http.Request) (*beaconResponse, error) { + epoch, err := epochFromRequest(r) + if err != nil { + return nil, err + } + maxEpoch := utils.GetCurrentEpoch(a.genesisCfg.GenesisTime, a.beaconChainCfg.SecondsPerSlot, a.beaconChainCfg.SlotsPerEpoch) + if epoch > maxEpoch { + return nil, beaconhttp.NewEndpointError(http.StatusBadRequest, fmt.Errorf("epoch %d is in the future, max epoch is %d", epoch, maxEpoch).Error()) + } + + var idxsStr []string + if err := json.NewDecoder(r.Body).Decode(&idxsStr); err != nil { + return nil, beaconhttp.NewEndpointError(http.StatusBadRequest, fmt.Errorf("could not decode request body: %w. request body is required.", err).Error()) + } + if len(idxsStr) == 0 { + return newBeaconResponse([]string{}), nil + } + idxSet := map[int]struct{}{} + // convert the request to uint64 + idxs := make([]uint64, 0, len(idxsStr)) + for _, idxStr := range idxsStr { + idx, err := strconv.ParseUint(idxStr, 10, 64) + if err != nil { + return nil, beaconhttp.NewEndpointError(http.StatusBadRequest, fmt.Errorf("could not parse validator index: %w", err).Error()) + } + if _, ok := idxSet[int(idx)]; ok { + continue + } + idxs = append(idxs, idx) + idxSet[int(idx)] = struct{}{} + } + + tx, err := a.indiciesDB.BeginRo(r.Context()) + if err != nil { + return nil, err + } + defer tx.Rollback() + ctx := r.Context() + liveSet := map[uint64]*live{} + // initialize resp. + for _, idx := range idxs { + liveSet[idx] = &live{Index: int(idx), IsLive: false} + } + var lastBlockRootProcess libcommon.Hash + var lastSlotProcess uint64 + // we need to obtain the relevant data: + // Use the blocks in the epoch as heuristic + for i := epoch * a.beaconChainCfg.SlotsPerEpoch; i < ((epoch+1)*a.beaconChainCfg.SlotsPerEpoch)-1; i++ { + block, err := a.blockReader.ReadBlockBySlot(ctx, tx, i) + if err != nil { + return nil, err + } + if block == nil { + continue + } + updateLivenessWithBlock(block, liveSet) + lastBlockRootProcess, err = block.Block.HashSSZ() + if err != nil { + return nil, err + } + lastSlotProcess = block.Block.Slot + } + // use the epoch partecipation as an additional heuristic + currentEpochPartecipation, previousEpochPartecipation, err := a.obtainCurrentEpochPartecipationFromEpoch(tx, epoch, lastBlockRootProcess, lastSlotProcess) + if err != nil { + return nil, err + } + if currentEpochPartecipation == nil { + return nil, beaconhttp.NewEndpointError(http.StatusNotFound, fmt.Sprintf("could not find partecipations for epoch %d, if this was an historical query, turn on --caplin.archive", epoch)) + } + for idx, live := range liveSet { + if live.IsLive { + continue + } + if idx >= uint64(currentEpochPartecipation.Length()) { + continue + } + if currentEpochPartecipation.Get(int(idx)) != 0 { + live.IsLive = true + continue + } + if idx >= uint64(previousEpochPartecipation.Length()) { + continue + } + live.IsLive = previousEpochPartecipation.Get(int(idx)) != 0 + } + + resp := []*live{} + for _, v := range liveSet { + resp = append(resp, v) + } + sort.Slice(resp, func(i, j int) bool { + return resp[i].Index < resp[j].Index + }) + + return newBeaconResponse(resp), nil +} + +func (a *ApiHandler) obtainCurrentEpochPartecipationFromEpoch(tx kv.Tx, epoch uint64, blockRoot libcommon.Hash, blockSlot uint64) (*solid.BitList, *solid.BitList, error) { + prevEpoch := epoch + if epoch > 0 { + prevEpoch-- + } + + currPartecipation, ok1 := a.forkchoiceStore.Partecipation(epoch) + prevPartecipation, ok2 := a.forkchoiceStore.Partecipation(prevEpoch) + if !ok1 || !ok2 { + return a.stateReader.ReadPartecipations(tx, blockSlot) + } + return currPartecipation, prevPartecipation, nil + +} + +func updateLivenessWithBlock(block *cltypes.SignedBeaconBlock, liveSet map[uint64]*live) { + body := block.Block.Body + if _, ok := liveSet[block.Block.ProposerIndex]; ok { + liveSet[block.Block.ProposerIndex].IsLive = true + } + body.VoluntaryExits.Range(func(index int, value *cltypes.SignedVoluntaryExit, length int) bool { + if _, ok := liveSet[value.VoluntaryExit.ValidatorIndex]; ok { + liveSet[value.VoluntaryExit.ValidatorIndex].IsLive = true + } + return true + }) + body.ExecutionChanges.Range(func(index int, value *cltypes.SignedBLSToExecutionChange, length int) bool { + if _, ok := liveSet[value.Message.ValidatorIndex]; ok { + liveSet[value.Message.ValidatorIndex].IsLive = true + } + return true + }) +} diff --git a/cl/beacon/handler/liveness_test.go b/cl/beacon/handler/liveness_test.go new file mode 100644 index 00000000000..73bb5deec91 --- /dev/null +++ b/cl/beacon/handler/liveness_test.go @@ -0,0 +1,62 @@ +package handler + +import ( + "bytes" + "encoding/json" + "math" + "net/http" + "net/http/httptest" + "strconv" + "testing" + + "github.com/ledgerwatch/erigon/cl/clparams" + "github.com/ledgerwatch/erigon/cl/cltypes/solid" + "github.com/stretchr/testify/require" +) + +func TestLiveness(t *testing.T) { + // i just want the correct schema to be generated + _, blocks, _, _, _, handler, _, _, fcu := setupTestingHandler(t, clparams.Phase0Version) + + fcu.HeadSlotVal = blocks[len(blocks)-1].Block.Slot + + fcu.FinalizedCheckpointVal = solid.NewCheckpointFromParameters(fcu.HeadVal, fcu.HeadSlotVal/32) + fcu.FinalizedSlotVal = math.MaxUint64 + reqBody := `["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]` + server := httptest.NewServer(handler.mux) + defer server.Close() + // + body := bytes.Buffer{} + body.WriteString(reqBody) + // Query the block in the handler with /eth/v2/beacon/states/{block_id} with content-type octet-stream + req, err := http.NewRequest("POST", server.URL+"/eth/v1/validator/liveness/"+strconv.FormatUint(fcu.HeadSlotVal/32, 10), &body) + require.NoError(t, err) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + + defer resp.Body.Close() + require.Equal(t, http.StatusOK, resp.StatusCode) + + out := map[string]interface{}{} + require.NoError(t, json.NewDecoder(resp.Body).Decode(&out)) + data := out["data"].([]interface{}) + require.Equal(t, 11, len(data)) + // check that is has is_live (bool) and index (stringifed int) + for _, d := range data { + d := d.(map[string]interface{}) + require.Equal(t, 2, len(d)) + isLive, ok := d["is_live"] + require.True(t, ok) + _, ok = isLive.(bool) + require.True(t, ok) + i1, ok := d["index"] + require.True(t, ok) + strIndex, ok := i1.(string) + require.True(t, ok) + _, err := strconv.ParseUint(strIndex, 10, 64) + require.NoError(t, err) + + } + +} diff --git a/cl/beacon/handler/rewards.go b/cl/beacon/handler/rewards.go index eb0c3373ebe..dc8ca4f8119 100644 --- a/cl/beacon/handler/rewards.go +++ b/cl/beacon/handler/rewards.go @@ -47,7 +47,8 @@ func (a *ApiHandler) getBlockRewards(w http.ResponseWriter, r *http.Request) (*b return nil, beaconhttp.NewEndpointError(http.StatusNotFound, "block not found") } slot := blk.Header.Slot - if slot >= a.forkchoiceStore.FinalizedSlot() { + isFinalized := slot <= a.forkchoiceStore.FinalizedSlot() + if slot >= a.forkchoiceStore.LowestAvaiableSlot() { // finalized case blkRewards, ok := a.forkchoiceStore.BlockRewards(root) if !ok { @@ -60,7 +61,7 @@ func (a *ApiHandler) getBlockRewards(w http.ResponseWriter, r *http.Request) (*b AttesterSlashings: blkRewards.AttesterSlashings, SyncAggregate: blkRewards.SyncAggregate, Total: blkRewards.Attestations + blkRewards.ProposerSlashings + blkRewards.AttesterSlashings + blkRewards.SyncAggregate, - }).withFinalized(false), nil + }).withFinalized(isFinalized), nil } slotData, err := state_accessors.ReadSlotData(tx, slot) if err != nil { @@ -76,7 +77,7 @@ func (a *ApiHandler) getBlockRewards(w http.ResponseWriter, r *http.Request) (*b AttesterSlashings: slotData.AttesterSlashings, SyncAggregate: slotData.SyncAggregateRewards, Total: slotData.AttestationsRewards + slotData.ProposerSlashings + slotData.AttesterSlashings + slotData.SyncAggregateRewards, - }).withFinalized(true), nil + }).withFinalized(isFinalized), nil } type syncCommitteeReward struct { diff --git a/cl/beacon/handler/states.go b/cl/beacon/handler/states.go index 93a82c71638..335296f349d 100644 --- a/cl/beacon/handler/states.go +++ b/cl/beacon/handler/states.go @@ -357,3 +357,63 @@ func (a *ApiHandler) getSyncCommittees(w http.ResponseWriter, r *http.Request) ( return newBeaconResponse(response).withFinalized(canonicalRoot == blockRoot && *slot <= a.forkchoiceStore.FinalizedSlot()), nil } + +type randaoResponse struct { + Randao libcommon.Hash `json:"randao"` +} + +func (a *ApiHandler) getRandao(w http.ResponseWriter, r *http.Request) (*beaconResponse, error) { + ctx := r.Context() + + tx, err := a.indiciesDB.BeginRo(ctx) + if err != nil { + return nil, err + } + defer tx.Rollback() + blockId, err := stateIdFromRequest(r) + if err != nil { + return nil, beaconhttp.NewEndpointError(http.StatusBadRequest, err.Error()) + } + + blockRoot, httpStatus, err := a.blockRootFromStateId(ctx, tx, blockId) + if err != nil { + return nil, beaconhttp.NewEndpointError(httpStatus, err.Error()) + } + + epochReq, err := uint64FromQueryParams(r, "epoch") + if err != nil { + return nil, err + } + slotPtr, err := beacon_indicies.ReadBlockSlotByBlockRoot(tx, blockRoot) + if err != nil { + return nil, err + } + if slotPtr == nil { + return nil, beaconhttp.NewEndpointError(http.StatusNotFound, fmt.Sprintf("could not read block slot: %x", blockRoot)) + } + slot := *slotPtr + epoch := slot / a.beaconChainCfg.SlotsPerEpoch + if epochReq != nil { + epoch = *epochReq + } + randaoMixes := a.randaoMixesPool.Get().(solid.HashListSSZ) + defer a.randaoMixesPool.Put(randaoMixes) + + if a.forkchoiceStore.RandaoMixes(blockRoot, randaoMixes) { + mix := randaoMixes.Get(int(epoch % a.beaconChainCfg.EpochsPerHistoricalVector)) + return newBeaconResponse(randaoResponse{Randao: mix}).withFinalized(slot <= a.forkchoiceStore.FinalizedSlot()), nil + } + // check if the block is canonical + canonicalRoot, err := beacon_indicies.ReadCanonicalBlockRoot(tx, slot) + if err != nil { + return nil, err + } + if canonicalRoot != blockRoot { + return nil, beaconhttp.NewEndpointError(http.StatusNotFound, fmt.Sprintf("could not read randao: %x", blockRoot)) + } + mix, err := a.stateReader.ReadRandaoMixBySlotAndIndex(tx, slot, epoch%a.beaconChainCfg.EpochsPerHistoricalVector) + if err != nil { + return nil, err + } + return newBeaconResponse(randaoResponse{Randao: mix}).withFinalized(slot <= a.forkchoiceStore.FinalizedSlot()), nil +} diff --git a/cl/beacon/handler/states_test.go b/cl/beacon/handler/states_test.go index cd145f1e729..eaa6d633a9e 100644 --- a/cl/beacon/handler/states_test.go +++ b/cl/beacon/handler/states_test.go @@ -445,3 +445,60 @@ func TestGetStateFinalityCheckpoints(t *testing.T) { }) } } + +func TestGetRandao(t *testing.T) { + + // setupTestingHandler(t, clparams.Phase0Version) + _, blocks, _, _, postState, handler, _, _, fcu := setupTestingHandler(t, clparams.BellatrixVersion) + + postRoot, err := postState.HashSSZ() + require.NoError(t, err) + + fcu.HeadVal, err = blocks[len(blocks)-1].Block.HashSSZ() + require.NoError(t, err) + + fcu.HeadSlotVal = blocks[len(blocks)-1].Block.Slot + + fcu.JustifiedCheckpointVal = solid.NewCheckpointFromParameters(fcu.HeadVal, fcu.HeadSlotVal/32) + + cases := []struct { + blockID string + code int + }{ + { + blockID: "0x" + common.Bytes2Hex(postRoot[:]), + code: http.StatusOK, + }, + { + blockID: "justified", + code: http.StatusOK, + }, + { + blockID: "0x" + common.Bytes2Hex(make([]byte, 32)), + code: http.StatusNotFound, + }, + { + blockID: strconv.FormatInt(int64(postState.Slot()), 10), + code: http.StatusOK, + }, + } + expected := `{"data":{"randao":"0xdeec617717272914bfd73e02ca1da113a83cf4cf33cd4939486509e2da4ccf4e"},"finalized":false,"execution_optimistic":false}` + "\n" + for _, c := range cases { + t.Run(c.blockID, func(t *testing.T) { + server := httptest.NewServer(handler.mux) + defer server.Close() + resp, err := http.Get(server.URL + "/eth/v1/beacon/states/" + c.blockID + "/randao") + require.NoError(t, err) + + defer resp.Body.Close() + require.Equal(t, c.code, resp.StatusCode) + if resp.StatusCode != http.StatusOK { + return + } + // read the all of the octect + out, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, string(out), expected) + }) + } +} diff --git a/cl/cltypes/solid/bitlist.go b/cl/cltypes/solid/bitlist.go index cf14cf0644c..f54d55af93a 100644 --- a/cl/cltypes/solid/bitlist.go +++ b/cl/cltypes/solid/bitlist.go @@ -65,6 +65,13 @@ func (u *BitList) CopyTo(target IterableSSZ[byte]) { } } +func (u *BitList) Copy() *BitList { + n := NewBitList(u.l, u.c) + n.u = make([]byte, len(u.u), cap(u.u)) + copy(n.u, u.u) + return n +} + // Range allows us to do something to each bit in the list, just like a Power Rangers roll call. func (u *BitList) Range(fn func(index int, value byte, length int) bool) { for i, v := range u.u { diff --git a/cl/phase1/core/state/raw/getters.go b/cl/phase1/core/state/raw/getters.go index 9d8fc435548..1d234a647a0 100644 --- a/cl/phase1/core/state/raw/getters.go +++ b/cl/phase1/core/state/raw/getters.go @@ -92,6 +92,10 @@ func (b *BeaconState) PreviousEpochParticipation() *solid.BitList { return b.previousEpochParticipation } +func (b *BeaconState) CurrentEpochParticipation() *solid.BitList { + return b.currentEpochParticipation +} + func (b *BeaconState) ValidatorLength() int { return b.validators.Length() } diff --git a/cl/phase1/forkchoice/fork_choice_test.go b/cl/phase1/forkchoice/fork_choice_test.go index f34df7f0c73..f712a447124 100644 --- a/cl/phase1/forkchoice/fork_choice_test.go +++ b/cl/phase1/forkchoice/fork_choice_test.go @@ -3,6 +3,7 @@ package forkchoice_test import ( "context" _ "embed" + "fmt" "testing" "github.com/ledgerwatch/erigon/cl/antiquary/tests" @@ -11,6 +12,7 @@ import ( "github.com/ledgerwatch/erigon/cl/phase1/forkchoice" "github.com/ledgerwatch/erigon/cl/phase1/forkchoice/fork_graph" "github.com/ledgerwatch/erigon/cl/pool" + "github.com/ledgerwatch/erigon/cl/transition" "github.com/spf13/afero" libcommon "github.com/ledgerwatch/erigon-lib/common" @@ -111,6 +113,16 @@ func TestForkChoiceBasic(t *testing.T) { func TestForkChoiceChainBellatrix(t *testing.T) { blocks, anchorState, _ := tests.GetBellatrixRandom() + + intermediaryState, err := anchorState.Copy() + require.NoError(t, err) + + intermediaryBlockRoot := blocks[0].Block.ParentRoot + for i := 0; i < 35; i++ { + require.NoError(t, transition.TransitionState(intermediaryState, blocks[i], nil, false)) + intermediaryBlockRoot, err = blocks[i].Block.HashSSZ() + require.NoError(t, err) + } // Initialize forkchoice store pool := pool.NewOperationsPool(&clparams.MainnetBeaconConfig) store, err := forkchoice.NewForkChoiceStore(context.Background(), anchorState, nil, nil, pool, fork_graph.NewForkGraphDisk(anchorState, afero.NewMemMapFs())) @@ -125,4 +137,15 @@ func TestForkChoiceChainBellatrix(t *testing.T) { rewards, ok := store.BlockRewards(libcommon.Hash(root1)) require.True(t, ok) require.Equal(t, rewards.Attestations, uint64(0x511ad)) + // test randao mix + mixes := solid.NewHashVector(int(clparams.MainnetBeaconConfig.EpochsPerHistoricalVector)) + require.True(t, store.RandaoMixes(intermediaryBlockRoot, mixes)) + for i := 0; i < mixes.Length(); i++ { + require.Equal(t, mixes.Get(i), intermediaryState.RandaoMixes().Get(i), fmt.Sprintf("mixes mismatch at index %d, have: %x, expected: %x", i, mixes.Get(i), intermediaryState.RandaoMixes().Get(i))) + } + currentIntermediarySyncCommittee, nextIntermediarySyncCommittee, ok := store.GetSyncCommittees(intermediaryBlockRoot) + require.True(t, ok) + + require.Equal(t, intermediaryState.CurrentSyncCommittee(), currentIntermediarySyncCommittee) + require.Equal(t, intermediaryState.NextSyncCommittee(), nextIntermediarySyncCommittee) } diff --git a/cl/phase1/forkchoice/fork_graph/fork_graph_disk.go b/cl/phase1/forkchoice/fork_graph/fork_graph_disk.go index 42ebaaa8d47..1030cba9014 100644 --- a/cl/phase1/forkchoice/fork_graph/fork_graph_disk.go +++ b/cl/phase1/forkchoice/fork_graph/fork_graph_disk.go @@ -92,7 +92,7 @@ type forkGraphDisk struct { beaconCfg *clparams.BeaconChainConfig genesisTime uint64 // highest block seen - highestSeen, anchorSlot uint64 + highestSeen, lowestAvaiableSlot, anchorSlot uint64 // reusable buffers sszBuffer bytes.Buffer @@ -132,9 +132,10 @@ func NewForkGraphDisk(anchorState *state.CachingBeaconState, aferoFs afero.Fs) F finalizedCheckpoints: make(map[libcommon.Hash]solid.Checkpoint), blockRewards: make(map[libcommon.Hash]*eth2.BlockRewardsCollector), // configuration - beaconCfg: anchorState.BeaconConfig(), - genesisTime: anchorState.GenesisTime(), - anchorSlot: anchorState.Slot(), + beaconCfg: anchorState.BeaconConfig(), + genesisTime: anchorState.GenesisTime(), + anchorSlot: anchorState.Slot(), + lowestAvaiableSlot: anchorState.Slot(), } f.dumpBeaconStateOnDisk(anchorState, anchorRoot) return f @@ -402,6 +403,7 @@ func (f *forkGraphDisk) Prune(pruneSlot uint64) (err error) { } oldRoots = append(oldRoots, hash) } + f.lowestAvaiableSlot = pruneSlot + 1 for _, root := range oldRoots { delete(f.badBlocks, root) delete(f.blocks, root) @@ -430,3 +432,7 @@ func (f *forkGraphDisk) GetBlockRewards(blockRoot libcommon.Hash) (*eth2.BlockRe obj, has := f.blockRewards[blockRoot] return obj, has } + +func (f *forkGraphDisk) LowestAvaiableSlot() uint64 { + return f.lowestAvaiableSlot +} diff --git a/cl/phase1/forkchoice/fork_graph/interface.go b/cl/phase1/forkchoice/fork_graph/interface.go index 4b276797338..23d9e106040 100644 --- a/cl/phase1/forkchoice/fork_graph/interface.go +++ b/cl/phase1/forkchoice/fork_graph/interface.go @@ -29,6 +29,7 @@ type ForkGraph interface { AnchorSlot() uint64 Prune(uint64) error GetBlockRewards(blockRoot libcommon.Hash) (*eth2.BlockRewardsCollector, bool) + LowestAvaiableSlot() uint64 // extra methods for validator api GetStateAtSlot(slot uint64, alwaysCopy bool) (*state.CachingBeaconState, error) diff --git a/cl/phase1/forkchoice/forkchoice.go b/cl/phase1/forkchoice/forkchoice.go index e5a7326328c..f6533b96ebb 100644 --- a/cl/phase1/forkchoice/forkchoice.go +++ b/cl/phase1/forkchoice/forkchoice.go @@ -27,6 +27,11 @@ const ( allowedCachedStates = 8 ) +type randaoDelta struct { + epoch uint64 + delta libcommon.Hash +} + type finalityCheckpoints struct { finalizedCheckpoint solid.Checkpoint currentJustifiedCheckpoint solid.Checkpoint @@ -66,6 +71,11 @@ type ForkChoiceStore struct { preverifiedSizes *lru.Cache[libcommon.Hash, preverifiedAppendListsSizes] finalityCheckpoints *lru.Cache[libcommon.Hash, finalityCheckpoints] totalActiveBalances *lru.Cache[libcommon.Hash, uint64] + // Randao mixes + randaoMixesLists *lru.Cache[libcommon.Hash, solid.HashListSSZ] // limited randao mixes full list (only 16 elements) + randaoDeltas *lru.Cache[libcommon.Hash, randaoDelta] // small entry can be lots of elements. + // participation tracking + participation *lru.Cache[uint64, *solid.BitList] // epoch -> [partecipation] mu sync.Mutex // EL @@ -103,6 +113,16 @@ func NewForkChoiceStore(ctx context.Context, anchorState *state2.CachingBeaconSt return nil, err } + randaoMixesLists, err := lru.New[libcommon.Hash, solid.HashListSSZ](allowedCachedStates) + if err != nil { + return nil, err + } + + randaoDeltas, err := lru.New[libcommon.Hash, randaoDelta](checkpointsPerCache) + if err != nil { + return nil, err + } + finalityCheckpoints, err := lru.New[libcommon.Hash, finalityCheckpoints](checkpointsPerCache) if err != nil { return nil, err @@ -131,8 +151,18 @@ func NewForkChoiceStore(ctx context.Context, anchorState *state2.CachingBeaconSt if err != nil { return nil, err } - totalActiveBalances.Add(anchorRoot, anchorState.GetTotalActiveBalance()) + participation, err := lru.New[uint64, *solid.BitList](16) + if err != nil { + return nil, err + } + + participation.Add(state.Epoch(anchorState.BeaconState), anchorState.CurrentEpochParticipation().Copy()) + + totalActiveBalances.Add(anchorRoot, anchorState.GetTotalActiveBalance()) + r := solid.NewHashVector(int(anchorState.BeaconConfig().EpochsPerHistoricalVector)) + anchorState.RandaoMixes().CopyTo(r) + randaoMixesLists.Add(anchorRoot, r) return &ForkChoiceStore{ ctx: ctx, highestSeen: anchorState.Slot(), @@ -156,6 +186,9 @@ func NewForkChoiceStore(ctx context.Context, anchorState *state2.CachingBeaconSt preverifiedSizes: preverifiedSizes, finalityCheckpoints: finalityCheckpoints, totalActiveBalances: totalActiveBalances, + randaoMixesLists: randaoMixesLists, + randaoDeltas: randaoDeltas, + participation: participation, }, nil } @@ -235,7 +268,7 @@ func (f *ForkChoiceStore) FinalizedCheckpoint() solid.Checkpoint { func (f *ForkChoiceStore) FinalizedSlot() uint64 { f.mu.Lock() defer f.mu.Unlock() - return f.computeStartSlotAtEpoch(f.finalizedCheckpoint.Epoch()) + return f.computeStartSlotAtEpoch(f.finalizedCheckpoint.Epoch()) + (f.beaconCfg.SlotsPerEpoch - 1) } // FinalizedCheckpoint returns justified checkpoint @@ -321,3 +354,48 @@ func (f *ForkChoiceStore) BlockRewards(root libcommon.Hash) (*eth2.BlockRewardsC func (f *ForkChoiceStore) TotalActiveBalance(root libcommon.Hash) (uint64, bool) { return f.totalActiveBalances.Get(root) } + +func (f *ForkChoiceStore) LowestAvaiableSlot() uint64 { + f.mu.Lock() + defer f.mu.Unlock() + return f.forkGraph.LowestAvaiableSlot() +} + +func (f *ForkChoiceStore) RandaoMixes(blockRoot libcommon.Hash, out solid.HashListSSZ) bool { + f.mu.Lock() + defer f.mu.Unlock() + relevantDeltas := map[uint64]randaoDelta{} + currentBlockRoot := blockRoot + var currentSlot uint64 + for { + h, ok := f.forkGraph.GetHeader(currentBlockRoot) + if !ok { + return false + } + currentSlot = h.Slot + if f.randaoMixesLists.Contains(currentBlockRoot) { + break + } + randaoDelta, ok := f.randaoDeltas.Get(currentBlockRoot) + if !ok { + return false + } + currentBlockRoot = h.ParentRoot + if _, ok := relevantDeltas[currentSlot/f.beaconCfg.SlotsPerEpoch]; !ok { + relevantDeltas[currentSlot/f.beaconCfg.SlotsPerEpoch] = randaoDelta + } + } + randaoMixes, ok := f.randaoMixesLists.Get(currentBlockRoot) + if !ok { + return false + } + randaoMixes.CopyTo(out) + for epoch, delta := range relevantDeltas { + out.Set(int(epoch%f.beaconCfg.EpochsPerHistoricalVector), delta.delta) + } + return true +} + +func (f *ForkChoiceStore) Partecipation(epoch uint64) (*solid.BitList, bool) { + return f.participation.Get(epoch) +} diff --git a/cl/phase1/forkchoice/forkchoice_mock.go b/cl/phase1/forkchoice/forkchoice_mock.go index 0e4c717c18a..3b2b35eccc6 100644 --- a/cl/phase1/forkchoice/forkchoice_mock.go +++ b/cl/phase1/forkchoice/forkchoice_mock.go @@ -59,6 +59,8 @@ type ForkChoiceStorageMock struct { SlotVal uint64 TimeVal uint64 + ParticipationVal *solid.BitList + StateAtBlockRootVal map[common.Hash]*state.CachingBeaconState StateAtSlotVal map[uint64]*state.CachingBeaconState GetSyncCommitteesVal map[common.Hash][2]*solid.SyncCommittee @@ -181,3 +183,15 @@ func (f *ForkChoiceStorageMock) BlockRewards(root common.Hash) (*eth2.BlockRewar func (f *ForkChoiceStorageMock) TotalActiveBalance(root common.Hash) (uint64, bool) { panic("implement me") } + +func (f *ForkChoiceStorageMock) RandaoMixes(blockRoot common.Hash, out solid.HashListSSZ) bool { + return false +} + +func (f *ForkChoiceStorageMock) LowestAvaiableSlot() uint64 { + return f.FinalizedSlotVal +} + +func (f *ForkChoiceStorageMock) Partecipation(epoch uint64) (*solid.BitList, bool) { + return f.ParticipationVal, f.ParticipationVal != nil +} diff --git a/cl/phase1/forkchoice/interface.go b/cl/phase1/forkchoice/interface.go index e61583304ac..60320b4c715 100644 --- a/cl/phase1/forkchoice/interface.go +++ b/cl/phase1/forkchoice/interface.go @@ -21,6 +21,7 @@ type ForkChoiceStorageReader interface { Engine() execution_client.ExecutionEngine FinalizedCheckpoint() solid.Checkpoint FinalizedSlot() uint64 + LowestAvaiableSlot() uint64 GetEth1Hash(eth2Root common.Hash) common.Hash GetHead() (common.Hash, uint64, error) HighestSeen() uint64 @@ -32,6 +33,8 @@ type ForkChoiceStorageReader interface { GetSyncCommittees(blockRoot libcommon.Hash) (*solid.SyncCommittee, *solid.SyncCommittee, bool) Slot() uint64 Time() uint64 + Partecipation(epoch uint64) (*solid.BitList, bool) + RandaoMixes(blockRoot libcommon.Hash, out solid.HashListSSZ) bool BlockRewards(root libcommon.Hash) (*eth2.BlockRewardsCollector, bool) TotalActiveBalance(root libcommon.Hash) (uint64, bool) diff --git a/cl/phase1/forkchoice/on_block.go b/cl/phase1/forkchoice/on_block.go index 8d36f49b45a..06b28c5e772 100644 --- a/cl/phase1/forkchoice/on_block.go +++ b/cl/phase1/forkchoice/on_block.go @@ -8,7 +8,9 @@ import ( "github.com/ledgerwatch/log/v3" "github.com/ledgerwatch/erigon/cl/cltypes" + "github.com/ledgerwatch/erigon/cl/cltypes/solid" "github.com/ledgerwatch/erigon/cl/freezer" + "github.com/ledgerwatch/erigon/cl/phase1/core/state" "github.com/ledgerwatch/erigon/cl/phase1/forkchoice/fork_graph" "github.com/ledgerwatch/erigon/cl/transition/impl/eth2/statechange" ) @@ -74,7 +76,17 @@ func (f *ForkChoiceStore) OnBlock(block *cltypes.SignedBeaconBlock, newPayload, if err := freezer.PutObjectSSZIntoFreezer("beaconState", "caplin_core", lastProcessedState.Slot(), lastProcessedState, f.recorder); err != nil { return err } + // Update randao mixes + r := solid.NewHashVector(int(f.beaconCfg.EpochsPerHistoricalVector)) + lastProcessedState.RandaoMixes().CopyTo(r) + f.randaoMixesLists.Add(blockRoot, r) + } else { + f.randaoDeltas.Add(blockRoot, randaoDelta{ + epoch: state.Epoch(lastProcessedState), + delta: lastProcessedState.GetRandaoMixes(state.Epoch(lastProcessedState)), + }) } + f.participation.Add(state.Epoch(lastProcessedState), lastProcessedState.CurrentEpochParticipation().Copy()) f.preverifiedSizes.Add(blockRoot, preverifiedAppendListsSizes{ validatorLength: uint64(lastProcessedState.ValidatorLength()), historicalRootsLength: lastProcessedState.HistoricalRootsLength(),