diff --git a/app/eth2wrap/eth2wrap_gen.go b/app/eth2wrap/eth2wrap_gen.go index 6342f308b..93de9ef28 100644 --- a/app/eth2wrap/eth2wrap_gen.go +++ b/app/eth2wrap/eth2wrap_gen.go @@ -24,6 +24,7 @@ type Client interface { eth2exp.SyncCommitteeSelectionAggregator eth2exp.ProposerConfigProvider BlockAttestationsProvider + BeaconStateCommitteesProvider NodePeerCountProvider CachedValidatorsProvider diff --git a/app/eth2wrap/genwrap/genwrap.go b/app/eth2wrap/genwrap/genwrap.go index b2c115aa6..667b636b1 100644 --- a/app/eth2wrap/genwrap/genwrap.go +++ b/app/eth2wrap/genwrap/genwrap.go @@ -51,6 +51,7 @@ type Client interface { eth2exp.SyncCommitteeSelectionAggregator eth2exp.ProposerConfigProvider BlockAttestationsProvider + BeaconStateCommitteesProvider NodePeerCountProvider CachedValidatorsProvider diff --git a/app/eth2wrap/httpwrap.go b/app/eth2wrap/httpwrap.go index a0d89ef6b..9e1a135f9 100644 --- a/app/eth2wrap/httpwrap.go +++ b/app/eth2wrap/httpwrap.go @@ -11,6 +11,7 @@ import ( "io" "net/http" "net/url" + "strconv" "sync" "testing" "time" @@ -26,6 +27,7 @@ import ( "github.com/obolnetwork/charon/app/z" "github.com/obolnetwork/charon/eth2util" "github.com/obolnetwork/charon/eth2util/eth2exp" + "github.com/obolnetwork/charon/eth2util/statecomm" ) // BlockAttestationsProvider is the interface for providing attestations included in blocks. @@ -37,6 +39,13 @@ type BlockAttestationsProvider interface { BlockAttestationsV2(ctx context.Context, stateID string) ([]*spec.VersionedAttestation, error) } +// BeaconStateCommitteesProvider is the interface for providing committees for given slot. +// It is a standard beacon API endpoint not implemented by eth2client. +// See https://ethereum.github.io/beacon-APIs/#/Beacon/getEpochCommittees. +type BeaconStateCommitteesProvider interface { + BeaconStateCommittees(ctx context.Context, slot uint64) ([]*statecomm.StateCommittee, error) +} + // NodePeerCountProvider is the interface for providing node peer count. // It is a standard beacon API endpoint not implemented by eth2client. // See https://ethereum.github.io/beacon-APIs/#/Node/getPeerCount. @@ -184,7 +193,7 @@ func (h *httpAdapter) AggregateSyncCommitteeSelections(ctx context.Context, sele // See https://ethereum.github.io/beacon-APIs/#/Beacon/getBlockAttestations. func (h *httpAdapter) BlockAttestations(ctx context.Context, stateID string) ([]*eth2p0.Attestation, error) { path := fmt.Sprintf("/eth/v1/beacon/blocks/%s/attestations", stateID) - respBody, statusCode, err := httpGet(ctx, h.address, path, h.timeout) + respBody, statusCode, err := httpGet(ctx, h.address, path, nil, h.timeout) if err != nil { return nil, errors.Wrap(err, "request block attestations") } else if statusCode == http.StatusNotFound { @@ -205,21 +214,24 @@ func (h *httpAdapter) BlockAttestations(ctx context.Context, stateID string) ([] // See https://ethereum.github.io/beacon-APIs/#/Beacon/getBlockAttestationsV2. func (h *httpAdapter) BlockAttestationsV2(ctx context.Context, stateID string) ([]*spec.VersionedAttestation, error) { path := fmt.Sprintf("/eth/v2/beacon/blocks/%s/attestations", stateID) - resp, err := httpGetRaw(ctx, h.address, path, h.timeout) + ctx, cancel := context.WithTimeout(ctx, h.timeout) + defer cancel() + + resp, err := httpGetRaw(ctx, h.address, path, nil) if err != nil { return nil, errors.Wrap(err, "request block attestations") } defer resp.Body.Close() - respBody, err := io.ReadAll(resp.Body) - if err != nil { - return nil, errors.Wrap(err, "request block attestations body") - } - if resp.StatusCode == http.StatusNotFound { return nil, nil // No block for slot, so no attestations. } else if resp.StatusCode != http.StatusOK { - return nil, errors.New("request block attestations failed", z.Int("status", resp.StatusCode), z.Str("body", string(respBody))) + return nil, errors.New("request block attestations failed", z.Int("status", resp.StatusCode)) + } + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, errors.Wrap(err, "request block attestations body") } version, err := fetchConsensusVersion(resp) @@ -284,9 +296,35 @@ func (h *httpAdapter) BlockAttestationsV2(ctx context.Context, stateID string) ( return res, nil } +// BeaconStateCommittees returns the attestations included in the requested block. +// See https://ethereum.github.io/beacon-APIs/#/Beacon/getStateValidators. +func (h *httpAdapter) BeaconStateCommittees(ctx context.Context, slot uint64) ([]*statecomm.StateCommittee, error) { + r := strconv.FormatUint(slot, 10) + path := fmt.Sprintf("/eth/v1/beacon/states/%v/committees", r) + queryParams := map[string]string{ + "slot": strconv.FormatUint(slot, 10), + } + respBody, statusCode, err := httpGet(ctx, h.address, path, queryParams, h.timeout) + if err != nil { + return nil, errors.Wrap(err, "request state committees for slot", z.Int("status", statusCode), z.U64("slot", slot)) + } + + if statusCode != http.StatusOK { + return nil, errors.New("request state committees for slot failed", z.Int("status", statusCode), z.U64("slot", slot)) + } + + var res statecomm.StateCommitteesResponse + err = json.Unmarshal(respBody, &res) + if err != nil { + return nil, errors.Wrap(err, "unmarshal state committees", z.Int("status", statusCode), z.U64("slot", slot)) + } + + return res.Data, nil +} + // ProposerConfig implements eth2exp.ProposerConfigProvider. func (h *httpAdapter) ProposerConfig(ctx context.Context) (*eth2exp.ProposerConfigResponse, error) { - respBody, statusCode, err := httpGet(ctx, h.address, "/proposer_config", h.timeout) + respBody, statusCode, err := httpGet(ctx, h.address, "/proposer_config", nil, h.timeout) if err != nil { return nil, errors.Wrap(err, "submit sync committee selections") } else if statusCode != http.StatusOK { @@ -305,7 +343,7 @@ func (h *httpAdapter) ProposerConfig(ctx context.Context) (*eth2exp.ProposerConf // See https://ethereum.github.io/beacon-APIs/#/Node/getPeerCount. func (h *httpAdapter) NodePeerCount(ctx context.Context) (int, error) { const path = "/eth/v1/node/peer_count" - respBody, statusCode, err := httpGet(ctx, h.address, path, h.timeout) + respBody, statusCode, err := httpGet(ctx, h.address, path, nil, h.timeout) if err != nil { return 0, errors.Wrap(err, "request beacon node peer count") } else if statusCode != http.StatusOK { @@ -400,10 +438,7 @@ func httpPost(ctx context.Context, base string, endpoint string, body io.Reader, } // httpGetRaw performs a GET request and returns the raw http response or an error. -func httpGetRaw(ctx context.Context, base string, endpoint string, timeout time.Duration) (*http.Response, error) { - ctx, cancel := context.WithTimeout(ctx, timeout) - defer cancel() - +func httpGetRaw(ctx context.Context, base string, endpoint string, queryParams map[string]string) (*http.Response, error) { addr, err := url.JoinPath(base, endpoint) if err != nil { return nil, errors.Wrap(err, "invalid address") @@ -419,6 +454,12 @@ func httpGetRaw(ctx context.Context, base string, endpoint string, timeout time. return nil, errors.Wrap(err, "new GET request with ctx") } + q := req.URL.Query() + for key, val := range queryParams { + q.Add(key, val) + } + req.URL.RawQuery = q.Encode() + res, err := new(http.Client).Do(req) if err != nil { return nil, errors.Wrap(err, "failed to call GET endpoint") @@ -428,8 +469,11 @@ func httpGetRaw(ctx context.Context, base string, endpoint string, timeout time. } // httpGet performs a GET request and returns the body and status code or an error. -func httpGet(ctx context.Context, base string, endpoint string, timeout time.Duration) ([]byte, int, error) { - res, err := httpGetRaw(ctx, base, endpoint, timeout) +func httpGet(ctx context.Context, base string, endpoint string, queryParams map[string]string, timeout time.Duration) ([]byte, int, error) { + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + res, err := httpGetRaw(ctx, base, endpoint, queryParams) if err != nil { return nil, 0, errors.Wrap(err, "failed to read GET response") } @@ -437,7 +481,7 @@ func httpGet(ctx context.Context, base string, endpoint string, timeout time.Dur data, err := io.ReadAll(res.Body) if err != nil { - return nil, 0, errors.Wrap(err, "failed to read GET response body") + return nil, res.StatusCode, errors.Wrap(err, "failed to read GET response body") } return data, res.StatusCode, nil diff --git a/app/eth2wrap/lazy.go b/app/eth2wrap/lazy.go index a6d5adbaf..f5f0eefd0 100644 --- a/app/eth2wrap/lazy.go +++ b/app/eth2wrap/lazy.go @@ -11,6 +11,7 @@ import ( eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/obolnetwork/charon/eth2util/eth2exp" + "github.com/obolnetwork/charon/eth2util/statecomm" ) //go:generate mockery --name=Client --output=mocks --outpkg=mocks --case=underscore @@ -212,6 +213,15 @@ func (l *lazy) BlockAttestationsV2(ctx context.Context, stateID string) ([]*spec return cl.BlockAttestationsV2(ctx, stateID) } +func (l *lazy) BeaconStateCommittees(ctx context.Context, slot uint64) ([]*statecomm.StateCommittee, error) { + cl, err := l.getOrCreateClient(ctx) + if err != nil { + return nil, err + } + + return cl.BeaconStateCommittees(ctx, slot) +} + func (l *lazy) NodePeerCount(ctx context.Context) (int, error) { cl, err := l.getOrCreateClient(ctx) if err != nil { diff --git a/app/eth2wrap/mocks/client.go b/app/eth2wrap/mocks/client.go index f716ca26d..25d5de77f 100644 --- a/app/eth2wrap/mocks/client.go +++ b/app/eth2wrap/mocks/client.go @@ -23,6 +23,8 @@ import ( time "time" v1 "github.com/attestantio/go-eth2-client/api/v1" + + "github.com/obolnetwork/charon/eth2util/statecomm" ) // Client is an autogenerated mock type for the Client type @@ -319,6 +321,36 @@ func (_m *Client) BlockAttestationsV2(ctx context.Context, stateID string) ([]*s return r0, r1 } +// BeaconStateCommittees provides a mock function with given fields: ctx, slot +func (_m *Client) BeaconStateCommittees(ctx context.Context, slot uint64) ([]*statecomm.StateCommittee, error) { + ret := _m.Called(ctx, slot) + + if len(ret) == 0 { + panic("no return value specified for BeaconStateCommittees") + } + + var r0 []*statecomm.StateCommittee + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uint64) ([]*statecomm.StateCommittee, error)); ok { + return rf(ctx, slot) + } + if rf, ok := ret.Get(0).(func(context.Context, uint64) []*statecomm.StateCommittee); ok { + r0 = rf(ctx, slot) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*statecomm.StateCommittee) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, uint64) error); ok { + r1 = rf(ctx, slot) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // CompleteValidators provides a mock function with given fields: ctx func (_m *Client) CompleteValidators(ctx context.Context) (eth2wrap.CompleteValidators, error) { ret := _m.Called(ctx) diff --git a/app/eth2wrap/multi.go b/app/eth2wrap/multi.go index 6da9a729e..fcf192138 100644 --- a/app/eth2wrap/multi.go +++ b/app/eth2wrap/multi.go @@ -9,6 +9,7 @@ import ( eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/obolnetwork/charon/eth2util/eth2exp" + "github.com/obolnetwork/charon/eth2util/statecomm" ) // NewMultiForT creates a new mutil client for testing. @@ -213,6 +214,24 @@ func (m multi) BlockAttestationsV2(ctx context.Context, stateID string) ([]*spec return res, err } +func (m multi) BeaconStateCommittees(ctx context.Context, slot uint64) ([]*statecomm.StateCommittee, error) { + const label = "beacon_state_committees" + defer latency(ctx, label, false)() + + res, err := provide(ctx, m.clients, m.fallbacks, + func(ctx context.Context, args provideArgs) ([]*statecomm.StateCommittee, error) { + return args.client.BeaconStateCommittees(ctx, slot) + }, + nil, m.selector, + ) + if err != nil { + incError(label) + err = wrapError(ctx, err, label) + } + + return res, err +} + func (m multi) NodePeerCount(ctx context.Context) (int, error) { const label = "node_peer_count" defer latency(ctx, label, false)() diff --git a/core/signeddata_test.go b/core/signeddata_test.go index 5b66497d2..ed1afafef 100644 --- a/core/signeddata_test.go +++ b/core/signeddata_test.go @@ -272,7 +272,7 @@ func TestSignedDataSetSignature(t *testing.T) { Version: eth2spec.DataVersionPhase0, Phase0: ð2p0.Attestation{ AggregationBits: testutil.RandomBitList(1), - Data: testutil.RandomAttestationData(), + Data: testutil.RandomAttestationDataPhase0(), Signature: testutil.RandomEth2Signature(), }, }, @@ -285,7 +285,7 @@ func TestSignedDataSetSignature(t *testing.T) { Version: eth2spec.DataVersionAltair, Altair: ð2p0.Attestation{ AggregationBits: testutil.RandomBitList(1), - Data: testutil.RandomAttestationData(), + Data: testutil.RandomAttestationDataPhase0(), Signature: testutil.RandomEth2Signature(), }, }, @@ -298,7 +298,7 @@ func TestSignedDataSetSignature(t *testing.T) { Version: eth2spec.DataVersionBellatrix, Bellatrix: ð2p0.Attestation{ AggregationBits: testutil.RandomBitList(1), - Data: testutil.RandomAttestationData(), + Data: testutil.RandomAttestationDataPhase0(), Signature: testutil.RandomEth2Signature(), }, }, @@ -311,7 +311,7 @@ func TestSignedDataSetSignature(t *testing.T) { Version: eth2spec.DataVersionCapella, Capella: ð2p0.Attestation{ AggregationBits: testutil.RandomBitList(1), - Data: testutil.RandomAttestationData(), + Data: testutil.RandomAttestationDataPhase0(), Signature: testutil.RandomEth2Signature(), }, }, @@ -324,7 +324,7 @@ func TestSignedDataSetSignature(t *testing.T) { Version: eth2spec.DataVersionDeneb, Deneb: ð2p0.Attestation{ AggregationBits: testutil.RandomBitList(1), - Data: testutil.RandomAttestationData(), + Data: testutil.RandomAttestationDataPhase0(), Signature: testutil.RandomEth2Signature(), }, }, @@ -337,7 +337,7 @@ func TestSignedDataSetSignature(t *testing.T) { Version: eth2spec.DataVersionElectra, Electra: &electra.Attestation{ AggregationBits: testutil.RandomBitList(1), - Data: testutil.RandomAttestationData(), + Data: testutil.RandomAttestationDataPhase0(), Signature: testutil.RandomEth2Signature(), CommitteeBits: testutil.RandomBitVec64(), }, @@ -747,7 +747,7 @@ func TestVersionedSignedProposal(t *testing.T) { } func TestVersionedSignedAggregateAndProofUtilFunctions(t *testing.T) { - data := testutil.RandomAttestationData() + data := testutil.RandomAttestationDataPhase0() aggregationBits := testutil.RandomBitList(64) type testCase struct { name string diff --git a/core/tracker/inclusion.go b/core/tracker/inclusion.go index e3620d795..b7883b449 100644 --- a/core/tracker/inclusion.go +++ b/core/tracker/inclusion.go @@ -19,6 +19,7 @@ import ( "github.com/obolnetwork/charon/app/log" "github.com/obolnetwork/charon/app/z" "github.com/obolnetwork/charon/core" + "github.com/obolnetwork/charon/eth2util/statecomm" ) const ( @@ -53,6 +54,13 @@ type submission struct { type block struct { Slot uint64 AttestationsByDataRoot map[eth2p0.Root]*eth2spec.VersionedAttestation + BeaconCommitees []*statecomm.StateCommittee +} + +// attCommittee is a versioned attestation with its aggregation bits mapped to the respective beacon committee +type attCommittee struct { + Attestation *eth2spec.VersionedAttestation + CommitteeAggregations map[eth2p0.CommitteeIndex]bitfield.Bitlist } // trackerInclFunc defines the tracker callback for the inclusion checker. @@ -277,24 +285,36 @@ func checkAttestationInclusion(sub submission, block block) (bool, error) { if !ok { return false, nil } - - attAggregationBits, err := att.AggregationBits() + attAggBits, err := att.AggregationBits() if err != nil { return false, errors.Wrap(err, "get attestation aggregation bits") } - subBits, err := sub.Data.(core.VersionedAttestation).AggregationBits() + + subData, ok := sub.Data.(core.VersionedAttestation) + if !ok { + return false, errors.New("invalid attestation") + } + subAggBits, err := subData.AggregationBits() if err != nil { return false, errors.Wrap(err, "get attestation aggregation bits") } - ok, err = attAggregationBits.Contains(subBits) + if len(subAggBits.BitIndices()) != 1 { + return false, errors.New("unexpected number of aggregation bits") + } + subAggIdx := subAggBits.BitIndices()[0] + subCommIdx, err := subData.CommitteeIndex() if err != nil { - return false, errors.Wrap(err, "check aggregation bits", - z.U64("block_bits", attAggregationBits.Len()), - z.U64("sub_bits", subBits.Len()), - ) + return false, errors.Wrap(err, "get committee index") } - return ok, nil + // Calculate the length of validators of committees before the committee index of the submitted attestation. + previousCommsValidatorsLen := 0 + for idx := range subCommIdx { + previousCommsValidatorsLen += len(block.BeaconCommitees[idx].Validators) + } + + // Previous committees validators length + validator index in attestation committee gives the index of the attestation in the full agreggation bits bitlist. + return attAggBits.BitAt(uint64(previousCommsValidatorsLen) + uint64(subAggIdx)), nil } // reportMissed reports duties that were broadcast but never included on chain. @@ -460,8 +480,22 @@ func (a *InclusionChecker) checkBlock(ctx context.Context, slot uint64) error { return nil // No block for this slot } - // Map attestations by data root, merging duplicates (with identical attestation data). - attsMap := make(map[eth2p0.Root]*eth2spec.VersionedAttestation) + // Get the slot for which the attestations in the current slot are. + // This is usually the previous slot, except when the previous is a missed proposal. + attestation0Data, err := atts[0].Data() + if err != nil { + return err + } + attestedSlot := attestation0Data.Slot + + // Get the beacon committee for the above mentioned slot. + committeesForState, err := a.eth2Cl.BeaconStateCommittees(ctx, uint64(attestedSlot)) + if err != nil { + return err + } + + // Map attestations by data root, merging duplicates' aggregation bits. + attsCommitteesMap := make(map[eth2p0.Root]*attCommittee) for _, att := range atts { if att == nil { return errors.New("invalid attestation") @@ -486,31 +520,34 @@ func (a *InclusionChecker) checkBlock(ctx context.Context, slot uint64) error { return err } - attAggregationBits, err := att.AggregationBits() + attCommittee := &attCommittee{ + Attestation: att, + } + committeeAggregations, err := conjugateAggregationBits(attCommittee, attsCommitteesMap, root, committeesForState) if err != nil { - return errors.Wrap(err, "get attestation aggregation bits") + return err } + attCommittee.CommitteeAggregations = committeeAggregations + attsCommitteesMap[root] = attCommittee + } - if exist, ok := attsMap[root]; ok { - existAttAggregationBits, err := exist.AggregationBits() - if err != nil { - return errors.Wrap(err, "get attestation aggregation bits") - } - // Merge duplicate attestations (only aggregation bits) - bits, err := attAggregationBits.Or(existAttAggregationBits) - if err != nil { - return errors.Wrap(err, "merge attestation aggregation bits") + attsMap := make(map[eth2p0.Root]*eth2spec.VersionedAttestation) + for root, att := range attsCommitteesMap { + unwrapedAtt := att.Attestation + if att.CommitteeAggregations != nil { + aggBits := bitfield.Bitlist{} + for _, commBits := range att.CommitteeAggregations { + aggBits = append(aggBits, commBits...) } - err = setAttestationAggregationBits(*att, bits) + err = setAttestationAggregationBits(*unwrapedAtt, aggBits) if err != nil { - return errors.Wrap(err, "set attestation aggregation bits") + return err } } - - attsMap[root] = att + attsMap[root] = unwrapedAtt } - a.checkBlockFunc(ctx, block{Slot: slot, AttestationsByDataRoot: attsMap}) + a.checkBlockFunc(ctx, block{Slot: slot, AttestationsByDataRoot: attsMap, BeaconCommitees: committeesForState}) return nil } @@ -612,3 +649,116 @@ func setAttestationAggregationBits(att eth2spec.VersionedAttestation, bits bitfi return errors.New("unknown attestation version", z.Str("version", att.Version.String())) } } + +func conjugateAggregationBits(att *attCommittee, attsMap map[eth2p0.Root]*attCommittee, root eth2p0.Root, committeesForState []*statecomm.StateCommittee) (map[eth2p0.CommitteeIndex]bitfield.Bitlist, error) { + switch att.Attestation.Version { + case eth2spec.DataVersionPhase0: + if att.Attestation.Phase0 == nil { + return nil, errors.New("no Phase0 attestation") + } + + return nil, conjugateAggregationBitsPhase0(att, attsMap, root) + case eth2spec.DataVersionAltair: + if att.Attestation.Altair == nil { + return nil, errors.New("no Altair attestation") + } + + return nil, conjugateAggregationBitsPhase0(att, attsMap, root) + case eth2spec.DataVersionBellatrix: + if att.Attestation.Bellatrix == nil { + return nil, errors.New("no Bellatrix attestation") + } + + return nil, conjugateAggregationBitsPhase0(att, attsMap, root) + case eth2spec.DataVersionCapella: + if att.Attestation.Capella == nil { + return nil, errors.New("no Capella attestation") + } + + return nil, conjugateAggregationBitsPhase0(att, attsMap, root) + case eth2spec.DataVersionDeneb: + if att.Attestation.Deneb == nil { + return nil, errors.New("no Deneb attestation") + } + + return nil, conjugateAggregationBitsPhase0(att, attsMap, root) + case eth2spec.DataVersionElectra: + if att.Attestation.Electra == nil { + return nil, errors.New("no Electra attestation") + } + + return conjugateAggregationBitsElectra(att, attsMap, root, committeesForState) + default: + return nil, errors.New("unknown attestation version", z.Str("version", att.Attestation.Version.String())) + } +} + +func conjugateAggregationBitsPhase0(att *attCommittee, attsMap map[eth2p0.Root]*attCommittee, root eth2p0.Root) error { + attAggregationBits, err := att.Attestation.AggregationBits() + if err != nil { + return errors.Wrap(err, "get attestation aggregation bits") + } + + if exist, ok := attsMap[root]; ok { + existAttAggregationBits, err := exist.Attestation.AggregationBits() + if err != nil { + return errors.Wrap(err, "get attestation aggregation bits") + } + // Merge duplicate attestations (only aggregation bits). + bits, err := attAggregationBits.Or(existAttAggregationBits) + if err != nil { + return errors.Wrap(err, "merge attestation aggregation bits") + } + err = setAttestationAggregationBits(*att.Attestation, bits) + if err != nil { + return errors.Wrap(err, "set attestation aggregation bits") + } + } + + return nil +} + +func conjugateAggregationBitsElectra(att *attCommittee, attsMap map[eth2p0.Root]*attCommittee, root eth2p0.Root, committeesForState []*statecomm.StateCommittee) (map[eth2p0.CommitteeIndex]bitfield.Bitlist, error) { + fullAttestationAggregationBits, err := att.Attestation.AggregationBits() + if err != nil { + return nil, err + } + committeeBits, err := att.Attestation.CommitteeBits() + if err != nil { + return nil, err + } + + var updated map[eth2p0.CommitteeIndex]bitfield.Bitlist + if exist, ok := attsMap[root]; ok { + updated = updateAggregationBits(committeeBits, exist.CommitteeAggregations, fullAttestationAggregationBits) + } else { + // Create new empty map of committee indices and aggregations per committee. + attsAggBits := make(map[eth2p0.CommitteeIndex]bitfield.Bitlist) + // Create a 0'ed bitlist of aggregations of size the amount of validators for all committees. + for _, comm := range committeesForState { + attsAggBits[comm.Index] = bitfield.NewBitlist(uint64(len(comm.Validators))) + } + + updated = updateAggregationBits(committeeBits, attsAggBits, fullAttestationAggregationBits) + } + + return updated, nil +} + +func updateAggregationBits(committeeBits bitfield.Bitvector64, committeeAggregation map[eth2p0.CommitteeIndex]bitfield.Bitlist, fullAttestationAggregationBits bitfield.Bitlist) map[eth2p0.CommitteeIndex]bitfield.Bitlist { + offset := uint64(0) + // Iterate over all committees that attested in the current attestation object. + for _, committeeIndex := range committeeBits.BitIndices() { + validatorsInCommittee := committeeAggregation[eth2p0.CommitteeIndex(committeeIndex)].Len() + // Iterate over all validators in the committee. + for idx := range validatorsInCommittee { + // Update the existing map if the said validator attested. + if fullAttestationAggregationBits.BitAt(offset + idx) { + committeeAggregation[eth2p0.CommitteeIndex(committeeIndex)].SetBitAt(idx, fullAttestationAggregationBits.BitAt(offset+idx)) + } + } + offset += validatorsInCommittee + } + + return committeeAggregation +} diff --git a/core/tracker/inclusion_internal_test.go b/core/tracker/inclusion_internal_test.go index 4eef23b31..c67da3a36 100644 --- a/core/tracker/inclusion_internal_test.go +++ b/core/tracker/inclusion_internal_test.go @@ -5,6 +5,7 @@ package tracker import ( "context" "math/rand" + "slices" "testing" eth2spec "github.com/attestantio/go-eth2-client/spec" @@ -15,6 +16,7 @@ import ( "github.com/obolnetwork/charon/app/eth2wrap" "github.com/obolnetwork/charon/core" + "github.com/obolnetwork/charon/eth2util/statecomm" "github.com/obolnetwork/charon/testutil" "github.com/obolnetwork/charon/testutil/beaconmock" ) @@ -23,66 +25,137 @@ func TestDuplicateAttData(t *testing.T) { ctx := context.Background() tests := []struct { - name string - attestationsFunc func(*eth2p0.AttestationData, bitfield.Bitlist, bitfield.Bitlist, bitfield.Bitlist) []*eth2spec.VersionedAttestation + name string + attData *eth2p0.AttestationData + attestationsFunc func(*eth2p0.AttestationData, bitfield.Bitlist, bitfield.Bitlist, bitfield.Bitlist) []*eth2spec.VersionedAttestation + beaconStateCommitteesFunc func(*eth2p0.AttestationData) []*statecomm.StateCommittee }{ { - name: "phase0", - attestationsFunc: func(attData *eth2p0.AttestationData, bits1 bitfield.Bitlist, bits2 bitfield.Bitlist, bits3 bitfield.Bitlist) []*eth2spec.VersionedAttestation { + name: "phase0", + attData: testutil.RandomAttestationDataPhase0(), + attestationsFunc: func(attData *eth2p0.AttestationData, aggBits1 bitfield.Bitlist, aggBits2 bitfield.Bitlist, aggBits3 bitfield.Bitlist) []*eth2spec.VersionedAttestation { return []*eth2spec.VersionedAttestation{ - {Version: eth2spec.DataVersionPhase0, Phase0: ð2p0.Attestation{AggregationBits: bits1, Data: attData}}, - {Version: eth2spec.DataVersionPhase0, Phase0: ð2p0.Attestation{AggregationBits: bits2, Data: attData}}, - {Version: eth2spec.DataVersionPhase0, Phase0: ð2p0.Attestation{AggregationBits: bits3, Data: attData}}, + {Version: eth2spec.DataVersionPhase0, Phase0: ð2p0.Attestation{AggregationBits: aggBits1, Data: attData}}, + {Version: eth2spec.DataVersionPhase0, Phase0: ð2p0.Attestation{AggregationBits: aggBits2, Data: attData}}, + {Version: eth2spec.DataVersionPhase0, Phase0: ð2p0.Attestation{AggregationBits: aggBits3, Data: attData}}, + } + }, + beaconStateCommitteesFunc: func(attData *eth2p0.AttestationData) []*statecomm.StateCommittee { + return []*statecomm.StateCommittee{ + {Index: attData.Index, Slot: attData.Slot, Validators: []eth2p0.ValidatorIndex{0, 1}}, + } + }, + }, + { + name: "altair", + attData: testutil.RandomAttestationDataPhase0(), + attestationsFunc: func(attData *eth2p0.AttestationData, aggBits1 bitfield.Bitlist, aggBits2 bitfield.Bitlist, aggBits3 bitfield.Bitlist) []*eth2spec.VersionedAttestation { + return []*eth2spec.VersionedAttestation{ + {Version: eth2spec.DataVersionAltair, Altair: ð2p0.Attestation{AggregationBits: aggBits1, Data: attData}}, + {Version: eth2spec.DataVersionAltair, Altair: ð2p0.Attestation{AggregationBits: aggBits2, Data: attData}}, + {Version: eth2spec.DataVersionAltair, Altair: ð2p0.Attestation{AggregationBits: aggBits3, Data: attData}}, + } + }, + beaconStateCommitteesFunc: func(attData *eth2p0.AttestationData) []*statecomm.StateCommittee { + return []*statecomm.StateCommittee{ + {Index: attData.Index, Slot: attData.Slot, Validators: []eth2p0.ValidatorIndex{0, 1}}, } }, }, { - name: "altair", - attestationsFunc: func(attData *eth2p0.AttestationData, bits1 bitfield.Bitlist, bits2 bitfield.Bitlist, bits3 bitfield.Bitlist) []*eth2spec.VersionedAttestation { + name: "bellatrix", + attData: testutil.RandomAttestationDataPhase0(), + attestationsFunc: func(attData *eth2p0.AttestationData, aggBits1 bitfield.Bitlist, aggBits2 bitfield.Bitlist, aggBits3 bitfield.Bitlist) []*eth2spec.VersionedAttestation { return []*eth2spec.VersionedAttestation{ - {Version: eth2spec.DataVersionAltair, Altair: ð2p0.Attestation{AggregationBits: bits1, Data: attData}}, - {Version: eth2spec.DataVersionAltair, Altair: ð2p0.Attestation{AggregationBits: bits2, Data: attData}}, - {Version: eth2spec.DataVersionAltair, Altair: ð2p0.Attestation{AggregationBits: bits3, Data: attData}}, + {Version: eth2spec.DataVersionBellatrix, Bellatrix: ð2p0.Attestation{AggregationBits: aggBits1, Data: attData}}, + {Version: eth2spec.DataVersionBellatrix, Bellatrix: ð2p0.Attestation{AggregationBits: aggBits2, Data: attData}}, + {Version: eth2spec.DataVersionBellatrix, Bellatrix: ð2p0.Attestation{AggregationBits: aggBits3, Data: attData}}, + } + }, + beaconStateCommitteesFunc: func(attData *eth2p0.AttestationData) []*statecomm.StateCommittee { + return []*statecomm.StateCommittee{ + {Index: attData.Index, Slot: attData.Slot, Validators: []eth2p0.ValidatorIndex{0, 1}}, } }, }, { - name: "bellatrix", - attestationsFunc: func(attData *eth2p0.AttestationData, bits1 bitfield.Bitlist, bits2 bitfield.Bitlist, bits3 bitfield.Bitlist) []*eth2spec.VersionedAttestation { + name: "capella", + attData: testutil.RandomAttestationDataPhase0(), + attestationsFunc: func(attData *eth2p0.AttestationData, aggBits1 bitfield.Bitlist, aggBits2 bitfield.Bitlist, aggBits3 bitfield.Bitlist) []*eth2spec.VersionedAttestation { return []*eth2spec.VersionedAttestation{ - {Version: eth2spec.DataVersionBellatrix, Bellatrix: ð2p0.Attestation{AggregationBits: bits1, Data: attData}}, - {Version: eth2spec.DataVersionBellatrix, Bellatrix: ð2p0.Attestation{AggregationBits: bits2, Data: attData}}, - {Version: eth2spec.DataVersionBellatrix, Bellatrix: ð2p0.Attestation{AggregationBits: bits3, Data: attData}}, + {Version: eth2spec.DataVersionCapella, Capella: ð2p0.Attestation{AggregationBits: aggBits1, Data: attData}}, + {Version: eth2spec.DataVersionCapella, Capella: ð2p0.Attestation{AggregationBits: aggBits2, Data: attData}}, + {Version: eth2spec.DataVersionCapella, Capella: ð2p0.Attestation{AggregationBits: aggBits3, Data: attData}}, + } + }, + beaconStateCommitteesFunc: func(attData *eth2p0.AttestationData) []*statecomm.StateCommittee { + return []*statecomm.StateCommittee{ + {Index: attData.Index, Slot: attData.Slot, Validators: []eth2p0.ValidatorIndex{0, 1}}, } }, }, { - name: "capella", - attestationsFunc: func(attData *eth2p0.AttestationData, bits1 bitfield.Bitlist, bits2 bitfield.Bitlist, bits3 bitfield.Bitlist) []*eth2spec.VersionedAttestation { + name: "deneb", + attData: testutil.RandomAttestationDataPhase0(), + attestationsFunc: func(attData *eth2p0.AttestationData, aggBits1 bitfield.Bitlist, aggBits2 bitfield.Bitlist, aggBits3 bitfield.Bitlist) []*eth2spec.VersionedAttestation { return []*eth2spec.VersionedAttestation{ - {Version: eth2spec.DataVersionCapella, Capella: ð2p0.Attestation{AggregationBits: bits1, Data: attData}}, - {Version: eth2spec.DataVersionCapella, Capella: ð2p0.Attestation{AggregationBits: bits2, Data: attData}}, - {Version: eth2spec.DataVersionCapella, Capella: ð2p0.Attestation{AggregationBits: bits3, Data: attData}}, + {Version: eth2spec.DataVersionDeneb, Deneb: ð2p0.Attestation{AggregationBits: aggBits1, Data: attData}}, + {Version: eth2spec.DataVersionDeneb, Deneb: ð2p0.Attestation{AggregationBits: aggBits2, Data: attData}}, + {Version: eth2spec.DataVersionDeneb, Deneb: ð2p0.Attestation{AggregationBits: aggBits3, Data: attData}}, + } + }, + beaconStateCommitteesFunc: func(attData *eth2p0.AttestationData) []*statecomm.StateCommittee { + return []*statecomm.StateCommittee{ + {Index: attData.Index, Slot: attData.Slot, Validators: []eth2p0.ValidatorIndex{0, 1}}, } }, }, { - name: "deneb", - attestationsFunc: func(attData *eth2p0.AttestationData, bits1 bitfield.Bitlist, bits2 bitfield.Bitlist, bits3 bitfield.Bitlist) []*eth2spec.VersionedAttestation { + name: "electra", + attData: testutil.RandomAttestationDataElectra(), + attestationsFunc: func(attData *eth2p0.AttestationData, aggBits1 bitfield.Bitlist, aggBits2 bitfield.Bitlist, aggBits3 bitfield.Bitlist) []*eth2spec.VersionedAttestation { + zeroComm := bitfield.NewBitvector64() + zeroComm.SetBitAt(0, true) + oneComm := bitfield.NewBitvector64() + oneComm.SetBitAt(1, true) + twoComm := bitfield.NewBitvector64() + twoComm.SetBitAt(2, true) + return []*eth2spec.VersionedAttestation{ - {Version: eth2spec.DataVersionDeneb, Deneb: ð2p0.Attestation{AggregationBits: bits1, Data: attData}}, - {Version: eth2spec.DataVersionDeneb, Deneb: ð2p0.Attestation{AggregationBits: bits2, Data: attData}}, - {Version: eth2spec.DataVersionDeneb, Deneb: ð2p0.Attestation{AggregationBits: bits3, Data: attData}}, + {Version: eth2spec.DataVersionElectra, Electra: &electra.Attestation{AggregationBits: aggBits1, Data: attData, CommitteeBits: zeroComm}}, + {Version: eth2spec.DataVersionElectra, Electra: &electra.Attestation{AggregationBits: aggBits2, Data: attData, CommitteeBits: oneComm}}, + {Version: eth2spec.DataVersionElectra, Electra: &electra.Attestation{AggregationBits: aggBits3, Data: attData, CommitteeBits: twoComm}}, + } + }, + beaconStateCommitteesFunc: func(attData *eth2p0.AttestationData) []*statecomm.StateCommittee { + return []*statecomm.StateCommittee{ + {Index: 0, Slot: attData.Slot, Validators: []eth2p0.ValidatorIndex{0, 1, 2}}, + {Index: 1, Slot: attData.Slot, Validators: []eth2p0.ValidatorIndex{0, 1, 2}}, + {Index: 2, Slot: attData.Slot, Validators: []eth2p0.ValidatorIndex{0, 1, 2}}, } }, }, { - name: "electra", - attestationsFunc: func(attData *eth2p0.AttestationData, bits1 bitfield.Bitlist, bits2 bitfield.Bitlist, bits3 bitfield.Bitlist) []*eth2spec.VersionedAttestation { + name: "electra - multiple committies per attestation", + attData: testutil.RandomAttestationDataElectra(), + attestationsFunc: func(attData *eth2p0.AttestationData, aggBits1 bitfield.Bitlist, aggBits2 bitfield.Bitlist, aggBits3 bitfield.Bitlist) []*eth2spec.VersionedAttestation { + zeroTwoComm := bitfield.NewBitvector64() + zeroTwoComm.SetBitAt(0, true) + zeroTwoComm.SetBitAt(2, true) + oneComm := bitfield.NewBitvector64() + oneComm.SetBitAt(1, true) + complexAttestationAggBits := slices.Concat(aggBits1, aggBits2) + return []*eth2spec.VersionedAttestation{ - {Version: eth2spec.DataVersionElectra, Electra: &electra.Attestation{AggregationBits: bits1, Data: attData}}, - {Version: eth2spec.DataVersionElectra, Electra: &electra.Attestation{AggregationBits: bits2, Data: attData}}, - {Version: eth2spec.DataVersionElectra, Electra: &electra.Attestation{AggregationBits: bits3, Data: attData}}, + {Version: eth2spec.DataVersionElectra, Electra: &electra.Attestation{AggregationBits: complexAttestationAggBits, Data: attData, CommitteeBits: zeroTwoComm}}, + {Version: eth2spec.DataVersionElectra, Electra: &electra.Attestation{AggregationBits: aggBits2, Data: attData, CommitteeBits: oneComm}}, + } + }, + beaconStateCommitteesFunc: func(attData *eth2p0.AttestationData) []*statecomm.StateCommittee { + return []*statecomm.StateCommittee{ + {Index: 0, Slot: attData.Slot, Validators: []eth2p0.ValidatorIndex{0, 1, 2}}, + {Index: 1, Slot: attData.Slot, Validators: []eth2p0.ValidatorIndex{0, 1, 2}}, + {Index: 2, Slot: attData.Slot, Validators: []eth2p0.ValidatorIndex{0, 1, 2}}, } }, }, @@ -94,13 +167,17 @@ func TestDuplicateAttData(t *testing.T) { require.NoError(t, err) // Mock 3 attestations, with same data but different aggregation bits. - bits1 := testutil.RandomBitList(8) - bits2 := testutil.RandomBitList(8) - bits3 := testutil.RandomBitList(8) - attData := testutil.RandomAttestationData() + attData := test.attData + aggBits1 := testutil.RandomBitList(8) + aggBits2 := testutil.RandomBitList(8) + aggBits3 := testutil.RandomBitList(8) bmock.BlockAttestationsV2Func = func(_ context.Context, _ string) ([]*eth2spec.VersionedAttestation, error) { - return test.attestationsFunc(attData, bits1, bits2, bits3), nil + return test.attestationsFunc(attData, aggBits1, aggBits2, aggBits3), nil + } + + bmock.BeaconStateCommitteesFunc = func(_ context.Context, slot uint64) ([]*statecomm.StateCommittee, error) { + return test.beaconStateCommitteesFunc(attData), nil } noopTrackerInclFunc := func(duty core.Duty, key core.PubKey, data core.SignedData, err error) {} @@ -120,19 +197,19 @@ func TestDuplicateAttData(t *testing.T) { aggBits1, err := att.AggregationBits() require.NoError(t, err) - ok, err = aggBits1.Contains(bits1) + ok, err = aggBits1.Contains(aggBits1) require.NoError(t, err) require.True(t, ok) aggBits2, err := att.AggregationBits() require.NoError(t, err) - ok, err = aggBits2.Contains(bits2) + ok, err = aggBits2.Contains(aggBits2) require.NoError(t, err) require.True(t, ok) aggBits3, err := att.AggregationBits() require.NoError(t, err) - ok, err = aggBits3.Contains(bits3) + ok, err = aggBits3.Contains(aggBits3) require.NoError(t, err) require.True(t, ok) @@ -163,6 +240,7 @@ func TestInclusion(t *testing.T) { // Create some duties att1 := testutil.RandomDenebVersionedAttestation() + att1.Deneb.Data.Index = 0 att1Data, err := att1.Data() require.NoError(t, err) att1Duty := core.NewAttesterDuty(uint64(att1Data.Slot)) @@ -173,6 +251,7 @@ func TestInclusion(t *testing.T) { agg2Duty := core.NewAggregatorDuty(uint64(slot)) att3 := testutil.RandomDenebVersionedAttestation() + att3.Deneb.Data.Index = 1 att3Data, err := att3.Data() require.NoError(t, err) att3Duty := core.NewAttesterDuty(uint64(att3Data.Slot)) @@ -215,12 +294,34 @@ func TestInclusion(t *testing.T) { addRandomBits(att1.Deneb.AggregationBits) addRandomBits(agg2.Deneb.Message.Aggregate.AggregationBits) + att1CommIdx, err := att1.CommitteeIndex() + require.NoError(t, err) + att1AggBits, err := att1.AggregationBits() + require.NoError(t, err) + + att3CommIdx, err := att3.CommitteeIndex() + require.NoError(t, err) + att3AggBits, err := att3.AggregationBits() + require.NoError(t, err) + block := block{ Slot: block4Duty.Slot, AttestationsByDataRoot: map[eth2p0.Root]*eth2spec.VersionedAttestation{ att1Root: att1, att2Root: {Version: eth2spec.DataVersionDeneb, Deneb: agg2.Deneb.Message.Aggregate}, }, + BeaconCommitees: []*statecomm.StateCommittee{ + { + Index: att1CommIdx, + Slot: att1Data.Slot, + Validators: []eth2p0.ValidatorIndex{eth2p0.ValidatorIndex((att1AggBits.BitIndices()[0]))}, + }, + { + Index: att3CommIdx, + Slot: att3Data.Slot, + Validators: []eth2p0.ValidatorIndex{eth2p0.ValidatorIndex((att3AggBits.BitIndices()[0]))}, + }, + }, } // Check the block diff --git a/core/unsigneddata_test.go b/core/unsigneddata_test.go index f9cc5816e..648ca2613 100644 --- a/core/unsigneddata_test.go +++ b/core/unsigneddata_test.go @@ -193,7 +193,7 @@ func TestNewVersionedAggregatedAttestation(t *testing.T) { } func TestVersionedAggregatedAttestationUtilFunctions(t *testing.T) { - data := testutil.RandomAttestationData() + data := testutil.RandomAttestationDataPhase0() aggregationBits := testutil.RandomBitList(64) type testCase struct { name string diff --git a/core/validatorapi/router_internal_test.go b/core/validatorapi/router_internal_test.go index c898ccd62..11d06acaa 100644 --- a/core/validatorapi/router_internal_test.go +++ b/core/validatorapi/router_internal_test.go @@ -1010,7 +1010,7 @@ func TestRouter(t *testing.T) { t.Run("attestation data", func(t *testing.T) { handler := testHandler{ AttestationDataFunc: func(ctx context.Context, opts *eth2api.AttestationDataOpts) (*eth2api.Response[*eth2p0.AttestationData], error) { - data := testutil.RandomAttestationData() + data := testutil.RandomAttestationDataPhase0() data.Slot = opts.Slot data.Index = opts.CommitteeIndex diff --git a/eth2util/statecomm/statecomm.go b/eth2util/statecomm/statecomm.go new file mode 100644 index 000000000..e7f002745 --- /dev/null +++ b/eth2util/statecomm/statecomm.go @@ -0,0 +1,102 @@ +// Copyright © 2022-2024 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 + +package statecomm + +import ( + "encoding/json" + "fmt" + "strconv" + + eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0" + + "github.com/obolnetwork/charon/app/errors" +) + +// StateCommitteesResponse is simplified response structure for fetching the beacon node committees for a given state. +// This is the response from the BN API endpoint: /eth/v1/beacon/states/{state_id}/validators. +type StateCommitteesResponse struct { + Data []*StateCommittee `json:"data"` +} + +// StateCommittee is the Data field from the BN API endpoint /eth/v1/beacon/states/{state_id}/validators response. +type StateCommittee struct { + Index eth2p0.CommitteeIndex `json:"index"` + Slot eth2p0.Slot `json:"slot"` + Validators []eth2p0.ValidatorIndex `json:"validators"` +} + +// beaconCommitteeSelectionJSON is the spec representation of the struct. +type stateCommitteeJSON struct { + Index string `json:"index"` + Slot string `json:"slot"` + Validators []string `json:"validators"` +} + +// MarshalJSON implements json.Marshaler. +func (b *StateCommittee) MarshalJSON() ([]byte, error) { + var validators []string + for _, v := range b.Validators { + validators = append(validators, fmt.Sprintf("%d", v)) + } + + resp, err := json.Marshal(&stateCommitteeJSON{ + Index: fmt.Sprintf("%d", b.Index), + Slot: fmt.Sprintf("%d", b.Slot), + Validators: validators, + }) + if err != nil { + return nil, errors.Wrap(err, "marshal state committee subscription") + } + + return resp, nil +} + +// UnmarshalJSON implements json.Unmarshaler. +func (b *StateCommittee) UnmarshalJSON(input []byte) error { + var err error + + var stateCommitteeJSON stateCommitteeJSON + if err = json.Unmarshal(input, &stateCommitteeJSON); err != nil { + return errors.Wrap(err, "invalid JSON") + } + + if stateCommitteeJSON.Index == "" { + return errors.New("index missing") + } + index, err := strconv.ParseUint(stateCommitteeJSON.Index, 10, 64) + if err != nil { + return errors.Wrap(err, "invalid value for index") + } + b.Index = eth2p0.CommitteeIndex(index) + + if stateCommitteeJSON.Slot == "" { + return errors.New("slot missing") + } + slot, err := strconv.ParseUint(stateCommitteeJSON.Slot, 10, 64) + if err != nil { + return errors.Wrap(err, "invalid value for slot") + } + b.Slot = eth2p0.Slot(slot) + + var validators []eth2p0.ValidatorIndex + for _, v := range stateCommitteeJSON.Validators { + validator, err := strconv.ParseUint(v, 10, 64) + if err != nil { + return errors.Wrap(err, "invalid value for validator") + } + validators = append(validators, eth2p0.ValidatorIndex(validator)) + } + b.Validators = validators + + return nil +} + +// String returns a string version of the structure. +func (b *StateCommittee) String() (string, error) { + data, err := json.Marshal(b) + if err != nil { + return "", errors.Wrap(err, "marshal StateCommittee") + } + + return string(data), nil +} diff --git a/testutil/beaconmock/beaconmock.go b/testutil/beaconmock/beaconmock.go index eebf2c45d..55d40fde9 100644 --- a/testutil/beaconmock/beaconmock.go +++ b/testutil/beaconmock/beaconmock.go @@ -39,6 +39,7 @@ import ( "github.com/obolnetwork/charon/app/errors" "github.com/obolnetwork/charon/app/eth2wrap" "github.com/obolnetwork/charon/eth2util/eth2exp" + "github.com/obolnetwork/charon/eth2util/statecomm" ) // Interface assertions. @@ -128,6 +129,7 @@ type Mock struct { AttesterDutiesFunc func(context.Context, eth2p0.Epoch, []eth2p0.ValidatorIndex) ([]*eth2v1.AttesterDuty, error) BlockAttestationsFunc func(ctx context.Context, stateID string) ([]*eth2p0.Attestation, error) BlockAttestationsV2Func func(ctx context.Context, stateID string) ([]*eth2spec.VersionedAttestation, error) + BeaconStateCommitteesFunc func(ctx context.Context, slot uint64) ([]*statecomm.StateCommittee, error) NodePeerCountFunc func(ctx context.Context) (int, error) ProposalFunc func(ctx context.Context, opts *eth2api.ProposalOpts) (*eth2api.VersionedProposal, error) SignedBeaconBlockFunc func(ctx context.Context, blockID string) (*eth2spec.VersionedSignedBeaconBlock, error) @@ -287,6 +289,10 @@ func (m Mock) BlockAttestationsV2(ctx context.Context, stateID string) ([]*eth2s return m.BlockAttestationsV2Func(ctx, stateID) } +func (m Mock) BeaconStateCommittees(ctx context.Context, slot uint64) ([]*statecomm.StateCommittee, error) { + return m.BeaconStateCommitteesFunc(ctx, slot) +} + func (m Mock) NodePeerCount(ctx context.Context) (int, error) { return m.NodePeerCountFunc(ctx) } diff --git a/testutil/random.go b/testutil/random.go index b38818937..66d57cf30 100644 --- a/testutil/random.go +++ b/testutil/random.go @@ -122,7 +122,7 @@ func RandomValidatorSet(t *testing.T, vals int) map[eth2p0.ValidatorIndex]*eth2v func RandomPhase0Attestation() *eth2p0.Attestation { return ð2p0.Attestation{ AggregationBits: RandomBitList(1), - Data: RandomAttestationData(), + Data: RandomAttestationDataPhase0(), Signature: RandomEth2Signature(), } } @@ -130,7 +130,7 @@ func RandomPhase0Attestation() *eth2p0.Attestation { func RandomElectraAttestation() *electra.Attestation { return &electra.Attestation{ AggregationBits: RandomBitList(1), - Data: RandomAttestationData(), + Data: RandomAttestationDataPhase0(), Signature: RandomEth2Signature(), CommitteeBits: RandomBitVec64(), } @@ -171,7 +171,7 @@ func RandomElectraVersionedAttestation() *eth2spec.VersionedAttestation { func RandomAggregateAttestation() *eth2p0.Attestation { return ð2p0.Attestation{ AggregationBits: RandomBitList(64), - Data: RandomAttestationData(), + Data: RandomAttestationDataPhase0(), Signature: RandomEth2Signature(), } } @@ -182,18 +182,18 @@ func RandomDenebCoreVersionedAggregateAttestation() core.VersionedAggregatedAtte Version: eth2spec.DataVersionDeneb, Deneb: ð2p0.Attestation{ AggregationBits: RandomBitList(64), - Data: RandomAttestationData(), + Data: RandomAttestationDataPhase0(), Signature: RandomEth2Signature(), }, }, } } -func RandomAttestationData() *eth2p0.AttestationData { - return RandomAttestationDataSeed(NewSeedRand()) +func RandomAttestationDataPhase0() *eth2p0.AttestationData { + return RandomAttestationDataSeedPhase0(NewSeedRand()) } -func RandomAttestationDataSeed(r *rand.Rand) *eth2p0.AttestationData { +func RandomAttestationDataSeedPhase0(r *rand.Rand) *eth2p0.AttestationData { return ð2p0.AttestationData{ Slot: RandomSlotSeed(r), Index: RandomCommIdxSeed(r), @@ -203,6 +203,20 @@ func RandomAttestationDataSeed(r *rand.Rand) *eth2p0.AttestationData { } } +func RandomAttestationDataElectra() *eth2p0.AttestationData { + return RandomAttestationDataSeedElectra(NewSeedRand()) +} + +func RandomAttestationDataSeedElectra(r *rand.Rand) *eth2p0.AttestationData { + return ð2p0.AttestationData{ + Slot: RandomSlotSeed(r), + Index: 0, + BeaconBlockRoot: RandomRootSeed(r), + Source: RandomCheckpointSeed(r), + Target: RandomCheckpointSeed(r), + } +} + func RandomPhase0BeaconBlock() *eth2p0.BeaconBlock { return ð2p0.BeaconBlock{ Slot: RandomSlot(), @@ -1425,7 +1439,7 @@ func RandomCoreAttestationDataSeed(t *testing.T, r *rand.Rand) core.AttestationD t.Helper() duty := RandomAttestationDutySeed(t, r) - data := RandomAttestationDataSeed(r) + data := RandomAttestationDataSeedPhase0(r) return core.AttestationData{ Data: *data,