From 64ecd4f47a13c99b41a8c02f6eb3f4733829aec5 Mon Sep 17 00:00:00 2001 From: Vaclav R Date: Wed, 22 Jan 2025 13:18:51 +0100 Subject: [PATCH 1/6] chore(networkconfig): remove old SSV Labs bootnodes --- networkconfig/holesky.go | 1 - networkconfig/mainnet.go | 1 - 2 files changed, 2 deletions(-) diff --git a/networkconfig/holesky.go b/networkconfig/holesky.go index 3edbf6836f..5f94dfce68 100644 --- a/networkconfig/holesky.go +++ b/networkconfig/holesky.go @@ -19,6 +19,5 @@ var Holesky = NetworkConfig{ Bootnodes: []string{ // SSV Labs "enr:-Ja4QKFD3u5tZob7xukp-JKX9QJMFqqI68cItsE4tBbhsOyDR0M_1UUjb35hbrqvTP3bnXO_LnKh-jNLTeaUqN4xiduGAZKaP_sagmlkgnY0gmlwhDb0fh6Jc2VjcDI1NmsxoQMw_H2anuiqP9NmEaZwbUfdvPFog7PvcKmoVByDa576SINzc3YBg3RjcIITioN1ZHCCD6I", - "enr:-Li4QFIQzamdvTxGJhvcXG_DFmCeyggSffDnllY5DiU47pd_K_1MRnSaJimWtfKJ-MD46jUX9TwgW5Jqe0t4pH41RYWGAYuFnlyth2F0dG5ldHOIAAAAAAAAAACEZXRoMpD1pf1CAAAAAP__________gmlkgnY0gmlwhCLdu_SJc2VjcDI1NmsxoQN4v-N9zFYwEqzGPBBX37q24QPFvAVUtokIo1fblIsmTIN0Y3CCE4uDdWRwgg-j", }, } diff --git a/networkconfig/mainnet.go b/networkconfig/mainnet.go index a5931afa76..f884e6dcf2 100644 --- a/networkconfig/mainnet.go +++ b/networkconfig/mainnet.go @@ -19,7 +19,6 @@ var Mainnet = NetworkConfig{ Bootnodes: []string{ // SSV Labs "enr:-Ja4QAbDe5XANqJUDyJU1GmtS01qqMwDYx9JNZgymjBb55fMaha80E2HznRYoUGy6NFVSvs1u1cFqSM0MgJI-h1QKLeGAZKaTo7LgmlkgnY0gmlwhDQrfraJc2VjcDI1NmsxoQNEj0Pgq9-VxfeX83LPDOUPyWiTVzdI-DnfMdO1n468u4Nzc3YBg3RjcIITioN1ZHCCD6I", - "enr:-Li4QHEPYASj5ZY3BXXKXAoWcoIw0ChgUlTtfOSxgNlYxlmpEWUR_K6Nr04VXsMpWSQxWWM4QHDyypnl92DQNpWkMS-GAYiWUvo8h2F0dG5ldHOIAAAAAAAAAACEZXRoMpD1pf1CAAAAAP__________gmlkgnY0gmlwhCzmKVSJc2VjcDI1NmsxoQOW29na1pUAQw4jF3g0zsPgJG89ViHJOOkHFFklnC2UyIN0Y3CCE4qDdWRwgg-i", // 0NEinfra bootnode "enr:-Li4QDwrOuhEq5gBJBzFUPkezoYiy56SXZUwkSD7bxYo8RAhPnHyS0de0nOQrzl-cL47RY9Jg8k6Y_MgaUd9a5baYXeGAYnfZE76h2F0dG5ldHOIAAAAAAAAAACEZXRoMpD1pf1CAAAAAP__________gmlkgnY0gmlwhDaTS0mJc2VjcDI1NmsxoQMZzUHaN3eClRgF9NAqRNc-ilGpJDDJxdenfo4j-zWKKYN0Y3CCE4iDdWRwgg-g", From 1a5c07ef9b8a38fc1c5b0dfde7d0fc7d657a2e1a Mon Sep 17 00:00:00 2001 From: Matus Kysel Date: Sun, 9 Feb 2025 11:50:55 +0100 Subject: [PATCH 2/6] fix: GetAttestationData by double checking the cache (#2029) --- beacon/goclient/attest.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/beacon/goclient/attest.go b/beacon/goclient/attest.go index a9bf2066cd..40280cecdc 100644 --- a/beacon/goclient/attest.go +++ b/beacon/goclient/attest.go @@ -48,18 +48,18 @@ func (gc *GoClient) GetAttestationData(slot phase0.Slot, committeeIndex phase0.C spec.DataVersion, error, ) { - // Check cache. - cachedResult := gc.attestationDataCache.Get(slot) - if cachedResult != nil { - data, err := withCommitteeIndex(cachedResult.Value(), committeeIndex) - if err != nil { - return nil, DataVersionNil, fmt.Errorf("failed to set committee index: %w", err) - } - return data, spec.DataVersionPhase0, nil - } - // Have to make beacon node request and cache the result. result, err, _ := gc.attestationReqInflight.Do(slot, func() (*phase0.AttestationData, error) { + // Check cache. + cachedResult := gc.attestationDataCache.Get(slot) + if cachedResult != nil { + data, err := withCommitteeIndex(cachedResult.Value(), committeeIndex) + if err != nil { + return nil, fmt.Errorf("failed to set committee index: %w", err) + } + return data, nil + } + attDataReqStart := time.Now() resp, err := gc.multiClient.AttestationData(gc.ctx, &api.AttestationDataOpts{ Slot: slot, From 714616c62fd8ff2504134fd71434a8eabe78e29f Mon Sep 17 00:00:00 2001 From: iurii-ssv <183610124+iurii-ssv@users.noreply.github.com> Date: Tue, 18 Feb 2025 13:46:11 +0200 Subject: [PATCH 3/6] migrations: ehnance sanity check in migration_5 (#2006) --- migrations/migration_5_share_gob_to_ssz.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/migrations/migration_5_share_gob_to_ssz.go b/migrations/migration_5_share_gob_to_ssz.go index 2fbc1af232..be45b70258 100644 --- a/migrations/migration_5_share_gob_to_ssz.go +++ b/migrations/migration_5_share_gob_to_ssz.go @@ -40,6 +40,7 @@ var migration_5_change_share_format_from_gob_to_ssz = Migration{ // we like without "breaking" anything) sharesSSZEncoded := make([]basedb.Obj, 0) + // sharesGOB maps share ID to GOB-encoded shares we already have stored in DB sharesGOB := make(map[string]*storageShareGOB) err = opt.Db.GetAll(append(opstorage.OperatorStoragePrefix, sharesPrefixGOB...), func(i int, obj basedb.Obj) error { shareGOB := &storageShareGOB{} @@ -92,11 +93,7 @@ var migration_5_change_share_format_from_gob_to_ssz = Migration{ sID := shareID(shareSSZ.ValidatorPubKey) shareGOB, ok := sharesGOB[sID] if !ok { - // this shouldn't really happen & we should probably return error if it does, but - // on stage since we already have some SSV nodes that migrated to SSZ format and - // potentially added new validators (new SSZ shares) erroring would prevent migration - // from completing, so we don't return error here - return nil + return fmt.Errorf("SSZ share %s doesn't have corresponding GOB share", sID) } if !matchGOBvsSSZ(shareGOB, shareSSZ) { return fmt.Errorf( From 47907fc5323dcda670834253bd143aa62ef38912 Mon Sep 17 00:00:00 2001 From: Matus Kysel Date: Wed, 19 Feb 2025 18:37:54 +0100 Subject: [PATCH 4/6] feat: support of electra fork (#2007) * feat: initial implementation of electra fork * update to newest spec after review * update to newest spec after review * implement fetching static data * fix the tests * fix spec tests * small improvement * proper handling of SubmitAggregateSelectionProof * add if checks * add custom log * mekong testnet * add mekong to config * fix spec * update bootnode config * update ekm * fix ValidatorIndex * adjust to devnet6 * remove mekong and adjust to latest spec * spec align * fix merge bug * use hooks to update fork value * fix tests * fix: GetAttestationData by double checking the cache * fix typos * add missing changes to signer * fix block summary * add test for dataversion * update go-eth2-client * aligin to new version of spec * revert go-eth2-client back go 0.24.0 * remove ELConnection check * increase ekm version * fix: aggregator committee index * fix: wrong CommitteeIndex in aggregator * revert CommitteeIndex in agg request * try without Data.Index in agg * fix spec version * fix: update attestation data index based on data version * fix: update fork epoch handling to use FarFutureEpoch as default * fix qa --------- Co-authored-by: moshe-blox --- beacon/goclient/aggregator.go | 91 ++++++- beacon/goclient/attest.go | 40 +-- beacon/goclient/attest_test.go | 28 +-- beacon/goclient/dataversion.go | 108 ++++++++ beacon/goclient/dataversion_test.go | 237 ++++++++++++++++++ beacon/goclient/goclient.go | 46 +++- beacon/goclient/proposer.go | 63 ++++- .../testdata/mock-beacon-responses.json | 4 + beacon/goclient/types.go | 17 +- ekm/eth_key_manager_signer.go | 14 ++ go.mod | 14 +- go.sum | 16 +- .../qbft/tests/temp_testing_beacon_network.go | 11 +- message/validation/partial_validation.go | 2 +- message/validation/validation_test.go | 30 +-- network/topics/params/topic_score.go | 2 +- networkconfig/config.go | 1 - protocol/v2/blockchain/beacon/mock_client.go | 32 ++- .../v2/qbft/spectest/qbft_mapping_test.go | 2 +- protocol/v2/ssv/runner/aggregator.go | 62 ++++- protocol/v2/ssv/runner/committee.go | 85 ++++--- protocol/v2/ssv/runner/proposer.go | 30 +++ protocol/v2/ssv/spectest/ssv_mapping_test.go | 2 +- protocol/v2/testing/test_utils.go | 128 +++------- scripts/spec-alignment/differ.config.yaml | 2 +- 25 files changed, 815 insertions(+), 252 deletions(-) create mode 100644 beacon/goclient/dataversion.go create mode 100644 beacon/goclient/dataversion_test.go diff --git a/beacon/goclient/aggregator.go b/beacon/goclient/aggregator.go index 882628a61a..c9b24187c1 100644 --- a/beacon/goclient/aggregator.go +++ b/beacon/goclient/aggregator.go @@ -8,6 +8,7 @@ import ( "github.com/attestantio/go-eth2-client/api" "github.com/attestantio/go-eth2-client/spec" + "github.com/attestantio/go-eth2-client/spec/electra" "github.com/attestantio/go-eth2-client/spec/phase0" ssz "github.com/ferranbt/fastssz" "go.uber.org/zap" @@ -29,10 +30,13 @@ func (gc *GoClient) SubmitAggregateSelectionProof(slot phase0.Slot, committeeInd return nil, DataVersionNil, fmt.Errorf("validator is not an aggregator") } - attData, _, err := gc.GetAttestationData(slot, committeeIndex) + attData, _, err := gc.GetAttestationData(slot) if err != nil { return nil, DataVersionNil, fmt.Errorf("failed to get attestation data: %w", err) } + if gc.DataVersion(gc.network.EstimatedEpochAtSlot(attData.Slot)) < spec.DataVersionElectra { + attData.Index = committeeIndex + } // Get aggregate attestation data. root, err := attData.HashTreeRoot() @@ -44,6 +48,7 @@ func (gc *GoClient) SubmitAggregateSelectionProof(slot phase0.Slot, committeeInd aggDataResp, err := gc.multiClient.AggregateAttestation(gc.ctx, &api.AggregateAttestationOpts{ Slot: slot, AttestationDataRoot: root, + CommitteeIndex: committeeIndex, }) recordRequestDuration(gc.ctx, "AggregateAttestation", gc.multiClient.Address(), http.MethodGet, time.Since(aggDataReqStart), err) if err != nil { @@ -69,22 +74,92 @@ func (gc *GoClient) SubmitAggregateSelectionProof(slot phase0.Slot, committeeInd var selectionProof phase0.BLSSignature copy(selectionProof[:], slotSig) - return &phase0.AggregateAndProof{ - AggregatorIndex: index, - Aggregate: aggDataResp.Data, - SelectionProof: selectionProof, - }, spec.DataVersionPhase0, nil + switch aggDataResp.Data.Version { + case spec.DataVersionElectra: + if aggDataResp.Data.Electra == nil { + gc.log.Error(clNilResponseForkDataErrMsg, + zap.String("api", "AggregateAttestation"), + ) + return nil, DataVersionNil, fmt.Errorf("aggregate attestation electra data is nil") + } + return &electra.AggregateAndProof{ + AggregatorIndex: index, + Aggregate: aggDataResp.Data.Electra, + SelectionProof: selectionProof, + }, aggDataResp.Data.Version, nil + case spec.DataVersionDeneb: + if aggDataResp.Data.Deneb == nil { + gc.log.Error(clNilResponseForkDataErrMsg, + zap.String("api", "AggregateAttestation"), + ) + return nil, DataVersionNil, fmt.Errorf("aggregate attestation deneb data is nil") + } + return &phase0.AggregateAndProof{ + AggregatorIndex: index, + Aggregate: aggDataResp.Data.Deneb, + SelectionProof: selectionProof, + }, aggDataResp.Data.Version, nil + case spec.DataVersionCapella: + if aggDataResp.Data.Capella == nil { + gc.log.Error(clNilResponseForkDataErrMsg, + zap.String("api", "AggregateAttestation"), + ) + return nil, DataVersionNil, fmt.Errorf("aggregate attestation capella data is nil") + } + return &phase0.AggregateAndProof{ + AggregatorIndex: index, + Aggregate: aggDataResp.Data.Capella, + SelectionProof: selectionProof, + }, aggDataResp.Data.Version, nil + case spec.DataVersionBellatrix: + if aggDataResp.Data.Bellatrix == nil { + gc.log.Error(clNilResponseForkDataErrMsg, + zap.String("api", "AggregateAttestation"), + ) + return nil, DataVersionNil, fmt.Errorf("aggregate attestation bellatrix data is nil") + } + return &phase0.AggregateAndProof{ + AggregatorIndex: index, + Aggregate: aggDataResp.Data.Bellatrix, + SelectionProof: selectionProof, + }, aggDataResp.Data.Version, nil + case spec.DataVersionAltair: + if aggDataResp.Data.Altair == nil { + gc.log.Error(clNilResponseForkDataErrMsg, + zap.String("api", "AggregateAttestation"), + ) + return nil, DataVersionNil, fmt.Errorf("aggregate attestation altair data is nil") + } + return &phase0.AggregateAndProof{ + AggregatorIndex: index, + Aggregate: aggDataResp.Data.Altair, + SelectionProof: selectionProof, + }, aggDataResp.Data.Version, nil + default: + if aggDataResp.Data.Phase0 == nil { + gc.log.Error(clNilResponseForkDataErrMsg, + zap.String("api", "AggregateAttestation"), + ) + return nil, DataVersionNil, fmt.Errorf("aggregate attestation phase0 data is nil") + } + return &phase0.AggregateAndProof{ + AggregatorIndex: index, + Aggregate: aggDataResp.Data.Phase0, + SelectionProof: selectionProof, + }, aggDataResp.Data.Version, nil + } } // SubmitSignedAggregateSelectionProof broadcasts a signed aggregator msg -func (gc *GoClient) SubmitSignedAggregateSelectionProof(msg *phase0.SignedAggregateAndProof) error { +func (gc *GoClient) SubmitSignedAggregateSelectionProof(msg *spec.VersionedSignedAggregateAndProof) error { clientAddress := gc.multiClient.Address() logger := gc.log.With( zap.String("api", "SubmitAggregateAttestations"), zap.String("client_addr", clientAddress)) start := time.Now() - err := gc.multiClient.SubmitAggregateAttestations(gc.ctx, []*phase0.SignedAggregateAndProof{msg}) + + err := gc.multiClient.SubmitAggregateAttestations(gc.ctx, &api.SubmitAggregateAttestationsOpts{SignedAggregateAndProofs: []*spec.VersionedSignedAggregateAndProof{msg}}) recordRequestDuration(gc.ctx, "SubmitAggregateAttestations", gc.multiClient.Address(), http.MethodPost, time.Since(start), err) if err != nil { logger.Error(clResponseErrMsg, zap.Error(err)) diff --git a/beacon/goclient/attest.go b/beacon/goclient/attest.go index 40280cecdc..e5d3c9a425 100644 --- a/beacon/goclient/attest.go +++ b/beacon/goclient/attest.go @@ -39,11 +39,10 @@ func (gc *GoClient) AttesterDuties(ctx context.Context, epoch phase0.Epoch, vali return resp.Data, nil } -// GetAttestationData returns attestation data for a given slot and committeeIndex. +// GetAttestationData returns attestation data for a given slot. // Multiple calls for the same slot are joined into a single request, after which // the result is cached for a short duration, deep copied and returned -// with the provided committeeIndex set. -func (gc *GoClient) GetAttestationData(slot phase0.Slot, committeeIndex phase0.CommitteeIndex) ( +func (gc *GoClient) GetAttestationData(slot phase0.Slot) ( *phase0.AttestationData, spec.DataVersion, error, @@ -53,16 +52,13 @@ func (gc *GoClient) GetAttestationData(slot phase0.Slot, committeeIndex phase0.C // Check cache. cachedResult := gc.attestationDataCache.Get(slot) if cachedResult != nil { - data, err := withCommitteeIndex(cachedResult.Value(), committeeIndex) - if err != nil { - return nil, fmt.Errorf("failed to set committee index: %w", err) - } - return data, nil + return cachedResult.Value(), nil } attDataReqStart := time.Now() resp, err := gc.multiClient.AttestationData(gc.ctx, &api.AttestationDataOpts{ - Slot: slot, + Slot: slot, + CommitteeIndex: 0, }) recordRequestDuration(gc.ctx, "AttestationData", gc.multiClient.Address(), http.MethodGet, time.Since(attDataReqStart), err) @@ -97,38 +93,18 @@ func (gc *GoClient) GetAttestationData(slot phase0.Slot, committeeIndex phase0.C return nil, DataVersionNil, err } - data, err := withCommitteeIndex(result, committeeIndex) - if err != nil { - return nil, DataVersionNil, fmt.Errorf("failed to set committee index: %w", err) - } - return data, spec.DataVersionPhase0, nil -} - -// withCommitteeIndex returns a deep copy of the attestation data with the provided committee index set. -func withCommitteeIndex(data *phase0.AttestationData, committeeIndex phase0.CommitteeIndex) (*phase0.AttestationData, error) { - // Marshal & unmarshal to make a deep copy. This is safer because it won't break silently if - // a new field is added to AttestationData. - ssz, err := data.MarshalSSZ() - if err != nil { - return nil, fmt.Errorf("failed to marshal attestation data: %w", err) - } - var cpy phase0.AttestationData - if err := cpy.UnmarshalSSZ(ssz); err != nil { - return nil, fmt.Errorf("failed to unmarshal attestation data: %w", err) - } - cpy.Index = committeeIndex - return &cpy, nil + return result, spec.DataVersionPhase0, nil } // SubmitAttestations implements Beacon interface -func (gc *GoClient) SubmitAttestations(attestations []*phase0.Attestation) error { +func (gc *GoClient) SubmitAttestations(attestations []*spec.VersionedAttestation) error { clientAddress := gc.multiClient.Address() logger := gc.log.With( zap.String("api", "SubmitAttestations"), zap.String("client_addr", clientAddress)) start := time.Now() - err := gc.multiClient.SubmitAttestations(gc.ctx, attestations) + err := gc.multiClient.SubmitAttestations(gc.ctx, &api.SubmitAttestationsOpts{Attestations: attestations}) recordRequestDuration(gc.ctx, "SubmitAttestations", clientAddress, http.MethodPost, time.Since(start), err) if err != nil { logger.Error(clResponseErrMsg, zap.Error(err)) diff --git a/beacon/goclient/attest_test.go b/beacon/goclient/attest_test.go index 9f1b52d52d..8f1eb5048b 100644 --- a/beacon/goclient/attest_test.go +++ b/beacon/goclient/attest_test.go @@ -58,7 +58,12 @@ func TestGoClient_GetAttestationData(t *testing.T) { "SYNC_COMMITTEE_SUBNET_COUNT": "4", "TARGET_AGGREGATORS_PER_COMMITTEE": "16", "TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE": "16", - "INTERVALS_PER_SLOT": "3" + "INTERVALS_PER_SLOT": "3", + "ALTAIR_FORK_EPOCH": "74240", + "BELLATRIX_FORK_EPOCH": "144896", + "CAPELLA_FORK_EPOCH": "194048", + "DENEB_FORK_EPOCH": "269568", + "ELECTRA_FORK_EPOCH": "18446744073709551615" } }`), "/eth/v1/beacon/genesis": []byte(`{ @@ -152,8 +157,6 @@ func TestGoClient_GetAttestationData(t *testing.T) { t.Run("requests must be cached (per slot)", func(t *testing.T) { slot1 := phase0.Slot(12345678) slot2 := phase0.Slot(12345679) - committeeIndex1 := phase0.CommitteeIndex(1) - committeeIndex2 := phase0.CommitteeIndex(2) server, serverGotRequests := newMockServer() @@ -177,11 +180,10 @@ func TestGoClient_GetAttestationData(t *testing.T) { require.NoError(t, err) // First request with slot1. - gotResult1a, gotVersion, err := client.GetAttestationData(slot1, committeeIndex1) + gotResult1a, gotVersion, err := client.GetAttestationData(slot1) require.NoError(t, err) require.Equal(t, spec.DataVersionPhase0, gotVersion) require.Equal(t, slot1, gotResult1a.Slot) - require.Equal(t, committeeIndex1, gotResult1a.Index) require.NotEmpty(t, gotResult1a.BeaconBlockRoot) require.NotEmpty(t, gotResult1a.Source.Epoch) require.NotEmpty(t, gotResult1a.Source.Root) @@ -189,11 +191,10 @@ func TestGoClient_GetAttestationData(t *testing.T) { require.NotEmpty(t, gotResult1a.Target.Root) // Second request with slot1, result should have been cached. - gotResult1b, gotVersion, err := client.GetAttestationData(slot1, committeeIndex1) + gotResult1b, gotVersion, err := client.GetAttestationData(slot1) require.NoError(t, err) require.Equal(t, spec.DataVersionPhase0, gotVersion) require.Equal(t, slot1, gotResult1b.Slot) - require.Equal(t, committeeIndex1, gotResult1b.Index) require.NotEmpty(t, gotResult1b.BeaconBlockRoot) require.NotEmpty(t, gotResult1b.Source.Epoch) require.NotEmpty(t, gotResult1b.Source.Root) @@ -207,11 +208,10 @@ func TestGoClient_GetAttestationData(t *testing.T) { require.Equal(t, gotResult1b.Target.Root, gotResult1a.Target.Root) // Third request with slot2. - gotResult2a, gotVersion, err := client.GetAttestationData(slot2, committeeIndex2) + gotResult2a, gotVersion, err := client.GetAttestationData(slot2) require.NoError(t, err) require.Equal(t, spec.DataVersionPhase0, gotVersion) require.Equal(t, slot2, gotResult2a.Slot) - require.Equal(t, committeeIndex2, gotResult2a.Index) require.NotEmpty(t, gotResult2a.BeaconBlockRoot) require.NotEmpty(t, gotResult2a.Source.Epoch) require.NotEmpty(t, gotResult2a.Source.Root) @@ -219,11 +219,10 @@ func TestGoClient_GetAttestationData(t *testing.T) { require.NotEmpty(t, gotResult2a.Target.Root) // Fourth request with slot2, result should have been cached. - gotResult2b, gotVersion, err := client.GetAttestationData(slot2, committeeIndex2) + gotResult2b, gotVersion, err := client.GetAttestationData(slot2) require.NoError(t, err) require.Equal(t, spec.DataVersionPhase0, gotVersion) require.Equal(t, slot2, gotResult2b.Slot) - require.Equal(t, committeeIndex2, gotResult2b.Index) require.NotEmpty(t, gotResult2b.BeaconBlockRoot) require.NotEmpty(t, gotResult2b.Source.Epoch) require.NotEmpty(t, gotResult2b.Source.Root) @@ -237,11 +236,10 @@ func TestGoClient_GetAttestationData(t *testing.T) { require.Equal(t, gotResult2b.Target.Root, gotResult2a.Target.Root) // Second request with slot1, result STILL should be cached. - gotResult1c, gotVersion, err := client.GetAttestationData(slot1, committeeIndex1) + gotResult1c, gotVersion, err := client.GetAttestationData(slot1) require.NoError(t, err) require.Equal(t, spec.DataVersionPhase0, gotVersion) require.Equal(t, slot1, gotResult1c.Slot) - require.Equal(t, committeeIndex1, gotResult1c.Index) require.NotEmpty(t, gotResult1c.BeaconBlockRoot) require.NotEmpty(t, gotResult1c.Source.Epoch) require.NotEmpty(t, gotResult1c.Source.Root) @@ -297,13 +295,11 @@ func TestGoClient_GetAttestationData(t *testing.T) { p := pool.New() for i := 0; i < 1000; i++ { slot := phase0.Slot(slotStartPos + i%slotsTotalCnt) - committeeIndex := phase0.CommitteeIndex(i % 64) p.Go(func() { - gotResult, gotVersion, err := client.GetAttestationData(slot, committeeIndex) + gotResult, gotVersion, err := client.GetAttestationData(slot) require.NoError(t, err) require.Equal(t, spec.DataVersionPhase0, gotVersion) require.Equal(t, slot, gotResult.Slot) - require.Equal(t, committeeIndex, gotResult.Index) prevResult, ok := gotResults.GetOrSet(slot, gotResult) if ok { diff --git a/beacon/goclient/dataversion.go b/beacon/goclient/dataversion.go new file mode 100644 index 0000000000..b5cca4abf4 --- /dev/null +++ b/beacon/goclient/dataversion.go @@ -0,0 +1,108 @@ +package goclient + +import ( + "fmt" + + "github.com/attestantio/go-eth2-client/api" + "github.com/attestantio/go-eth2-client/spec" + "github.com/attestantio/go-eth2-client/spec/phase0" +) + +func (gc *GoClient) DataVersion(epoch phase0.Epoch) spec.DataVersion { + gc.ForkLock.RLock() + defer gc.ForkLock.RUnlock() + if epoch < gc.ForkEpochAltair { + return spec.DataVersionPhase0 + } else if epoch < gc.ForkEpochBellatrix { + return spec.DataVersionAltair + } else if epoch < gc.ForkEpochCapella { + return spec.DataVersionBellatrix + } else if epoch < gc.ForkEpochDeneb { + return spec.DataVersionCapella + } else if epoch < gc.ForkEpochElectra { + return spec.DataVersionDeneb + } + return spec.DataVersionElectra +} + +func (gc *GoClient) checkForkValues(specResponse *api.Response[map[string]any]) error { + // Validate the response. + if specResponse == nil { + return fmt.Errorf("spec response is nil") + } + if specResponse.Data == nil { + return fmt.Errorf("spec response data is nil") + } + + // Lock the fork values to ensure atomic read and update. + gc.ForkLock.Lock() + defer gc.ForkLock.Unlock() + + // We'll compute candidate new values first and update the fields only if all validations pass. + var newAltair, newBellatrix, newCapella, newDeneb, newElectra phase0.Epoch + + // processFork is a helper to handle required forks. + // It retrieves the candidate fork epoch from the response, + // and compares it with the current stored value. + // If the candidate is greater than the current value, that's an error. + // Otherwise, it returns the lower value (or the candidate if the current value is zero). + processFork := func(forkName, key string, current phase0.Epoch) (phase0.Epoch, error) { + raw, ok := specResponse.Data[key] + if !ok { + return 0, fmt.Errorf("%s fork epoch not known by chain", forkName) + } + forkVal, ok := raw.(uint64) + if !ok { + return 0, fmt.Errorf("failed to decode %s fork epoch", forkName) + } + if current != FarFutureEpoch && forkVal == uint64(FarFutureEpoch) { + return 0, fmt.Errorf("failed to decode %s fork epoch", forkName) + } + return phase0.Epoch(forkVal), nil + } + + var err error + // Process required forks. + if newAltair, err = processFork("ALTAIR", "ALTAIR_FORK_EPOCH", gc.ForkEpochAltair); err != nil { + return err + } + if newBellatrix, err = processFork("BELLATRIX", "BELLATRIX_FORK_EPOCH", gc.ForkEpochBellatrix); err != nil { + return err + } + if newCapella, err = processFork("CAPELLA", "CAPELLA_FORK_EPOCH", gc.ForkEpochCapella); err != nil { + return err + } + if newDeneb, err = processFork("DENEB", "DENEB_FORK_EPOCH", gc.ForkEpochDeneb); err != nil { + return err + } + + // Process the optional ELECTRA fork. + // If the key exists, perform the same validation; otherwise, keep the current value. + if raw, ok := specResponse.Data["ELECTRA_FORK_EPOCH"]; ok { + forkVal, ok := raw.(uint64) + if !ok { + return fmt.Errorf("failed to decode ELECTRA fork epoch") + } + candidate := phase0.Epoch(forkVal) + if gc.ForkEpochElectra != 0 && candidate > gc.ForkEpochElectra { + return fmt.Errorf("new ELECTRA fork epoch (%d) is greater than current (%d)", candidate, gc.ForkEpochElectra) + } + if gc.ForkEpochElectra == 0 || candidate < gc.ForkEpochElectra { + newElectra = candidate + } else { + newElectra = gc.ForkEpochElectra + } + } else { + newElectra = FarFutureEpoch + } + + // At this point, no error was encountered. + // Update all fork values atomically. + gc.ForkEpochAltair = newAltair + gc.ForkEpochBellatrix = newBellatrix + gc.ForkEpochCapella = newCapella + gc.ForkEpochDeneb = newDeneb + gc.ForkEpochElectra = newElectra + + return nil +} diff --git a/beacon/goclient/dataversion_test.go b/beacon/goclient/dataversion_test.go new file mode 100644 index 0000000000..a82cafe867 --- /dev/null +++ b/beacon/goclient/dataversion_test.go @@ -0,0 +1,237 @@ +package goclient + +import ( + "math" + "strings" + "testing" + + "github.com/attestantio/go-eth2-client/api" + "github.com/attestantio/go-eth2-client/spec" + "github.com/attestantio/go-eth2-client/spec/phase0" +) + +// TestDataVersion verifies that DataVersion returns the correct version based on fork epochs. +func TestDataVersion(t *testing.T) { + // Create a client with preset fork epochs. + client := &GoClient{ + ForkEpochAltair: phase0.Epoch(10), + ForkEpochBellatrix: phase0.Epoch(20), + ForkEpochCapella: phase0.Epoch(30), + ForkEpochDeneb: phase0.Epoch(40), + ForkEpochElectra: phase0.Epoch(50), + } + + tests := []struct { + epoch phase0.Epoch + expected spec.DataVersion + }{ + {epoch: 0, expected: spec.DataVersionPhase0}, + {epoch: 9, expected: spec.DataVersionPhase0}, + {epoch: 10, expected: spec.DataVersionAltair}, + {epoch: 15, expected: spec.DataVersionAltair}, + {epoch: 20, expected: spec.DataVersionBellatrix}, + {epoch: 25, expected: spec.DataVersionBellatrix}, + {epoch: 30, expected: spec.DataVersionCapella}, + {epoch: 35, expected: spec.DataVersionCapella}, + {epoch: 40, expected: spec.DataVersionDeneb}, + {epoch: 45, expected: spec.DataVersionDeneb}, + {epoch: 50, expected: spec.DataVersionElectra}, + {epoch: 55, expected: spec.DataVersionElectra}, + } + + for _, tc := range tests { + got := client.DataVersion(tc.epoch) + if got != tc.expected { + t.Errorf("DataVersion(%d): expected %v, got %v", tc.epoch, tc.expected, got) + } + } +} + +// TestCheckForkValues verifies the checkForkValues function across various scenarios. +func TestCheckForkValues(t *testing.T) { + tests := []struct { + name string + // initial fork values + initialAltair, initialBellatrix, initialCapella, + initialDeneb, initialElectra phase0.Epoch + // input response and expected outcomes + response *api.Response[map[string]any] + expectedErr string + expectedAltair, expectedBellatrix, expectedCapella, + expectedDeneb, expectedElectra phase0.Epoch + }{ + { + name: "nil response", + response: nil, + expectedErr: "spec response is nil", + }, + { + name: "nil data", + response: &api.Response[map[string]any]{ + Data: nil, + }, + expectedErr: "spec response data is nil", + }, + { + name: "missing ALTAIR", + initialAltair: math.MaxUint64, + initialBellatrix: math.MaxUint64, + initialCapella: math.MaxUint64, + initialDeneb: math.MaxUint64, + initialElectra: math.MaxUint64, + response: &api.Response[map[string]any]{ + Data: map[string]any{ + "BELLATRIX_FORK_EPOCH": uint64(20), + "CAPELLA_FORK_EPOCH": uint64(30), + "DENEB_FORK_EPOCH": uint64(40), + }, + }, + expectedErr: "ALTAIR fork epoch not known by chain", + }, + { + name: "invalid type for ALTAIR", + initialAltair: math.MaxUint64, + initialBellatrix: math.MaxUint64, + initialCapella: math.MaxUint64, + initialDeneb: math.MaxUint64, + initialElectra: math.MaxUint64, + response: &api.Response[map[string]any]{ + Data: map[string]any{ + "ALTAIR_FORK_EPOCH": "not a uint", + "BELLATRIX_FORK_EPOCH": uint64(20), + "CAPELLA_FORK_EPOCH": uint64(30), + "DENEB_FORK_EPOCH": uint64(40), + }, + }, + expectedErr: "failed to decode ALTAIR fork epoch", + }, + { + name: "valid update with initial zeros and electra provided", + initialAltair: math.MaxUint64, + initialBellatrix: math.MaxUint64, + initialCapella: math.MaxUint64, + initialDeneb: math.MaxUint64, + initialElectra: math.MaxUint64, + response: &api.Response[map[string]any]{ + Data: map[string]any{ + "ALTAIR_FORK_EPOCH": uint64(10), + "BELLATRIX_FORK_EPOCH": uint64(20), + "CAPELLA_FORK_EPOCH": uint64(30), + "DENEB_FORK_EPOCH": uint64(40), + "ELECTRA_FORK_EPOCH": uint64(50), + }, + }, + expectedAltair: phase0.Epoch(10), + expectedBellatrix: phase0.Epoch(20), + expectedCapella: phase0.Epoch(30), + expectedDeneb: phase0.Epoch(40), + expectedElectra: phase0.Epoch(50), + }, + { + name: "optional ELECTRA not provided, remains unchanged", + initialAltair: math.MaxUint64, + initialBellatrix: math.MaxUint64, + initialCapella: math.MaxUint64, + initialDeneb: math.MaxUint64, + initialElectra: math.MaxUint64, + response: &api.Response[map[string]any]{ + Data: map[string]any{ + "ALTAIR_FORK_EPOCH": uint64(10), + "BELLATRIX_FORK_EPOCH": uint64(20), + "CAPELLA_FORK_EPOCH": uint64(30), + "DENEB_FORK_EPOCH": uint64(40), + }, + }, + expectedAltair: phase0.Epoch(10), + expectedBellatrix: phase0.Epoch(20), + expectedCapella: phase0.Epoch(30), + expectedDeneb: phase0.Epoch(40), + expectedElectra: phase0.Epoch(math.MaxUint64), + }, + { + name: "optional ELECTRA provided and updates", + initialAltair: 10, + initialBellatrix: 20, + initialCapella: 30, + initialDeneb: 40, + initialElectra: 99, + response: &api.Response[map[string]any]{ + Data: map[string]any{ + "ALTAIR_FORK_EPOCH": uint64(10), + "BELLATRIX_FORK_EPOCH": uint64(20), + "CAPELLA_FORK_EPOCH": uint64(30), + "DENEB_FORK_EPOCH": uint64(40), + "ELECTRA_FORK_EPOCH": uint64(50), + }, + }, + expectedAltair: phase0.Epoch(10), + expectedBellatrix: phase0.Epoch(20), + expectedCapella: phase0.Epoch(30), + expectedDeneb: phase0.Epoch(40), + expectedElectra: phase0.Epoch(50), + }, + { + name: "optional ELECTRA provided, candidate greater than current", + initialAltair: 10, + initialBellatrix: 20, + initialCapella: 30, + initialDeneb: 40, + initialElectra: 50, + response: &api.Response[map[string]any]{ + Data: map[string]any{ + "ALTAIR_FORK_EPOCH": uint64(10), + "BELLATRIX_FORK_EPOCH": uint64(20), + "CAPELLA_FORK_EPOCH": uint64(30), + "DENEB_FORK_EPOCH": uint64(40), + "ELECTRA_FORK_EPOCH": uint64(60), + }, + }, + expectedErr: "new ELECTRA fork epoch (60) is greater than current (50)", + }, + } + + for _, tc := range tests { + tc := tc // capture range variable + t.Run(tc.name, func(t *testing.T) { + // Create a client with initial fork values. + client := &GoClient{ + ForkEpochAltair: tc.initialAltair, + ForkEpochBellatrix: tc.initialBellatrix, + ForkEpochCapella: tc.initialCapella, + ForkEpochDeneb: tc.initialDeneb, + ForkEpochElectra: tc.initialElectra, + } + + err := client.checkForkValues(tc.response) + if tc.expectedErr != "" { + if err == nil { + t.Fatalf("expected error containing %q but got nil", tc.expectedErr) + } + if !strings.Contains(err.Error(), tc.expectedErr) { + t.Fatalf("expected error containing %q, got %q", tc.expectedErr, err.Error()) + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // Verify that the fork epoch fields have been updated as expected. + if client.ForkEpochAltair != tc.expectedAltair { + t.Errorf("ForkEpochAltair: expected %d, got %d", tc.expectedAltair, client.ForkEpochAltair) + } + if client.ForkEpochBellatrix != tc.expectedBellatrix { + t.Errorf("ForkEpochBellatrix: expected %d, got %d", tc.expectedBellatrix, client.ForkEpochBellatrix) + } + if client.ForkEpochCapella != tc.expectedCapella { + t.Errorf("ForkEpochCapella: expected %d, got %d", tc.expectedCapella, client.ForkEpochCapella) + } + if client.ForkEpochDeneb != tc.expectedDeneb { + t.Errorf("ForkEpochDeneb: expected %d, got %d", tc.expectedDeneb, client.ForkEpochDeneb) + } + if client.ForkEpochElectra != tc.expectedElectra { + t.Errorf("ForkEpochElectra: expected %d, got %d", tc.expectedElectra, client.ForkEpochElectra) + } + }) + } +} diff --git a/beacon/goclient/goclient.go b/beacon/goclient/goclient.go index abc11b296b..e9b6c25f8e 100644 --- a/beacon/goclient/goclient.go +++ b/beacon/goclient/goclient.go @@ -19,6 +19,7 @@ import ( "github.com/jellydator/ttlcache/v3" "github.com/pkg/errors" "github.com/rs/zerolog" + specssv "github.com/ssvlabs/ssv-spec/ssv" spectypes "github.com/ssvlabs/ssv-spec/types" "go.uber.org/zap" "tailscale.com/util/singleflight" @@ -39,9 +40,10 @@ const ( DefaultCommonTimeout = time.Second * 5 // For dialing and most requests. DefaultLongTimeout = time.Second * 60 // For long requests. - clResponseErrMsg = "Consensus client returned an error" - clNilResponseErrMsg = "Consensus client returned a nil response" - clNilResponseDataErrMsg = "Consensus client returned a nil response data" + clResponseErrMsg = "Consensus client returned an error" + clNilResponseErrMsg = "Consensus client returned a nil response" + clNilResponseDataErrMsg = "Consensus client returned a nil response data" + clNilResponseForkDataErrMsg = "Consensus client returned a nil response fork data" ) // NodeClient is the type of the Beacon node. @@ -114,6 +116,7 @@ type GoClient struct { network beaconprotocol.Network clients []Client multiClient MultiClient + specssv.VersionCalls genesisVersion atomic.Pointer[phase0.Version] @@ -137,6 +140,13 @@ type GoClient struct { commonTimeout time.Duration longTimeout time.Duration + + ForkLock sync.RWMutex + ForkEpochElectra phase0.Epoch + ForkEpochDeneb phase0.Epoch + ForkEpochCapella phase0.Epoch + ForkEpochBellatrix phase0.Epoch + ForkEpochAltair phase0.Epoch } // New init new client and go-client instance @@ -171,6 +181,13 @@ func New( ), commonTimeout: commonTimeout, longTimeout: longTimeout, + + // Initialize forks with FAR_FUTURE_EPOCH. + ForkEpochAltair: math.MaxUint64, + ForkEpochBellatrix: math.MaxUint64, + ForkEpochCapella: math.MaxUint64, + ForkEpochDeneb: math.MaxUint64, + ForkEpochElectra: math.MaxUint64, } beaconAddrList := strings.Split(opt.BeaconNodeAddr, ";") // TODO: Decide what symbol to use as a separator. Bootnodes are currently separated by ";". Deployment bot currently uses ",". @@ -234,7 +251,6 @@ func (gc *GoClient) addSingleClient(ctx context.Context, addr string) error { eth2clienthttp.WithReducedMemoryUsage(true), eth2clienthttp.WithAllowDelayedStart(true), eth2clienthttp.WithHooks(gc.singleClientHooks()), - eth2clienthttp.WithELConnectionCheck(true), ) if err != nil { gc.log.Error("Consensus http client initialization failed", @@ -290,6 +306,24 @@ func (gc *GoClient) singleClientHooks() *eth2clienthttp.Hooks { ) return // Tests may override Fatal's behavior } + + spec, err := s.Spec(ctx, &api.SpecOpts{}) + if err != nil { + gc.log.Error(clResponseErrMsg, + zap.String("address", s.Address()), + zap.String("api", "Spec"), + zap.Error(err), + ) + return + } + + if err := gc.checkForkValues(spec); err != nil { + gc.log.Error("failed to check fork values", + zap.String("address", s.Address()), + zap.Error(err), + ) + return + } }, OnInactive: func(ctx context.Context, s *eth2clienthttp.Service) { gc.log.Warn("consensus client disconnected", @@ -375,10 +409,6 @@ func (gc *GoClient) Healthy(ctx context.Context) error { gc.log.Error("Consensus client is in optimistic mode") return fmt.Errorf("optimistic") } - if syncState.ELOffline { - gc.log.Error("Consensus client's EL node is offline") - return fmt.Errorf("EL is offline") - } recordBeaconClientStatus(ctx, statusSynced, gc.multiClient.Address()) diff --git a/beacon/goclient/proposer.go b/beacon/goclient/proposer.go index a2fc7051aa..9c57596b23 100644 --- a/beacon/goclient/proposer.go +++ b/beacon/goclient/proposer.go @@ -11,10 +11,12 @@ import ( eth2apiv1 "github.com/attestantio/go-eth2-client/api/v1" apiv1capella "github.com/attestantio/go-eth2-client/api/v1/capella" apiv1deneb "github.com/attestantio/go-eth2-client/api/v1/deneb" + apiv1electra "github.com/attestantio/go-eth2-client/api/v1/electra" "github.com/attestantio/go-eth2-client/spec" "github.com/attestantio/go-eth2-client/spec/bellatrix" "github.com/attestantio/go-eth2-client/spec/capella" "github.com/attestantio/go-eth2-client/spec/deneb" + "github.com/attestantio/go-eth2-client/spec/electra" "github.com/attestantio/go-eth2-client/spec/phase0" ssz "github.com/ferranbt/fastssz" "github.com/sourcegraph/conc/pool" @@ -118,6 +120,17 @@ func (gc *GoClient) GetBeaconBlock(slot phase0.Slot, graffitiBytes, randao []byt return nil, DataVersionNil, fmt.Errorf("deneb blinded block execution payload header is nil") } return beaconBlock.DenebBlinded, beaconBlock.Version, nil + case spec.DataVersionElectra: + if beaconBlock.ElectraBlinded == nil { + return nil, DataVersionNil, fmt.Errorf("electra blinded block is nil") + } + if beaconBlock.ElectraBlinded.Body == nil { + return nil, DataVersionNil, fmt.Errorf("electra blinded block body is nil") + } + if beaconBlock.ElectraBlinded.Body.ExecutionPayloadHeader == nil { + return nil, DataVersionNil, fmt.Errorf("electra blinded block execution payload header is nil") + } + return beaconBlock.ElectraBlinded, beaconBlock.Version, nil default: return nil, DataVersionNil, fmt.Errorf("beacon blinded block version %s not supported", beaconBlock.Version) } @@ -149,7 +162,20 @@ func (gc *GoClient) GetBeaconBlock(slot phase0.Slot, graffitiBytes, randao []byt return nil, DataVersionNil, fmt.Errorf("deneb block execution payload is nil") } return beaconBlock.Deneb, beaconBlock.Version, nil - + case spec.DataVersionElectra: + if beaconBlock.Electra == nil { + return nil, DataVersionNil, fmt.Errorf("electra block contents is nil") + } + if beaconBlock.Electra.Block == nil { + return nil, DataVersionNil, fmt.Errorf("electra block is nil") + } + if beaconBlock.Electra.Block.Body == nil { + return nil, DataVersionNil, fmt.Errorf("electra block body is nil") + } + if beaconBlock.Electra.Block.Body.ExecutionPayload == nil { + return nil, DataVersionNil, fmt.Errorf("electra block execution payload is nil") + } + return beaconBlock.Electra, beaconBlock.Version, nil default: return nil, DataVersionNil, fmt.Errorf("beacon block version %s not supported", beaconBlock.Version) } @@ -182,6 +208,20 @@ func (gc *GoClient) SubmitBlindedBeaconBlock(block *api.VersionedBlindedProposal Message: block.Deneb, } copy(signedBlock.Deneb.Signature[:], sig[:]) + case spec.DataVersionElectra: + if block.Electra == nil { + return fmt.Errorf("electra block contents is nil") + } + if block.Electra.Body == nil { + return fmt.Errorf("electra block body is nil") + } + if block.Electra.Body.ExecutionPayloadHeader == nil { + return fmt.Errorf("electra block execution payload header is nil") + } + signedBlock.Electra = &apiv1electra.SignedBlindedBeaconBlock{ + Message: block.Electra, + } + copy(signedBlock.Electra.Signature[:], sig[:]) default: return fmt.Errorf("unknown block version") } @@ -295,6 +335,27 @@ func (gc *GoClient) SubmitBeaconBlock(block *api.VersionedProposal, sig phase0.B Blobs: block.Deneb.Blobs, } copy(signedBlock.Deneb.SignedBlock.Signature[:], sig[:]) + case spec.DataVersionElectra: + if block.Electra == nil { + return fmt.Errorf("electra block contents is nil") + } + if block.Electra.Block == nil { + return fmt.Errorf("electra block is nil") + } + if block.Electra.Block.Body == nil { + return fmt.Errorf("electra block body is nil") + } + if block.Electra.Block.Body.ExecutionPayload == nil { + return fmt.Errorf("electra block execution payload header is nil") + } + signedBlock.Electra = &apiv1electra.SignedBlockContents{ + SignedBlock: &electra.SignedBeaconBlock{ + Message: block.Electra.Block, + }, + KZGProofs: block.Electra.KZGProofs, + Blobs: block.Electra.Blobs, + } + copy(signedBlock.Electra.SignedBlock.Signature[:], sig[:]) default: return fmt.Errorf("unknown block version") } diff --git a/beacon/goclient/testdata/mock-beacon-responses.json b/beacon/goclient/testdata/mock-beacon-responses.json index 1206a97b61..9aa238b73a 100644 --- a/beacon/goclient/testdata/mock-beacon-responses.json +++ b/beacon/goclient/testdata/mock-beacon-responses.json @@ -24,6 +24,10 @@ "BELLATRIX_FORK_EPOCH": "144896", "CAPELLA_FORK_VERSION": "0x03000000", "CAPELLA_FORK_EPOCH": "194048", + "DENEB_FORK_VERSION": "0x04000000", + "DENEB_FORK_EPOCH": "269568", + "ELECTRA_FORK_VERSION": "0x05000000", + "ELECTRA_FORK_EPOCH": "18446744073709551615", "SECONDS_PER_SLOT": "12", "SECONDS_PER_ETH1_BLOCK": "14", "MIN_VALIDATOR_WITHDRAWABILITY_DELAY": "256", diff --git a/beacon/goclient/types.go b/beacon/goclient/types.go index d2249b46aa..9589c2a87c 100644 --- a/beacon/goclient/types.go +++ b/beacon/goclient/types.go @@ -1,15 +1,18 @@ package goclient import ( + "math" + "github.com/attestantio/go-eth2-client/spec/phase0" ) var ( - SyncCommitteeSize uint64 = 512 - SyncCommitteeSubnetCount uint64 = 4 - TargetAggregatorsPerSyncSubcommittee uint64 = 16 - EpochsPerSyncCommitteePeriod uint64 = 256 - TargetAggregatorsPerCommittee uint64 = 16 - FarFutureEpoch phase0.Epoch = 1<<64 - 1 - IntervalsPerSlot uint64 = 3 + SyncCommitteeSize uint64 = 512 + SyncCommitteeSubnetCount uint64 = 4 + TargetAggregatorsPerSyncSubcommittee uint64 = 16 + EpochsPerSyncCommitteePeriod uint64 = 256 + TargetAggregatorsPerCommittee uint64 = 16 + // FarFutureEpoch is the null representation of an epoch. + FarFutureEpoch phase0.Epoch = math.MaxUint64 + IntervalsPerSlot uint64 = 3 ) diff --git a/ekm/eth_key_manager_signer.go b/ekm/eth_key_manager_signer.go index 763e9e85e4..67af7c6034 100644 --- a/ekm/eth_key_manager_signer.go +++ b/ekm/eth_key_manager_signer.go @@ -9,10 +9,12 @@ import ( eth2apiv1 "github.com/attestantio/go-eth2-client/api/v1" apiv1capella "github.com/attestantio/go-eth2-client/api/v1/capella" apiv1deneb "github.com/attestantio/go-eth2-client/api/v1/deneb" + apiv1electra "github.com/attestantio/go-eth2-client/api/v1/electra" "github.com/attestantio/go-eth2-client/spec" "github.com/attestantio/go-eth2-client/spec/altair" "github.com/attestantio/go-eth2-client/spec/capella" "github.com/attestantio/go-eth2-client/spec/deneb" + "github.com/attestantio/go-eth2-client/spec/electra" "github.com/attestantio/go-eth2-client/spec/phase0" ssz "github.com/ferranbt/fastssz" "github.com/herumi/bls-eth-go-binary/bls" @@ -154,6 +156,12 @@ func (km *ethKeyManagerSigner) signBeaconObject(obj ssz.HashRoot, domain phase0. Deneb: v, } return km.signer.SignBeaconBlock(vBlock, domain, pk) + case *electra.BeaconBlock: + vBlock := &spec.VersionedBeaconBlock{ + Version: spec.DataVersionElectra, + Electra: v, + } + return km.signer.SignBeaconBlock(vBlock, domain, pk) case *apiv1capella.BlindedBeaconBlock: vBlindedBlock := &api.VersionedBlindedBeaconBlock{ Version: spec.DataVersionCapella, @@ -166,6 +174,12 @@ func (km *ethKeyManagerSigner) signBeaconObject(obj ssz.HashRoot, domain phase0. Deneb: v, } return km.signer.SignBlindedBeaconBlock(vBlindedBlock, domain, pk) + case *apiv1electra.BlindedBeaconBlock: + vBlindedBlock := &api.VersionedBlindedBeaconBlock{ + Version: spec.DataVersionElectra, + Electra: v, + } + return km.signer.SignBlindedBeaconBlock(vBlindedBlock, domain, pk) default: return nil, nil, fmt.Errorf("obj type is unknown: %T", obj) } diff --git a/go.mod b/go.mod index c73e323e9f..34c16c1e14 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.22.6 require ( github.com/aquasecurity/table v1.8.0 - github.com/attestantio/go-eth2-client v0.21.7 + github.com/attestantio/go-eth2-client v0.24.0 github.com/btcsuite/btcd/btcec/v2 v2.3.4 github.com/cespare/xxhash/v2 v2.3.0 github.com/dgraph-io/badger/v4 v4.2.0 @@ -36,8 +36,8 @@ require ( github.com/sanity-io/litter v1.5.6 github.com/sourcegraph/conc v0.3.0 github.com/spf13/cobra v1.8.1 - github.com/ssvlabs/eth2-key-manager v1.4.2 - github.com/ssvlabs/ssv-spec v1.0.2 + github.com/ssvlabs/eth2-key-manager v1.5.0 + github.com/ssvlabs/ssv-spec v0.0.0-20250219144831-3a9cb8e35c0c github.com/status-im/keycard-go v0.2.0 github.com/stretchr/testify v1.9.0 github.com/wealdtech/go-eth2-types/v2 v2.8.1 @@ -194,7 +194,7 @@ require ( github.com/pion/transport/v2 v2.2.10 // indirect github.com/pion/turn/v2 v2.1.6 // indirect github.com/pion/webrtc/v3 v3.3.0 // indirect - github.com/pk910/dynamic-ssz v0.0.3 // indirect + github.com/pk910/dynamic-ssz v0.0.4 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/polydawn/refmt v0.89.0 // indirect github.com/prometheus/client_model v0.6.1 // indirect @@ -252,9 +252,3 @@ require ( replace github.com/google/flatbuffers => github.com/google/flatbuffers v1.11.0 replace github.com/dgraph-io/ristretto => github.com/dgraph-io/ristretto v0.1.1-0.20211108053508-297c39e6640f - -// github.com/attestantio/go-eth2-client doesn't support el_offline flag, it causes downtime when EL node is down but CL node is up -// Using a fix from https://github.com/ssvlabs/go-eth2-client/commits/syncing-el-offline-v0.21.7/ (https://github.com/attestantio/go-eth2-client/pull/192) -// NOTE: Prysm doesn't set el_offline correctly (https://github.com/prysmaticlabs/prysm/issues/14226), -// so the fix uses workaround with checking sync distance to check if EL is offline -replace github.com/attestantio/go-eth2-client => github.com/ssvlabs/go-eth2-client v0.6.31-0.20250203214635-0137e67b3b10 diff --git a/go.sum b/go.sum index 31ab21f2a1..d37a2e6b56 100644 --- a/go.sum +++ b/go.sum @@ -38,6 +38,8 @@ github.com/aristanetworks/glog v0.0.0-20191112221043-67e8567f59f3/go.mod h1:KASm github.com/aristanetworks/goarista v0.0.0-20200805130819-fd197cf57d96 h1:XJH0YfVFKbq782tlNThzN/Ud5qm/cx6LXOA/P6RkTxc= github.com/aristanetworks/goarista v0.0.0-20200805130819-fd197cf57d96/go.mod h1:QZe5Yh80Hp1b6JxQdpfSEEe8X7hTyTEZSosSrFf/oJE= github.com/aristanetworks/splunk-hec-go v0.3.3/go.mod h1:1VHO9r17b0K7WmOlLb9nTk/2YanvOEnLMUgsFrxBROc= +github.com/attestantio/go-eth2-client v0.24.0 h1:lGVbcnhlBwRglt1Zs56JOCgXVyLWKFZOmZN8jKhE7Ws= +github.com/attestantio/go-eth2-client v0.24.0/go.mod h1:/KTLN3WuH1xrJL7ZZrpBoWM1xCCihnFbzequD5L+83o= github.com/bazelbuild/rules_go v0.23.2 h1:Wxu7JjqnF78cKZbsBsARLSXx/jlGaSLCnUV3mTlyHvM= github.com/bazelbuild/rules_go v0.23.2/go.mod h1:MC23Dc/wkXEyk3Wpq6lCqz0ZAYOZDw2DR5y3N1q2i7M= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= @@ -614,8 +616,8 @@ github.com/pion/turn/v2 v2.1.6 h1:Xr2niVsiPTB0FPtt+yAWKFUkU1eotQbGgpTIld4x1Gc= github.com/pion/turn/v2 v2.1.6/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY= github.com/pion/webrtc/v3 v3.3.0 h1:Rf4u6n6U5t5sUxhYPQk/samzU/oDv7jk6BA5hyO2F9I= github.com/pion/webrtc/v3 v3.3.0/go.mod h1:hVmrDJvwhEertRWObeb1xzulzHGeVUoPlWvxdGzcfU0= -github.com/pk910/dynamic-ssz v0.0.3 h1:fCWzFowq9P6SYCc7NtJMkZcIHk+r5hSVD+32zVi6Aio= -github.com/pk910/dynamic-ssz v0.0.3/go.mod h1:b6CrLaB2X7pYA+OSEEbkgXDEcRnjLOZIxZTsMuO/Y9c= +github.com/pk910/dynamic-ssz v0.0.4 h1:DT29+1055tCEPCaR4V/ez+MOKW7BzBsmjyFvBRqx0ME= +github.com/pk910/dynamic-ssz v0.0.4/go.mod h1:b6CrLaB2X7pYA+OSEEbkgXDEcRnjLOZIxZTsMuO/Y9c= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -739,12 +741,10 @@ github.com/spf13/pflag v1.0.1-0.20170901120850-7aff26db30c1/go.mod h1:DYY7MBk1bd github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.0.0/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= -github.com/ssvlabs/eth2-key-manager v1.4.2 h1:YdeI7vk9jSa8vfSKogx2aSjtYz4eS9Bp85E0yfQF95o= -github.com/ssvlabs/eth2-key-manager v1.4.2/go.mod h1:RqsGIMCsOeUJQmC2nytr82z5vqn5gN3i3VAoY0OydV8= -github.com/ssvlabs/go-eth2-client v0.6.31-0.20250203214635-0137e67b3b10 h1:kRpjzC8yzk326TbRXWl+hhUbFHKahmaPX+JkzXIKvHc= -github.com/ssvlabs/go-eth2-client v0.6.31-0.20250203214635-0137e67b3b10/go.mod h1:d7ZPNrMX8jLfIgML5u7QZxFo2AukLM+5m08iMaLdqb8= -github.com/ssvlabs/ssv-spec v1.0.2 h1:wq0YUo0DOjFUvfwrr6UUvs38EKvSg8r1k6auBrcNvlI= -github.com/ssvlabs/ssv-spec v1.0.2/go.mod h1:lTqsNeTUIfpacMoztbN7YqvFttDigCLjINjy/8I2Wuc= +github.com/ssvlabs/eth2-key-manager v1.5.0 h1:0stZf5JOUPzMU5u5x7OgqYTiE3lyfJx31GP1JRbPhv8= +github.com/ssvlabs/eth2-key-manager v1.5.0/go.mod h1:yeUzAP+SBJXgeXPiGBrLeLuHIQCpeJZV7Jz3Fwzm/zk= +github.com/ssvlabs/ssv-spec v0.0.0-20250219144831-3a9cb8e35c0c h1:3ijOHIppBuQfi8S43R3IZv9xcfy8KhFjel9gOAIOlT8= +github.com/ssvlabs/ssv-spec v0.0.0-20250219144831-3a9cb8e35c0c/go.mod h1:pto7dDv99uVfCZidiLrrKgFR6VYy6WY3PGI1TiGCsIU= github.com/status-im/keycard-go v0.2.0 h1:QDLFswOQu1r5jsycloeQh3bVU8n/NatHHaZobtDnDzA= github.com/status-im/keycard-go v0.2.0/go.mod h1:wlp8ZLbsmrF6g6WjugPAx+IzoLrkdf9+mHxBEeo3Hbg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/integration/qbft/tests/temp_testing_beacon_network.go b/integration/qbft/tests/temp_testing_beacon_network.go index 10dadac3ba..1a0a15fcf7 100644 --- a/integration/qbft/tests/temp_testing_beacon_network.go +++ b/integration/qbft/tests/temp_testing_beacon_network.go @@ -28,8 +28,11 @@ func (bn *TestingBeaconNodeWrapped) GetBeaconNode() *spectestingutils.TestingBea return bn.Bn } -func (bn *TestingBeaconNodeWrapped) GetAttestationData(slot phase0.Slot, committeeIndex phase0.CommitteeIndex) (*phase0.AttestationData, spec.DataVersion, error) { - return bn.Bn.GetAttestationData(slot, committeeIndex) +func (bn *TestingBeaconNodeWrapped) GetAttestationData(slot phase0.Slot) (*phase0.AttestationData, spec.DataVersion, error) { + return bn.Bn.GetAttestationData(slot) +} +func (bn *TestingBeaconNodeWrapped) DataVersion(epoch phase0.Epoch) spec.DataVersion { + return bn.Bn.DataVersion(epoch) } func (bn *TestingBeaconNodeWrapped) DomainData(epoch phase0.Epoch, domain phase0.DomainType) (phase0.Domain, error) { return bn.Bn.DomainData(epoch, domain) @@ -58,7 +61,7 @@ func (bn *TestingBeaconNodeWrapped) SubmitValidatorRegistration(registration *ap func (bn *TestingBeaconNodeWrapped) SubmitVoluntaryExit(voluntaryExit *phase0.SignedVoluntaryExit) error { return bn.Bn.SubmitVoluntaryExit(voluntaryExit) } -func (bn *TestingBeaconNodeWrapped) SubmitAttestations(attestations []*phase0.Attestation) error { +func (bn *TestingBeaconNodeWrapped) SubmitAttestations(attestations []*spec.VersionedAttestation) error { return bn.Bn.SubmitAttestations(attestations) } func (bn *TestingBeaconNodeWrapped) SubmitSyncMessages(msgs []*altair.SyncCommitteeMessage) error { @@ -70,7 +73,7 @@ func (bn *TestingBeaconNodeWrapped) SubmitBlindedBeaconBlock(block *api.Versione func (bn *TestingBeaconNodeWrapped) SubmitSignedContributionAndProof(contribution *altair.SignedContributionAndProof) error { return bn.Bn.SubmitSignedContributionAndProof(contribution) } -func (bn *TestingBeaconNodeWrapped) SubmitSignedAggregateSelectionProof(msg *phase0.SignedAggregateAndProof) error { +func (bn *TestingBeaconNodeWrapped) SubmitSignedAggregateSelectionProof(msg *spec.VersionedSignedAggregateAndProof) error { return bn.Bn.SubmitSignedAggregateSelectionProof(msg) } func (bn *TestingBeaconNodeWrapped) SubmitBeaconBlock(block *api.VersionedProposal, sig phase0.BLSSignature) error { diff --git a/message/validation/partial_validation.go b/message/validation/partial_validation.go index 3df628a933..68d2b1d36e 100644 --- a/message/validation/partial_validation.go +++ b/message/validation/partial_validation.go @@ -207,7 +207,7 @@ func (mv *messageValidator) validatePartialSigMessagesByDutyLogic( } } } else if signedSSVMessage.SSVMessage.MsgID.GetRoleType() == types.RoleSyncCommitteeContribution { - // Rule: The number of signatures must be <= MaxSignaturesInSyncCommitteeContribution for the sync comittee contribution duty + // Rule: The number of signatures must be <= MaxSignaturesInSyncCommitteeContribution for the sync committee contribution duty if partialSignatureMessageCount > maxSignatures { e := ErrTooManyPartialSignatureMessages e.got = partialSignatureMessageCount diff --git a/message/validation/validation_test.go b/message/validation/validation_test.go index b010c01e98..3dbb0fdf69 100644 --- a/message/validation/validation_test.go +++ b/message/validation/validation_test.go @@ -13,6 +13,7 @@ import ( "time" eth2apiv1 "github.com/attestantio/go-eth2-client/api/v1" + "github.com/attestantio/go-eth2-client/spec" "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/herumi/bls-eth-go-binary/bls" pubsub "github.com/libp2p/go-libp2p-pubsub" @@ -696,9 +697,8 @@ func Test_ValidateSSVMessage(t *testing.T) { //// Get error when receiving a message with over 13 partial signatures t.Run("partial message too big", func(t *testing.T) { - slot := netCfg.Beacon.FirstSlotAtEpoch(1) - - msg := spectestingutils.PostConsensusAttestationMsg(ks.Shares[1], 1, specqbft.Height(slot)) + // slot := netCfg.Beacon.FirstSlotAtEpoch(1) + msg := spectestingutils.PostConsensusAttestationMsg(ks.Shares[1], 1, spec.DataVersionPhase0) for i := 0; i < 1512; i++ { msg.Messages = append(msg.Messages, msg.Messages[0]) } @@ -739,7 +739,7 @@ func Test_ValidateSSVMessage(t *testing.T) { slot := netCfg.Beacon.FirstSlotAtEpoch(1) - msg := spectestingutils.SignPartialSigSSVMessage(ks, spectestingutils.SSVMsgAggregator(nil, spectestingutils.PostConsensusAggregatorMsg(ks.Shares[1], 1))) + msg := spectestingutils.SignPartialSigSSVMessage(ks, spectestingutils.SSVMsgAggregator(nil, spectestingutils.PostConsensusAggregatorMsg(ks.Shares[1], 1, spec.DataVersionPhase0))) msg.OperatorIDs = []spectypes.OperatorID{0} receivedAt := netCfg.Beacon.GetSlotStartTime(slot) @@ -754,7 +754,7 @@ func Test_ValidateSSVMessage(t *testing.T) { slot := netCfg.Beacon.FirstSlotAtEpoch(1) - ssvMessage := spectestingutils.SSVMsgAggregator(nil, spectestingutils.PostConsensusAggregatorMsg(ks.Shares[1], 1)) + ssvMessage := spectestingutils.SSVMsgAggregator(nil, spectestingutils.PostConsensusAggregatorMsg(ks.Shares[1], 1, spec.DataVersionPhase0)) ssvMessage.MsgID = committeeIdentifier partialSigSSVMessage := spectestingutils.SignPartialSigSSVMessage(ks, ssvMessage) partialSigSSVMessage.OperatorIDs = []spectypes.OperatorID{2} @@ -774,7 +774,7 @@ func Test_ValidateSSVMessage(t *testing.T) { slot := netCfg.Beacon.FirstSlotAtEpoch(1) - messages := spectestingutils.PostConsensusAggregatorMsg(ks.Shares[1], 1) + messages := spectestingutils.PostConsensusAggregatorMsg(ks.Shares[1], 1, spec.DataVersionPhase0) messages.Messages = nil ssvMessage := spectestingutils.SSVMsgAggregator(nil, messages) ssvMessage.MsgID = committeeIdentifier @@ -792,7 +792,7 @@ func Test_ValidateSSVMessage(t *testing.T) { slot := netCfg.Beacon.FirstSlotAtEpoch(1) - partialSigSSVMessage := spectestingutils.SignPartialSigSSVMessage(ks, spectestingutils.SSVMsgAggregator(nil, spectestingutils.PostConsensusAggregatorMsg(ks.Shares[1], 1))) + partialSigSSVMessage := spectestingutils.SignPartialSigSSVMessage(ks, spectestingutils.SSVMsgAggregator(nil, spectestingutils.PostConsensusAggregatorMsg(ks.Shares[1], 1, spec.DataVersionPhase0))) partialSigSSVMessage.Signatures = [][]byte{{1}} receivedAt := netCfg.Beacon.GetSlotStartTime(slot) @@ -830,7 +830,7 @@ func Test_ValidateSSVMessage(t *testing.T) { validator := New(netCfg, validatorStore, ds, signatureVerifier).(*messageValidator) - messages := spectestingutils.PostConsensusAggregatorMsg(ks.Shares[1], 1) + messages := spectestingutils.PostConsensusAggregatorMsg(ks.Shares[1], 1, spec.DataVersionPhase0) messages.Type = msgType encodedMessages, err := messages.Encode() @@ -863,7 +863,7 @@ func Test_ValidateSSVMessage(t *testing.T) { t.Run("invalid message type", func(t *testing.T) { validator := New(netCfg, validatorStore, dutyStore, signatureVerifier).(*messageValidator) - messages := spectestingutils.PostConsensusAggregatorMsg(ks.Shares[1], 1) + messages := spectestingutils.PostConsensusAggregatorMsg(ks.Shares[1], 1, spec.DataVersionPhase0) messages.Type = math.MaxUint64 encodedMessages, err := messages.Encode() @@ -909,7 +909,7 @@ func Test_ValidateSSVMessage(t *testing.T) { validator := New(netCfg, validatorStore, ds, signatureVerifier).(*messageValidator) - messages := spectestingutils.PostConsensusAggregatorMsg(ks.Shares[1], 1) + messages := spectestingutils.PostConsensusAggregatorMsg(ks.Shares[1], 1, spec.DataVersionPhase0) messages.Type = msgType encodedMessages, err := messages.Encode() @@ -1643,7 +1643,7 @@ func Test_ValidateSSVMessage(t *testing.T) { slot := netCfg.Beacon.FirstSlotAtEpoch(1) - ssvMessage := spectestingutils.SSVMsgAggregator(nil, spectestingutils.PostConsensusAggregatorMsg(ks.Shares[1], 1)) + ssvMessage := spectestingutils.SSVMsgAggregator(nil, spectestingutils.PostConsensusAggregatorMsg(ks.Shares[1], 1, spec.DataVersionPhase0)) ssvMessage.MsgID = committeeIdentifier signedSSVMessage := spectestingutils.SignPartialSigSSVMessage(ks, ssvMessage) signedSSVMessage.FullData = []byte{1} @@ -1660,7 +1660,7 @@ func Test_ValidateSSVMessage(t *testing.T) { slot := netCfg.Beacon.FirstSlotAtEpoch(1) - ssvMessage := spectestingutils.SSVMsgAggregator(nil, spectestingutils.PostConsensusAggregatorMsg(ks.Shares[1], 1)) + ssvMessage := spectestingutils.SSVMsgAggregator(nil, spectestingutils.PostConsensusAggregatorMsg(ks.Shares[1], 1, spec.DataVersionPhase0)) ssvMessage.MsgID = committeeIdentifier signedSSVMessage := spectestingutils.SignPartialSigSSVMessage(ks, ssvMessage) signedSSVMessage.OperatorIDs = []spectypes.OperatorID{1, 2} @@ -1678,7 +1678,7 @@ func Test_ValidateSSVMessage(t *testing.T) { slot := netCfg.Beacon.FirstSlotAtEpoch(1) - messages := spectestingutils.PostConsensusAggregatorMsg(ks.Shares[1], 1) + messages := spectestingutils.PostConsensusAggregatorMsg(ks.Shares[1], 1, spec.DataVersionPhase0) for i := 0; i < 12; i++ { messages.Messages = append(messages.Messages, messages.Messages[0]) } @@ -1706,7 +1706,7 @@ func Test_ValidateSSVMessage(t *testing.T) { slot := netCfg.Beacon.FirstSlotAtEpoch(1) - messages := spectestingutils.PostConsensusAggregatorMsg(ks.Shares[1], 1) + messages := spectestingutils.PostConsensusAggregatorMsg(ks.Shares[1], 1, spec.DataVersionPhase0) for i := 0; i < 3; i++ { messages.Messages = append(messages.Messages, messages.Messages[0]) } @@ -1734,7 +1734,7 @@ func Test_ValidateSSVMessage(t *testing.T) { slot := netCfg.Beacon.FirstSlotAtEpoch(1) - messages := spectestingutils.PostConsensusAggregatorMsg(ks.Shares[1], 1) + messages := spectestingutils.PostConsensusAggregatorMsg(ks.Shares[1], 1, spec.DataVersionPhase0) messages.Messages[0].ValidatorIndex = math.MaxUint64 data, err := messages.Encode() diff --git a/network/topics/params/topic_score.go b/network/topics/params/topic_score.go index 54613631f6..ba778ad4b7 100644 --- a/network/topics/params/topic_score.go +++ b/network/topics/params/topic_score.go @@ -27,7 +27,7 @@ const ( maxFirstDeliveryScore = 80 // max score a peer can obtain from first deliveries // P3 - // Mesh scording is disabled for now. + // Mesh scoring is disabled for now. meshDeliveryDecayEpochs = time.Duration(16) meshDeliveryDampeningFactor = 1.0 / 50.0 meshDeliveryCapFactor = 16 diff --git a/networkconfig/config.go b/networkconfig/config.go index 58a1faba7b..347924993c 100644 --- a/networkconfig/config.go +++ b/networkconfig/config.go @@ -8,7 +8,6 @@ import ( "github.com/attestantio/go-eth2-client/spec/phase0" spectypes "github.com/ssvlabs/ssv-spec/types" - "github.com/ssvlabs/ssv/protocol/v2/blockchain/beacon" ) diff --git a/protocol/v2/blockchain/beacon/mock_client.go b/protocol/v2/blockchain/beacon/mock_client.go index a5fd3e8f9d..a428ca217b 100644 --- a/protocol/v2/blockchain/beacon/mock_client.go +++ b/protocol/v2/blockchain/beacon/mock_client.go @@ -29,6 +29,7 @@ import ( type MockbeaconDuties struct { ctrl *gomock.Controller recorder *MockbeaconDutiesMockRecorder + isgomock struct{} } // MockbeaconDutiesMockRecorder is the mock recorder for MockbeaconDuties. @@ -111,6 +112,7 @@ func (mr *MockbeaconDutiesMockRecorder) SyncCommitteeDuties(ctx, epoch, indices type MockbeaconSubscriber struct { ctrl *gomock.Controller recorder *MockbeaconSubscriberMockRecorder + isgomock struct{} } // MockbeaconSubscriberMockRecorder is the mock recorder for MockbeaconSubscriber. @@ -162,6 +164,7 @@ func (mr *MockbeaconSubscriberMockRecorder) SubmitSyncCommitteeSubscriptions(ctx type MockbeaconValidator struct { ctrl *gomock.Controller recorder *MockbeaconValidatorMockRecorder + isgomock struct{} } // MockbeaconValidatorMockRecorder is the mock recorder for MockbeaconValidator. @@ -200,6 +203,7 @@ func (mr *MockbeaconValidatorMockRecorder) GetValidatorData(validatorPubKeys any type Mockproposer struct { ctrl *gomock.Controller recorder *MockproposerMockRecorder + isgomock struct{} } // MockproposerMockRecorder is the mock recorder for Mockproposer. @@ -237,6 +241,7 @@ func (mr *MockproposerMockRecorder) SubmitProposalPreparation(feeRecipients any) type Mocksigner struct { ctrl *gomock.Controller recorder *MocksignerMockRecorder + isgomock struct{} } // MocksignerMockRecorder is the mock recorder for Mocksigner. @@ -275,6 +280,7 @@ func (mr *MocksignerMockRecorder) ComputeSigningRoot(object, domain any) *gomock type MockBeaconNode struct { ctrl *gomock.Controller recorder *MockBeaconNodeMockRecorder + isgomock struct{} } // MockBeaconNodeMockRecorder is the mock recorder for MockBeaconNode. @@ -324,6 +330,20 @@ func (mr *MockBeaconNodeMockRecorder) ComputeSigningRoot(object, domain any) *go return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ComputeSigningRoot", reflect.TypeOf((*MockBeaconNode)(nil).ComputeSigningRoot), object, domain) } +// DataVersion mocks base method. +func (m *MockBeaconNode) DataVersion(epoch phase0.Epoch) spec.DataVersion { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DataVersion", epoch) + ret0, _ := ret[0].(spec.DataVersion) + return ret0 +} + +// DataVersion indicates an expected call of DataVersion. +func (mr *MockBeaconNodeMockRecorder) DataVersion(epoch any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DataVersion", reflect.TypeOf((*MockBeaconNode)(nil).DataVersion), epoch) +} + // DomainData mocks base method. func (m *MockBeaconNode) DomainData(epoch phase0.Epoch, domain phase0.DomainType) (phase0.Domain, error) { m.ctrl.T.Helper() @@ -354,9 +374,9 @@ func (mr *MockBeaconNodeMockRecorder) Events(ctx, topics, handler any) *gomock.C } // GetAttestationData mocks base method. -func (m *MockBeaconNode) GetAttestationData(slot phase0.Slot, committeeIndex phase0.CommitteeIndex) (*phase0.AttestationData, spec.DataVersion, error) { +func (m *MockBeaconNode) GetAttestationData(slot phase0.Slot) (*phase0.AttestationData, spec.DataVersion, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetAttestationData", slot, committeeIndex) + ret := m.ctrl.Call(m, "GetAttestationData", slot) ret0, _ := ret[0].(*phase0.AttestationData) ret1, _ := ret[1].(spec.DataVersion) ret2, _ := ret[2].(error) @@ -364,9 +384,9 @@ func (m *MockBeaconNode) GetAttestationData(slot phase0.Slot, committeeIndex pha } // GetAttestationData indicates an expected call of GetAttestationData. -func (mr *MockBeaconNodeMockRecorder) GetAttestationData(slot, committeeIndex any) *gomock.Call { +func (mr *MockBeaconNodeMockRecorder) GetAttestationData(slot any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAttestationData", reflect.TypeOf((*MockBeaconNode)(nil).GetAttestationData), slot, committeeIndex) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAttestationData", reflect.TypeOf((*MockBeaconNode)(nil).GetAttestationData), slot) } // GetBeaconBlock mocks base method. @@ -493,7 +513,7 @@ func (mr *MockBeaconNodeMockRecorder) SubmitAggregateSelectionProof(slot, commit } // SubmitAttestations mocks base method. -func (m *MockBeaconNode) SubmitAttestations(attestations []*phase0.Attestation) error { +func (m *MockBeaconNode) SubmitAttestations(attestations []*spec.VersionedAttestation) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SubmitAttestations", attestations) ret0, _ := ret[0].(error) @@ -563,7 +583,7 @@ func (mr *MockBeaconNodeMockRecorder) SubmitProposalPreparation(feeRecipients an } // SubmitSignedAggregateSelectionProof mocks base method. -func (m *MockBeaconNode) SubmitSignedAggregateSelectionProof(msg *phase0.SignedAggregateAndProof) error { +func (m *MockBeaconNode) SubmitSignedAggregateSelectionProof(msg *spec.VersionedSignedAggregateAndProof) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SubmitSignedAggregateSelectionProof", msg) ret0, _ := ret[0].(error) diff --git a/protocol/v2/qbft/spectest/qbft_mapping_test.go b/protocol/v2/qbft/spectest/qbft_mapping_test.go index 734f089644..9d4cc70fb7 100644 --- a/protocol/v2/qbft/spectest/qbft_mapping_test.go +++ b/protocol/v2/qbft/spectest/qbft_mapping_test.go @@ -19,7 +19,7 @@ import ( func TestQBFTMapping(t *testing.T) { path, _ := os.Getwd() - jsonTests, err := protocoltesting.GetSpecTestJSON(path, "qbft") + jsonTests, err := protocoltesting.GenerateSpecTestJSON(path, "qbft") require.NoError(t, err) untypedTests := map[string]interface{}{} diff --git a/protocol/v2/ssv/runner/aggregator.go b/protocol/v2/ssv/runner/aggregator.go index 58e2372b6f..0807a574d0 100644 --- a/protocol/v2/ssv/runner/aggregator.go +++ b/protocol/v2/ssv/runner/aggregator.go @@ -6,6 +6,8 @@ import ( "encoding/json" "time" + "github.com/attestantio/go-eth2-client/spec" + "github.com/attestantio/go-eth2-client/spec/electra" "github.com/attestantio/go-eth2-client/spec/phase0" ssz "github.com/ferranbt/fastssz" "github.com/pkg/errors" @@ -150,13 +152,13 @@ func (r *AggregatorRunner) ProcessConsensus(ctx context.Context, logger *zap.Log decidedValue := encDecidedValue.(*spectypes.ValidatorConsensusData) - aggregateAndProof, err := decidedValue.GetAggregateAndProof() + _, aggregateAndProofHashRoot, err := decidedValue.GetAggregateAndProof() if err != nil { return errors.Wrap(err, "could not get aggregate and proof") } // specific duty sig - msg, err := r.BaseRunner.signBeaconObject(r, r.BaseRunner.State.StartingDuty.(*spectypes.ValidatorDuty), aggregateAndProof, decidedValue.Duty.Slot, spectypes.DomainAggregateAndProof) + msg, err := r.BaseRunner.signBeaconObject(r, r.BaseRunner.State.StartingDuty.(*spectypes.ValidatorDuty), aggregateAndProofHashRoot, decidedValue.Duty.Slot, spectypes.DomainAggregateAndProof) if err != nil { return errors.Wrap(err, "failed signing attestation data") } @@ -228,14 +230,14 @@ func (r *AggregatorRunner) ProcessPostConsensus(ctx context.Context, logger *zap if err != nil { return errors.Wrap(err, "could not create consensus data") } - aggregateAndProof, err := cd.GetAggregateAndProof() + aggregateAndProof, _, err := cd.GetAggregateAndProof() if err != nil { return errors.Wrap(err, "could not get aggregate and proof") } - msg := &phase0.SignedAggregateAndProof{ - Message: aggregateAndProof, - Signature: specSig, + msg, err := constructVersionedSignedAggregateAndProof(*aggregateAndProof, specSig) + if err != nil { + return errors.Wrap(err, "could not construct versioned aggregate and proof") } start := time.Now() @@ -277,12 +279,12 @@ func (r *AggregatorRunner) expectedPostConsensusRootsAndDomain() ([]ssz.HashRoot if err != nil { return nil, spectypes.DomainError, errors.Wrap(err, "could not create consensus data") } - aggregateAndProof, err := cd.GetAggregateAndProof() + _, hashRoot, err := cd.GetAggregateAndProof() if err != nil { return nil, phase0.DomainType{}, errors.Wrap(err, "could not get aggregate and proof") } - return []ssz.HashRoot{aggregateAndProof}, spectypes.DomainAggregateAndProof, nil + return []ssz.HashRoot{hashRoot}, spectypes.DomainAggregateAndProof, nil } // executeDuty steps: @@ -392,3 +394,47 @@ func (r *AggregatorRunner) GetRoot() ([32]byte, error) { ret := sha256.Sum256(marshaledRoot) return ret, nil } + +// Constructs a VersionedSignedAggregateAndProof from a VersionedAggregateAndProof and a signature +func constructVersionedSignedAggregateAndProof(aggregateAndProof spec.VersionedAggregateAndProof, signature phase0.BLSSignature) (*spec.VersionedSignedAggregateAndProof, error) { + ret := &spec.VersionedSignedAggregateAndProof{ + Version: aggregateAndProof.Version, + } + + switch ret.Version { + case spec.DataVersionPhase0: + ret.Phase0 = &phase0.SignedAggregateAndProof{ + Message: aggregateAndProof.Phase0, + Signature: signature, + } + case spec.DataVersionAltair: + ret.Altair = &phase0.SignedAggregateAndProof{ + Message: aggregateAndProof.Altair, + Signature: signature, + } + case spec.DataVersionBellatrix: + ret.Bellatrix = &phase0.SignedAggregateAndProof{ + Message: aggregateAndProof.Bellatrix, + Signature: signature, + } + case spec.DataVersionCapella: + ret.Capella = &phase0.SignedAggregateAndProof{ + Message: aggregateAndProof.Capella, + Signature: signature, + } + case spec.DataVersionDeneb: + ret.Deneb = &phase0.SignedAggregateAndProof{ + Message: aggregateAndProof.Deneb, + Signature: signature, + } + case spec.DataVersionElectra: + ret.Electra = &electra.SignedAggregateAndProof{ + Message: aggregateAndProof.Electra, + Signature: signature, + } + default: + return nil, errors.New("unknown version for signed aggregate and proof") + } + + return ret, nil +} diff --git a/protocol/v2/ssv/runner/committee.go b/protocol/v2/ssv/runner/committee.go index 0644eb18e6..c59054d5cf 100644 --- a/protocol/v2/ssv/runner/committee.go +++ b/protocol/v2/ssv/runner/committee.go @@ -9,14 +9,15 @@ import ( "math" "time" + "github.com/attestantio/go-eth2-client/spec" "github.com/attestantio/go-eth2-client/spec/altair" "github.com/attestantio/go-eth2-client/spec/phase0" ssz "github.com/ferranbt/fastssz" "github.com/pkg/errors" - "github.com/prysmaticlabs/go-bitfield" "go.uber.org/zap" specqbft "github.com/ssvlabs/ssv-spec/qbft" + specssv "github.com/ssvlabs/ssv-spec/ssv" spectypes "github.com/ssvlabs/ssv-spec/types" "github.com/ssvlabs/ssv/logging/fields" @@ -224,16 +225,20 @@ func (cr *CommitteeRunner) ProcessConsensus(ctx context.Context, logger *zap.Log beaconVote := decidedValue.(*spectypes.BeaconVote) validDuties := 0 - for _, duty := range duty.(*spectypes.CommitteeDuty).ValidatorDuties { - if err := cr.DutyGuard.ValidDuty(duty.Type, spectypes.ValidatorPK(duty.PubKey), duty.DutySlot()); err != nil { - logger.Warn("duty is no longer valid", fields.Validator(duty.PubKey[:]), fields.BeaconRole(duty.Type), zap.Error(err)) + + epoch := cr.beacon.GetBeaconNetwork().EstimatedEpochAtSlot(duty.DutySlot()) + version := cr.beacon.DataVersion(epoch) + + for _, validatorDuty := range duty.(*spectypes.CommitteeDuty).ValidatorDuties { + if err := cr.DutyGuard.ValidDuty(validatorDuty.Type, spectypes.ValidatorPK(validatorDuty.PubKey), validatorDuty.DutySlot()); err != nil { + logger.Warn("duty is no longer valid", fields.Validator(validatorDuty.PubKey[:]), fields.BeaconRole(validatorDuty.Type), zap.Error(err)) continue } - switch duty.Type { + switch validatorDuty.Type { case spectypes.BNRoleAttester: validDuties++ - attestationData := constructAttestationData(beaconVote, duty) - partialMsg, err := cr.BaseRunner.signBeaconObject(cr, duty, attestationData, duty.DutySlot(), + attestationData := constructAttestationData(beaconVote, validatorDuty, version) + partialMsg, err := cr.BaseRunner.signBeaconObject(cr, validatorDuty, attestationData, validatorDuty.DutySlot(), spectypes.DomainAttester) if err != nil { return errors.Wrap(err, "failed signing attestation data") @@ -246,8 +251,8 @@ func (cr *CommitteeRunner) ProcessConsensus(ctx context.Context, logger *zap.Log return errors.Wrap(err, "failed to hash attestation data") } logger.Debug("signed attestation data", - zap.Uint64("validator_index", uint64(duty.ValidatorIndex)), - zap.String("pub_key", hex.EncodeToString(duty.PubKey[:])), + zap.Uint64("validator_index", uint64(validatorDuty.ValidatorIndex)), + zap.String("pub_key", hex.EncodeToString(validatorDuty.PubKey[:])), zap.Any("attestation_data", attestationData), zap.String("attestation_data_root", hex.EncodeToString(attDataRoot[:])), zap.String("signing_root", hex.EncodeToString(partialMsg.SigningRoot[:])), @@ -256,14 +261,14 @@ func (cr *CommitteeRunner) ProcessConsensus(ctx context.Context, logger *zap.Log case spectypes.BNRoleSyncCommittee: validDuties++ blockRoot := beaconVote.BlockRoot - partialMsg, err := cr.BaseRunner.signBeaconObject(cr, duty, spectypes.SSZBytes(blockRoot[:]), duty.DutySlot(), + partialMsg, err := cr.BaseRunner.signBeaconObject(cr, validatorDuty, spectypes.SSZBytes(blockRoot[:]), validatorDuty.DutySlot(), spectypes.DomainSyncCommittee) if err != nil { return errors.Wrap(err, "failed signing sync committee message") } postConsensusMsg.Messages = append(postConsensusMsg.Messages, partialMsg) default: - return fmt.Errorf("invalid duty type: %s", duty.Type) + return fmt.Errorf("invalid duty type: %s", validatorDuty.Type) } } if validDuties == 0 { @@ -341,7 +346,7 @@ func (cr *CommitteeRunner) ProcessPostConsensus(ctx context.Context, logger *zap } var anyErr error - attestationsToSubmit := make(map[phase0.ValidatorIndex]*phase0.Attestation) + attestationsToSubmit := make(map[phase0.ValidatorIndex]*spec.VersionedAttestation) syncCommitteeMessagesToSubmit := make(map[phase0.ValidatorIndex]*altair.SyncCommitteeMessage) // Get unique roots to avoid repetition @@ -426,9 +431,13 @@ func (cr *CommitteeRunner) ProcessPostConsensus(ctx context.Context, logger *zap syncCommitteeMessagesToSubmit[validator] = syncMsg } else if role == spectypes.BNRoleAttester { - att := sszObject.(*phase0.Attestation) + att := sszObject.(*spec.VersionedAttestation) // Insert signature - att.Signature = specSig + att, err = specssv.VersionedAttestationWithSignature(att, specSig) + if err != nil { + anyErr = errors.Wrap(err, "could not insert signature in versioned attestation") + continue + } attestationsToSubmit[validator] = att } @@ -441,7 +450,7 @@ func (cr *CommitteeRunner) ProcessPostConsensus(ctx context.Context, logger *zap logger = logger.With(fields.PostConsensusTime(cr.measurements.PostConsensusTime())) // Submit multiple attestations - attestations := make([]*phase0.Attestation, 0, len(attestationsToSubmit)) + attestations := make([]*spec.VersionedAttestation, 0, len(attestationsToSubmit)) for _, att := range attestationsToSubmit { attestations = append(attestations, att) } @@ -466,11 +475,16 @@ func (cr *CommitteeRunner) ProcessPostConsensus(ctx context.Context, logger *zap spectypes.BNRoleAttester) } + attData, err := attestations[0].Data() + if err != nil { + return errors.Wrap(err, "could not get attestation data") + // TODO return error? + } logger.Info("✅ successfully submitted attestations", fields.Epoch(cr.GetBeaconNode().GetBeaconNetwork().EstimatedEpochAtSlot(cr.GetBaseRunner().State.StartingDuty.DutySlot())), fields.Height(cr.BaseRunner.QBFTController.Height), fields.Round(cr.BaseRunner.State.RunningInstance.State.Round), - fields.BlockRoot(attestations[0].Data.BeaconBlockRoot), + fields.BlockRoot(attData.BeaconBlockRoot), fields.SubmissionTime(time.Since(submissionStart)), fields.TotalConsensusTime(time.Since(cr.measurements.consensusStart))) @@ -608,11 +622,11 @@ func (cr CommitteeRunner) expectedPostConsensusRootsAndDomain() ([]ssz.HashRoot, func (cr *CommitteeRunner) expectedPostConsensusRootsAndBeaconObjects(logger *zap.Logger) ( attestationMap map[phase0.ValidatorIndex][32]byte, syncCommitteeMap map[phase0.ValidatorIndex][32]byte, - beaconObjects map[phase0.ValidatorIndex]map[[32]byte]ssz.HashRoot, error error, + beaconObjects map[phase0.ValidatorIndex]map[[32]byte]interface{}, error error, ) { attestationMap = make(map[phase0.ValidatorIndex][32]byte) syncCommitteeMap = make(map[phase0.ValidatorIndex][32]byte) - beaconObjects = make(map[phase0.ValidatorIndex]map[[32]byte]ssz.HashRoot) + beaconObjects = make(map[phase0.ValidatorIndex]map[[32]byte]interface{}) duty := cr.BaseRunner.State.StartingDuty // TODO DecidedValue should be interface?? beaconVoteData := cr.BaseRunner.State.DecidedValue @@ -621,6 +635,11 @@ func (cr *CommitteeRunner) expectedPostConsensusRootsAndBeaconObjects(logger *za return nil, nil, nil, errors.Wrap(err, "could not decode beacon vote") } + slot := duty.DutySlot() + epoch := cr.GetBaseRunner().BeaconNetwork.EstimatedEpochAtSlot(slot) + + dataVersion := cr.beacon.DataVersion(epoch) + for _, validatorDuty := range duty.(*spectypes.CommitteeDuty).ValidatorDuties { if validatorDuty == nil { continue @@ -635,12 +654,11 @@ func (cr *CommitteeRunner) expectedPostConsensusRootsAndBeaconObjects(logger *za switch validatorDuty.Type { case spectypes.BNRoleAttester: // Attestation object - attestationData := constructAttestationData(beaconVote, validatorDuty) - aggregationBitfield := bitfield.NewBitlist(validatorDuty.CommitteeLength) - aggregationBitfield.SetBitAt(validatorDuty.ValidatorCommitteeIndex, true) - unSignedAtt := &phase0.Attestation{ - Data: attestationData, - AggregationBits: aggregationBitfield, + attestationData := constructAttestationData(beaconVote, validatorDuty, dataVersion) + attestationResponse, err := specssv.ConstructVersionedAttestationWithoutSignature(attestationData, dataVersion, validatorDuty) + if err != nil { + logger.Debug("failed to construct attestation", zap.Error(err)) + continue } // Root @@ -659,9 +677,9 @@ func (cr *CommitteeRunner) expectedPostConsensusRootsAndBeaconObjects(logger *za // Add to map attestationMap[validatorDuty.ValidatorIndex] = root if _, ok := beaconObjects[validatorDuty.ValidatorIndex]; !ok { - beaconObjects[validatorDuty.ValidatorIndex] = make(map[[32]byte]ssz.HashRoot) + beaconObjects[validatorDuty.ValidatorIndex] = make(map[[32]byte]interface{}) } - beaconObjects[validatorDuty.ValidatorIndex][root] = unSignedAtt + beaconObjects[validatorDuty.ValidatorIndex][root] = attestationResponse case spectypes.BNRoleSyncCommittee: // Sync committee beacon object syncMsg := &altair.SyncCommitteeMessage{ @@ -687,7 +705,7 @@ func (cr *CommitteeRunner) expectedPostConsensusRootsAndBeaconObjects(logger *za // Set root and beacon object syncCommitteeMap[validatorDuty.ValidatorIndex] = root if _, ok := beaconObjects[validatorDuty.ValidatorIndex]; !ok { - beaconObjects[validatorDuty.ValidatorIndex] = make(map[[32]byte]ssz.HashRoot) + beaconObjects[validatorDuty.ValidatorIndex] = make(map[[32]byte]interface{}) } beaconObjects[validatorDuty.ValidatorIndex][root] = syncMsg default: @@ -702,9 +720,8 @@ func (cr *CommitteeRunner) executeDuty(ctx context.Context, logger *zap.Logger, start := time.Now() slot := duty.DutySlot() - // We set committeeIndex to 0 for simplicity, there is no need to specify it exactly because - // all 64 Ethereum committees assigned to this slot will get the same data to attest for. - attData, _, err := cr.GetBeaconNode().GetAttestationData(slot, 0) + + attData, _, err := cr.GetBeaconNode().GetAttestationData(slot) if err != nil { return errors.Wrap(err, "failed to get attestation data") } @@ -735,12 +752,16 @@ func (cr *CommitteeRunner) GetOperatorSigner() ssvtypes.OperatorSigner { return cr.operatorSigner } -func constructAttestationData(vote *spectypes.BeaconVote, duty *spectypes.ValidatorDuty) *phase0.AttestationData { - return &phase0.AttestationData{ +func constructAttestationData(vote *spectypes.BeaconVote, duty *spectypes.ValidatorDuty, version spec.DataVersion) *phase0.AttestationData { + attData := &phase0.AttestationData{ Slot: duty.Slot, Index: duty.CommitteeIndex, BeaconBlockRoot: vote.BlockRoot, Source: vote.Source, Target: vote.Target, } + if version >= spec.DataVersionElectra { + attData.Index = 0 // EIP-7549: Index should be set to 0 + } + return attData } diff --git a/protocol/v2/ssv/runner/proposer.go b/protocol/v2/ssv/runner/proposer.go index 06fd2bdec9..430f190608 100644 --- a/protocol/v2/ssv/runner/proposer.go +++ b/protocol/v2/ssv/runner/proposer.go @@ -10,9 +10,11 @@ import ( "github.com/attestantio/go-eth2-client/api" apiv1capella "github.com/attestantio/go-eth2-client/api/v1/capella" apiv1deneb "github.com/attestantio/go-eth2-client/api/v1/deneb" + apiv1electra "github.com/attestantio/go-eth2-client/api/v1/electra" "github.com/attestantio/go-eth2-client/spec" "github.com/attestantio/go-eth2-client/spec/capella" "github.com/attestantio/go-eth2-client/spec/deneb" + "github.com/attestantio/go-eth2-client/spec/electra" "github.com/attestantio/go-eth2-client/spec/phase0" ssz "github.com/ferranbt/fastssz" "github.com/pkg/errors" @@ -514,6 +516,8 @@ func summarizeBlock(block any) (summary blockSummary, err error) { return summarizeBlock(b.CapellaBlinded) case spec.DataVersionDeneb: return summarizeBlock(b.DenebBlinded) + case spec.DataVersionElectra: + return summarizeBlock(b.ElectraBlinded) default: return summary, fmt.Errorf("unsupported blinded block version %d", b.Version) } @@ -526,6 +530,11 @@ func summarizeBlock(block any) (summary blockSummary, err error) { return summary, fmt.Errorf("deneb block contents is nil") } return summarizeBlock(b.Deneb.Block) + case spec.DataVersionElectra: + if b.Electra == nil { + return summary, fmt.Errorf("electra block contents is nil") + } + return summarizeBlock(b.Electra.Block) default: return summary, fmt.Errorf("unsupported block version %d", b.Version) } @@ -536,6 +545,8 @@ func summarizeBlock(block any) (summary blockSummary, err error) { return summarizeBlock(b.Capella) case spec.DataVersionDeneb: return summarizeBlock(b.Deneb) + case spec.DataVersionElectra: + return summarizeBlock(b.Electra) default: return summary, fmt.Errorf("unsupported blinded block version %d", b.Version) } @@ -554,6 +565,16 @@ func summarizeBlock(block any) (summary blockSummary, err error) { summary.Hash = b.Body.ExecutionPayload.BlockHash summary.Version = spec.DataVersionDeneb + case *electra.BeaconBlock: + if b == nil || b.Body == nil || b.Body.ExecutionPayload == nil { + return summary, fmt.Errorf("block, body or execution payload is nil") + } + summary.Hash = b.Body.ExecutionPayload.BlockHash + summary.Version = spec.DataVersionElectra + + case *apiv1electra.BlockContents: + return summarizeBlock(b.Block) + case *apiv1deneb.BlockContents: return summarizeBlock(b.Block) @@ -572,6 +593,15 @@ func summarizeBlock(block any) (summary blockSummary, err error) { summary.Hash = b.Body.ExecutionPayloadHeader.BlockHash summary.Blinded = true summary.Version = spec.DataVersionDeneb + + case *apiv1electra.BlindedBeaconBlock: + if b == nil || b.Body == nil || b.Body.ExecutionPayloadHeader == nil { + return summary, fmt.Errorf("block, body or execution payload header is nil") + } + summary.Hash = b.Body.ExecutionPayloadHeader.BlockHash + summary.Blinded = true + summary.Version = spec.DataVersionElectra } + return } diff --git a/protocol/v2/ssv/spectest/ssv_mapping_test.go b/protocol/v2/ssv/spectest/ssv_mapping_test.go index e90905c4ab..f91763ad3f 100644 --- a/protocol/v2/ssv/spectest/ssv_mapping_test.go +++ b/protocol/v2/ssv/spectest/ssv_mapping_test.go @@ -38,7 +38,7 @@ import ( func TestSSVMapping(t *testing.T) { path, err := os.Getwd() require.NoError(t, err) - jsonTests, err := protocoltesting.GetSSVMappingSpecTestJSON(path, "ssv") + jsonTests, err := protocoltesting.GenerateSpecTestJSON(path, "ssv") require.NoError(t, err) logger := logging.TestLogger(t) diff --git a/protocol/v2/testing/test_utils.go b/protocol/v2/testing/test_utils.go index 2ae1c05dbb..babc47c9c2 100644 --- a/protocol/v2/testing/test_utils.go +++ b/protocol/v2/testing/test_utils.go @@ -1,14 +1,10 @@ package testing import ( - "archive/tar" - "compress/gzip" "crypto/rsa" - "encoding/json" "fmt" - "io" - "log" "os" + "os/exec" "path" "path/filepath" "strings" @@ -26,8 +22,7 @@ import ( ) var ( - specModule = "github.com/ssvlabs/ssv-spec" - specTestPath = "spectest/generate/tests.json" + specModule = "github.com/ssvlabs/ssv-spec" ) // TODO: add missing tests @@ -106,53 +101,58 @@ func SignMsg(t *testing.T, sks []*rsa.PrivateKey, signers []spectypes.OperatorID return testingutils.MultiSignQBFTMsg(sks, signers, msg) } -func GetSSVMappingSpecTestJSON(path string, module string) ([]byte, error) { +func GenerateSpecTestJSON(path string, module string) ([]byte, error) { + // Step 1: Get the spec directory. p, err := GetSpecDir(path, module) if err != nil { - return nil, errors.Wrap(err, "could not get spec test dir") + return nil, fmt.Errorf("could not get spec test dir: %w", err) } - gzPath := filepath.Join(p, "spectest", "generate", "tests.json.gz") - untypedTests := map[string]interface{}{} - file, err := os.Open(gzPath) // #nosec G304 - if err != nil { - return nil, errors.Wrap(err, "failed to open gzip file") - } - defer func() { - if err := file.Close(); err != nil { - // Handle the error, log it, or handle it as appropriate - log.Printf("Failed to close file: %v", err) - } - }() + p = filepath.Join(p, "spectest", "generate") - gzipReader, err := gzip.NewReader(file) - if err != nil { - return nil, errors.Wrap(err, "failed to create gzip reader") + // Step 2: Create a temporary directory at /tmp/. + tmpDir := filepath.Join("/tmp", module) + if err := os.MkdirAll(tmpDir, 0750); err != nil { + return nil, fmt.Errorf("failed to create tmp directory: %w", err) } + // Clean up the temp directory when the function exits. defer func() { - if err := gzipReader.Close(); err != nil { - // Handle the error, log it, or handle it as appropriate - log.Printf("Failed to close reader: %v", err) + err := os.RemoveAll(tmpDir) + if err != nil { + fmt.Printf("failed to remove tmp directory: %s", err.Error()) } }() - decompressedData, err := io.ReadAll(gzipReader) + // Step 3: Build the Go package, outputting an executable to tmpDir. + // We'll name the executable after the module. + binaryPath := filepath.Join(tmpDir, module) + // nolint: gosec + cmdBuild := exec.Command("go", "build", "-o", binaryPath, ".") + cmdBuild.Dir = p + buildOutput, err := cmdBuild.CombinedOutput() if err != nil { - return nil, errors.Wrap(err, "failed to read decompressed data") + return nil, fmt.Errorf("go build failed: %w; output: %s", err, buildOutput) } - if err := json.Unmarshal(decompressedData, &untypedTests); err != nil { - return nil, errors.Wrap(err, "failed to unmarshal JSON") + // Step 4: Execute the built binary. + // It is assumed that running the binary generates tests.json in tmpDir. + // nolint: gosec + cmdRun := exec.Command(binaryPath) + cmdRun.Dir = tmpDir + _, err = cmdRun.CombinedOutput() + if err != nil { + return nil, fmt.Errorf("failed to run binary: %w", err) } - return decompressedData, nil -} -func GetSpecTestJSON(path string, module string) ([]byte, error) { - p, err := GetSpecDir(path, module) + // Step 5: Read the tests.json file generated by the binary. + testJSONPath := filepath.Join(tmpDir, "tests.json") + // nolint: gosec + jsonBytes, err := os.ReadFile(testJSONPath) if err != nil { - return nil, fmt.Errorf("could not get spec test dir: %w", err) + return nil, fmt.Errorf("failed to read tests.json: %w", err) } - return os.ReadFile(filepath.Join(filepath.Clean(p), filepath.Clean(specTestPath))) + + return jsonBytes, nil } // GetSpecDir returns the path to the ssv-spec module. @@ -254,57 +254,3 @@ func getGoModFile(path string) (*modfile.File, error) { // parse go.mod return modfile.Parse("go.mod", buf, nil) } - -func ExtractTarGz(gzipStream io.Reader) { - uncompressedStream, err := gzip.NewReader(gzipStream) - if err != nil { - log.Fatal("ExtractTarGz: NewReader failed") - } - - tarReader := tar.NewReader(uncompressedStream) - - for { - header, err := tarReader.Next() - - if err == io.EOF { - break - } - - if err != nil { - log.Fatalf("ExtractTarGz: Next() failed: %s", err.Error()) - } - - switch header.Typeflag { - case tar.TypeDir: - if err := os.Mkdir(header.Name, 0750); err != nil { - log.Fatalf("ExtractTarGz: Mkdir() failed: %s", err.Error()) - } - case tar.TypeReg: - outFile, err := os.Create(header.Name) - if err != nil { - log.Fatalf("ExtractTarGz: Create() failed: %s", err.Error()) - } - // Set a maximum size limit for the decompressed data - maxSize := int64(50 * 1024 * 1024) // 50 MB, adjust as needed - - // Wrap the tarReader with a LimitedReader - limitedReader := &io.LimitedReader{R: tarReader, N: maxSize} - - // Perform the copy operation with the limited reader - if _, err := io.Copy(outFile, limitedReader); err != nil { - log.Fatalf("ExtractTarGz: Copy() failed: %s", err.Error()) - } - err = outFile.Close() - if err != nil { - log.Fatalf("faild to close file: %s", err.Error()) - } - - default: - log.Fatalf( - "ExtractTarGz: uknown type: %b in %s", - header.Typeflag, - header.Name) - } - - } -} diff --git a/scripts/spec-alignment/differ.config.yaml b/scripts/spec-alignment/differ.config.yaml index 74a314c5f2..7916471605 100644 --- a/scripts/spec-alignment/differ.config.yaml +++ b/scripts/spec-alignment/differ.config.yaml @@ -1,4 +1,4 @@ -ApprovedChanges: ["50e5bb7eda99594e", "870a3a66aeccd737","4e22a08543b079b","56ceb03cd44ff702","188adfe8914e04c1","2438f9c5b82b69a3","1a716ee3bdb3170","90b166f78390af18","68219b82a1d9d829","c4c4caa5d0938b85","dfe99ce1d27b6cb1","35f5dab1f128d193","9a3973b64d7e8932","f33f07301a770d03","3e9e0dddfad3b302","d4fef6512374c1f5","b49f54cb45787e4b","59b2375130aef5df","f094cd0460432170","8e51881e527dd603","a7d6d58d9fa06379","1d124224ca4d0fe3","39ea06bfd1477d2d","7e2550bab51f22b2","87ebd29bd49fc52f","ef39dd5223e0d080","fe14e7f0503ea188","6146023d4d5708a2","aebb8e4348b6d667","973a2e6704dbf3","fb4cac598a68c592","257c7eb81d6eb245","2a8e94fe037e13fd","5e7eb878de54eec6","960a9c64cd4ec93c","57dfd255520bd849","ec333ff8a708db69","1cc1ff39ad91ee69","5714652b88e2d44f","7a53b3b037c56325","8c02ef1964464c30","19a268910a20da3d","af6e01ed565029f3","318b5169ac4dabb6","372c6b332e8ba699","c0d8a364c0db855a","4287381be4fb1841","b1614afc1da7794f","c214975412f3fd7","8bbf7eba3fa0cf7e","8e4ec8debe331b36","7a671d8fcefc3793","e2b0e9c6454c1c08","6707ecfefa5fec21","d5a7389d730464f1","8dfae3b3223d2de0","a81c092c985de728","968df5082c727ed6","9e53c73ee60b1cc2","9d265e99dd31d4f5","a34619e078d2e42f","17e8cec4f0625d53","e913f373aa88f333","cfc1e05c372d88dc","e5de6901d78b8833","57c1885b43dd8d19","e8a49856a5edd893","22ea21d10a2f861c","954e4fce01631c4e","108b9575f7c1d4bc","1f8d076449068f64","5a7ad98296703f6","159536003eeddac8","8ca8f82e67ddd3dd","16ebe47404323cc1","48bfe5cf1e578b47","dd83182b693a7216","308d21d9830f7047","6dde03147e012b1a","730c3e5e59393b7d","5b44a4b425ecc397","df5debc50ec8babc","92a41554b2910bb8","c36c680554dde59f","447feaa5cdc1a010","fda90c61f44cb149","cdbb4930eced584c","274336ec1127e6c0","2a496f5b3ad542d2","6b395912dde33b0e","cac56ec14994216b","8850900b5d9bcc65","15e7706486c6359e","cc22f28953b787ea","3bad6ae11596a574","8f84422a240d889c","5b265432dfbbaac7","43794bf5953db193","7975821460ebe1e7","173c505e12aabb8f","47ee0d148148a56f","8cc38593ebe049b6","bda3aec7157b095a","248712911696a851","f4d9c910f1dbaef7","1a2146fcad37acb8","b0b146f9bdab64b6","edfd442b4d725fbb","122f053573538a32","d720d714a20833e1", "f9c984e71b685f9b","8c6b4fee5a4c13ce","c0a8d2019a2c30d5", "717bef26105c733f","2f70630c27062353","2f70337ba7566a69","dd607a44e1341e6b","5210501625ac3de5","f786bf475b5085aa","18a66ed6e613d9c1","e8943e7741f6843d","276a489bd5a00032","ba3bba59f10bf6b","3c50ce0c8089d871","89ee72f6c610ab84","c92b95a85da2cb11","927ea6aed3f98f20","9338904026a0ce37","9683cfa19dc544a3","4d3fa2b8dfcb5f5b", "f19e9a2b295bcfb3", "b10199b2de6f03b8", "1afc17e358f9ca79","4b58762c0b433442","d293ec1bc61bb707","3e88c3b49d093605","4890ff80c88cc41d","5227ff3a225dd20d","81a60407a3a0ba80","db2ad807eb66254a","d308bd7c553ccdcf","bdaf172971637cbe","6ade9202843071fe","2fe8e14083997744","19c9a5362d1e1d3a","5956f803d239f178","92c55a4548a8b760","9a95524213bccfff","2f51a7338b86c229","e96966a281d74505","3ee479b9cbbc3a1d","82b392ba39c6c594","b9d2404e5c570019","24f528d85fb021f2","fe9609a785305d81","b0934079dcd986cc","a9c520a19b26049","d19a9403fd732d94","74a928f5dcb2fdd9","cbbfdb5e68cdac80","10e39d2ceda91f34","f99a004cf6697875","8fa5e8ebf7d223ec","6c80c145ba705243","fbabbc90d0b4178a","ab0c7f24e551ca6","af38a11cb8682c75","b110cba51df9f8d2","c4ff2ed3d20dc419","9295a5bb10efcec7","ab56ea44a75f898a","ff51ef26ab53ba58","df3771e2589008f9","106e5689655bcfc6","f90e0fb6883bff93","667656095cec39ee","9a5597af260c748a","9168b9cfb0cb98c8","875393173d59b0f2","a347ee50e92f7334","91073f39440ab37f","1bbcbe4e8194370d","2169fe3af9e88ab9","a85dccf18844fc79","2bf91d3c3920c5c8","f4ce01b385c68a2","4366899a2cb05197","6f0aecc3342b13c0","874b67f800dd74d6","587785bbbcdc2016","d7397265deb360a6","d9fabd130a8ecdfb","707b55fec1513c90","e51b0346b0d4b612","417e005d18c1f7d4","306964e5ede31618","8c7ca4bb2b8abe1e","b878d79c5774d12d","15599c2b2bd60293","a7891090c37daba1","4192f3dd9bdddea8","ba5b31e5a1adb8e2","dadf83703db9234","3fd72034ab5b908","e939b25394581c4","67d24278154582d6","bc6a77eea2a1ba4b","41fa2c6b8e3aed38","1b6e3c30093fcd6e","98377d7e6f0eea0a","267886c6b07733d7","280250b5f6148515","1b79a8c6288d49d8","47c23de3a5c71c7e","de0f0cd6b40ee150","e91f5b3d583f23af","2036a5ff6aed3717","5ed363e465b9c98b","3ed6b5d40fb4a3b0","e7219eac943d306f","186382b7e69faced","943be3ce709a99d3","3a8800d72c312f75","b9bf74de5674c15d","cb9ad84a103b6499","acdcac18a6e34419", "ee323f91ee6f543c","be9935e5c2bedaef","60ac8eb415e6c748","119e95426dc4affb"] +ApprovedChanges: ["115ccce93a478b9a", "c8f122c9fb83793e", "220aede0f8fa5a4a", "fb9cab4ffb28869a", "3ca2431a8d79bc3a", "ca49cf46baf0a367", "50e5bb7eda99594e", "870a3a66aeccd737","4e22a08543b079b","56ceb03cd44ff702","188adfe8914e04c1","2438f9c5b82b69a3","1a716ee3bdb3170","90b166f78390af18","68219b82a1d9d829","c4c4caa5d0938b85","dfe99ce1d27b6cb1","35f5dab1f128d193","9a3973b64d7e8932","f33f07301a770d03","3e9e0dddfad3b302","d4fef6512374c1f5","b49f54cb45787e4b","59b2375130aef5df","f094cd0460432170","8e51881e527dd603","a7d6d58d9fa06379","1d124224ca4d0fe3","39ea06bfd1477d2d","7e2550bab51f22b2","87ebd29bd49fc52f","ef39dd5223e0d080","fe14e7f0503ea188","6146023d4d5708a2","aebb8e4348b6d667","973a2e6704dbf3","fb4cac598a68c592","257c7eb81d6eb245","2a8e94fe037e13fd","5e7eb878de54eec6","960a9c64cd4ec93c","57dfd255520bd849","ec333ff8a708db69","1cc1ff39ad91ee69","5714652b88e2d44f","7a53b3b037c56325","8c02ef1964464c30","19a268910a20da3d","af6e01ed565029f3","318b5169ac4dabb6","372c6b332e8ba699","c0d8a364c0db855a","4287381be4fb1841","b1614afc1da7794f","c214975412f3fd7","8bbf7eba3fa0cf7e","8e4ec8debe331b36","7a671d8fcefc3793","e2b0e9c6454c1c08","6707ecfefa5fec21","d5a7389d730464f1","8dfae3b3223d2de0","a81c092c985de728","968df5082c727ed6","9e53c73ee60b1cc2","9d265e99dd31d4f5","a34619e078d2e42f","17e8cec4f0625d53","e913f373aa88f333","cfc1e05c372d88dc","e5de6901d78b8833","57c1885b43dd8d19","e8a49856a5edd893","22ea21d10a2f861c","954e4fce01631c4e","108b9575f7c1d4bc","1f8d076449068f64","5a7ad98296703f6","159536003eeddac8","8ca8f82e67ddd3dd","16ebe47404323cc1","48bfe5cf1e578b47","dd83182b693a7216","308d21d9830f7047","6dde03147e012b1a","730c3e5e59393b7d","5b44a4b425ecc397","df5debc50ec8babc","92a41554b2910bb8","c36c680554dde59f","447feaa5cdc1a010","fda90c61f44cb149","cdbb4930eced584c","274336ec1127e6c0","2a496f5b3ad542d2","6b395912dde33b0e","cac56ec14994216b","8850900b5d9bcc65","15e7706486c6359e","cc22f28953b787ea","3bad6ae11596a574","8f84422a240d889c","5b265432dfbbaac7","43794bf5953db193","7975821460ebe1e7","173c505e12aabb8f","47ee0d148148a56f","8cc38593ebe049b6","bda3aec7157b095a","248712911696a851","f4d9c910f1dbaef7","1a2146fcad37acb8","b0b146f9bdab64b6","edfd442b4d725fbb","122f053573538a32","d720d714a20833e1", "f9c984e71b685f9b","8c6b4fee5a4c13ce","c0a8d2019a2c30d5", "717bef26105c733f","2f70630c27062353","2f70337ba7566a69","dd607a44e1341e6b","5210501625ac3de5","f786bf475b5085aa","18a66ed6e613d9c1","e8943e7741f6843d","276a489bd5a00032","ba3bba59f10bf6b","3c50ce0c8089d871","89ee72f6c610ab84","c92b95a85da2cb11","927ea6aed3f98f20","9338904026a0ce37","9683cfa19dc544a3","4d3fa2b8dfcb5f5b", "f19e9a2b295bcfb3", "b10199b2de6f03b8", "1afc17e358f9ca79","4b58762c0b433442","d293ec1bc61bb707","3e88c3b49d093605","4890ff80c88cc41d","5227ff3a225dd20d","81a60407a3a0ba80","db2ad807eb66254a","d308bd7c553ccdcf","bdaf172971637cbe","6ade9202843071fe","2fe8e14083997744","19c9a5362d1e1d3a","5956f803d239f178","92c55a4548a8b760","9a95524213bccfff","2f51a7338b86c229","e96966a281d74505","3ee479b9cbbc3a1d","82b392ba39c6c594","b9d2404e5c570019","24f528d85fb021f2","fe9609a785305d81","b0934079dcd986cc","a9c520a19b26049","d19a9403fd732d94","74a928f5dcb2fdd9","cbbfdb5e68cdac80","10e39d2ceda91f34","f99a004cf6697875","8fa5e8ebf7d223ec","6c80c145ba705243","fbabbc90d0b4178a","ab0c7f24e551ca6","af38a11cb8682c75","b110cba51df9f8d2","c4ff2ed3d20dc419","9295a5bb10efcec7","ab56ea44a75f898a","ff51ef26ab53ba58","df3771e2589008f9","106e5689655bcfc6","f90e0fb6883bff93","667656095cec39ee","9a5597af260c748a","9168b9cfb0cb98c8","875393173d59b0f2","a347ee50e92f7334","91073f39440ab37f","1bbcbe4e8194370d","2169fe3af9e88ab9","a85dccf18844fc79","2bf91d3c3920c5c8","f4ce01b385c68a2","4366899a2cb05197","6f0aecc3342b13c0","874b67f800dd74d6","587785bbbcdc2016","d7397265deb360a6","d9fabd130a8ecdfb","707b55fec1513c90","e51b0346b0d4b612","417e005d18c1f7d4","306964e5ede31618","8c7ca4bb2b8abe1e","b878d79c5774d12d","15599c2b2bd60293","a7891090c37daba1","4192f3dd9bdddea8","ba5b31e5a1adb8e2","dadf83703db9234","3fd72034ab5b908","e939b25394581c4","67d24278154582d6","bc6a77eea2a1ba4b","41fa2c6b8e3aed38","1b6e3c30093fcd6e","98377d7e6f0eea0a","267886c6b07733d7","280250b5f6148515","1b79a8c6288d49d8","47c23de3a5c71c7e","de0f0cd6b40ee150","e91f5b3d583f23af","2036a5ff6aed3717","5ed363e465b9c98b","3ed6b5d40fb4a3b0","e7219eac943d306f","186382b7e69faced","943be3ce709a99d3","3a8800d72c312f75","b9bf74de5674c15d","cb9ad84a103b6499","acdcac18a6e34419", "ee323f91ee6f543c","be9935e5c2bedaef","60ac8eb415e6c748","119e95426dc4affb"] IgnoredIdentifiers: - logger From f7bc9c6cb4b335aef0d4bb40f3ac66fa2b9911df Mon Sep 17 00:00:00 2001 From: moshe-blox <89339422+moshe-blox@users.noreply.github.com> Date: Wed, 19 Feb 2025 23:26:27 +0200 Subject: [PATCH 5/6] fix: conditional handling of Electra fork epoch (#2039) * fix: conditional handling of Electra fork epoch * feat: add logging for retrieved fork epochs in GoClient * refactor: remove unused zap import in GoClient data version * feat: log current data version when retrieving fork epochs in GoClient * fix: change current data version logging to use uint64 type in GoClient --- beacon/goclient/dataversion.go | 42 +++++++++---------------- beacon/goclient/dataversion_test.go | 48 ++++++++++++++--------------- beacon/goclient/goclient.go | 9 ++++++ 3 files changed, 48 insertions(+), 51 deletions(-) diff --git a/beacon/goclient/dataversion.go b/beacon/goclient/dataversion.go index b5cca4abf4..378e74456f 100644 --- a/beacon/goclient/dataversion.go +++ b/beacon/goclient/dataversion.go @@ -46,54 +46,42 @@ func (gc *GoClient) checkForkValues(specResponse *api.Response[map[string]any]) // and compares it with the current stored value. // If the candidate is greater than the current value, that's an error. // Otherwise, it returns the lower value (or the candidate if the current value is zero). - processFork := func(forkName, key string, current phase0.Epoch) (phase0.Epoch, error) { + processFork := func(forkName, key string, current phase0.Epoch, required bool) (phase0.Epoch, error) { raw, ok := specResponse.Data[key] if !ok { - return 0, fmt.Errorf("%s fork epoch not known by chain", forkName) + if required { + return 0, fmt.Errorf("%s fork epoch not known by chain", forkName) + } + return FarFutureEpoch, nil } forkVal, ok := raw.(uint64) if !ok { return 0, fmt.Errorf("failed to decode %s fork epoch", forkName) } - if current != FarFutureEpoch && forkVal == uint64(FarFutureEpoch) { - return 0, fmt.Errorf("failed to decode %s fork epoch", forkName) + if current != FarFutureEpoch && current != phase0.Epoch(forkVal) { + // Reject if candidate is missing the fork epoch that we've already seen. + return 0, fmt.Errorf("new %s fork epoch (%d) doesn't match current value (%d)", forkName, phase0.Epoch(forkVal), current) } return phase0.Epoch(forkVal), nil } var err error // Process required forks. - if newAltair, err = processFork("ALTAIR", "ALTAIR_FORK_EPOCH", gc.ForkEpochAltair); err != nil { + if newAltair, err = processFork("ALTAIR", "ALTAIR_FORK_EPOCH", gc.ForkEpochAltair, true); err != nil { return err } - if newBellatrix, err = processFork("BELLATRIX", "BELLATRIX_FORK_EPOCH", gc.ForkEpochBellatrix); err != nil { + if newBellatrix, err = processFork("BELLATRIX", "BELLATRIX_FORK_EPOCH", gc.ForkEpochBellatrix, true); err != nil { return err } - if newCapella, err = processFork("CAPELLA", "CAPELLA_FORK_EPOCH", gc.ForkEpochCapella); err != nil { + if newCapella, err = processFork("CAPELLA", "CAPELLA_FORK_EPOCH", gc.ForkEpochCapella, true); err != nil { return err } - if newDeneb, err = processFork("DENEB", "DENEB_FORK_EPOCH", gc.ForkEpochDeneb); err != nil { + if newDeneb, err = processFork("DENEB", "DENEB_FORK_EPOCH", gc.ForkEpochDeneb, true); err != nil { return err } - - // Process the optional ELECTRA fork. - // If the key exists, perform the same validation; otherwise, keep the current value. - if raw, ok := specResponse.Data["ELECTRA_FORK_EPOCH"]; ok { - forkVal, ok := raw.(uint64) - if !ok { - return fmt.Errorf("failed to decode ELECTRA fork epoch") - } - candidate := phase0.Epoch(forkVal) - if gc.ForkEpochElectra != 0 && candidate > gc.ForkEpochElectra { - return fmt.Errorf("new ELECTRA fork epoch (%d) is greater than current (%d)", candidate, gc.ForkEpochElectra) - } - if gc.ForkEpochElectra == 0 || candidate < gc.ForkEpochElectra { - newElectra = candidate - } else { - newElectra = gc.ForkEpochElectra - } - } else { - newElectra = FarFutureEpoch + alreadySeenElectra := gc.ForkEpochElectra != FarFutureEpoch + if newElectra, err = processFork("ELECTRA", "ELECTRA_FORK_EPOCH", gc.ForkEpochElectra, alreadySeenElectra); err != nil { + return err } // At this point, no error was encountered. diff --git a/beacon/goclient/dataversion_test.go b/beacon/goclient/dataversion_test.go index a82cafe867..25e664a6ae 100644 --- a/beacon/goclient/dataversion_test.go +++ b/beacon/goclient/dataversion_test.go @@ -1,7 +1,6 @@ package goclient import ( - "math" "strings" "testing" @@ -74,11 +73,11 @@ func TestCheckForkValues(t *testing.T) { }, { name: "missing ALTAIR", - initialAltair: math.MaxUint64, - initialBellatrix: math.MaxUint64, - initialCapella: math.MaxUint64, - initialDeneb: math.MaxUint64, - initialElectra: math.MaxUint64, + initialAltair: FarFutureEpoch, + initialBellatrix: FarFutureEpoch, + initialCapella: FarFutureEpoch, + initialDeneb: FarFutureEpoch, + initialElectra: FarFutureEpoch, response: &api.Response[map[string]any]{ Data: map[string]any{ "BELLATRIX_FORK_EPOCH": uint64(20), @@ -90,11 +89,11 @@ func TestCheckForkValues(t *testing.T) { }, { name: "invalid type for ALTAIR", - initialAltair: math.MaxUint64, - initialBellatrix: math.MaxUint64, - initialCapella: math.MaxUint64, - initialDeneb: math.MaxUint64, - initialElectra: math.MaxUint64, + initialAltair: FarFutureEpoch, + initialBellatrix: FarFutureEpoch, + initialCapella: FarFutureEpoch, + initialDeneb: FarFutureEpoch, + initialElectra: FarFutureEpoch, response: &api.Response[map[string]any]{ Data: map[string]any{ "ALTAIR_FORK_EPOCH": "not a uint", @@ -107,11 +106,11 @@ func TestCheckForkValues(t *testing.T) { }, { name: "valid update with initial zeros and electra provided", - initialAltair: math.MaxUint64, - initialBellatrix: math.MaxUint64, - initialCapella: math.MaxUint64, - initialDeneb: math.MaxUint64, - initialElectra: math.MaxUint64, + initialAltair: FarFutureEpoch, + initialBellatrix: FarFutureEpoch, + initialCapella: FarFutureEpoch, + initialDeneb: FarFutureEpoch, + initialElectra: FarFutureEpoch, response: &api.Response[map[string]any]{ Data: map[string]any{ "ALTAIR_FORK_EPOCH": uint64(10), @@ -129,11 +128,11 @@ func TestCheckForkValues(t *testing.T) { }, { name: "optional ELECTRA not provided, remains unchanged", - initialAltair: math.MaxUint64, - initialBellatrix: math.MaxUint64, - initialCapella: math.MaxUint64, - initialDeneb: math.MaxUint64, - initialElectra: math.MaxUint64, + initialAltair: FarFutureEpoch, + initialBellatrix: FarFutureEpoch, + initialCapella: FarFutureEpoch, + initialDeneb: FarFutureEpoch, + initialElectra: FarFutureEpoch, response: &api.Response[map[string]any]{ Data: map[string]any{ "ALTAIR_FORK_EPOCH": uint64(10), @@ -146,10 +145,10 @@ func TestCheckForkValues(t *testing.T) { expectedBellatrix: phase0.Epoch(20), expectedCapella: phase0.Epoch(30), expectedDeneb: phase0.Epoch(40), - expectedElectra: phase0.Epoch(math.MaxUint64), + expectedElectra: phase0.Epoch(FarFutureEpoch), }, { - name: "optional ELECTRA provided and updates", + name: "optional ELECTRA provided and can't change", initialAltair: 10, initialBellatrix: 20, initialCapella: 30, @@ -169,6 +168,7 @@ func TestCheckForkValues(t *testing.T) { expectedCapella: phase0.Epoch(30), expectedDeneb: phase0.Epoch(40), expectedElectra: phase0.Epoch(50), + expectedErr: "new ELECTRA fork epoch (50) doesn't match current value (99)", }, { name: "optional ELECTRA provided, candidate greater than current", @@ -186,7 +186,7 @@ func TestCheckForkValues(t *testing.T) { "ELECTRA_FORK_EPOCH": uint64(60), }, }, - expectedErr: "new ELECTRA fork epoch (60) is greater than current (50)", + expectedErr: "new ELECTRA fork epoch (60) doesn't match current value (50)", }, } diff --git a/beacon/goclient/goclient.go b/beacon/goclient/goclient.go index e9b6c25f8e..99b829f0aa 100644 --- a/beacon/goclient/goclient.go +++ b/beacon/goclient/goclient.go @@ -324,6 +324,15 @@ func (gc *GoClient) singleClientHooks() *eth2clienthttp.Hooks { ) return } + gc.log.Info("retrieved fork epochs", + zap.String("node_addr", s.Address()), + zap.Uint64("current_data_version", uint64(gc.DataVersion(gc.network.EstimatedCurrentEpoch()))), + zap.Uint64("altair", uint64(gc.ForkEpochAltair)), + zap.Uint64("bellatrix", uint64(gc.ForkEpochBellatrix)), + zap.Uint64("capella", uint64(gc.ForkEpochCapella)), + zap.Uint64("deneb", uint64(gc.ForkEpochDeneb)), + zap.Uint64("electra", uint64(gc.ForkEpochElectra)), + ) }, OnInactive: func(ctx context.Context, s *eth2clienthttp.Service) { gc.log.Warn("consensus client disconnected", From 83e58c0d3f40d4430535c499c9d014067c0ff263 Mon Sep 17 00:00:00 2001 From: moshe-blox <89339422+moshe-blox@users.noreply.github.com> Date: Wed, 19 Feb 2025 23:50:00 +0200 Subject: [PATCH 6/6] fix: update expectedElectra value in TestCheckForkValues (#2040) --- beacon/goclient/dataversion_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beacon/goclient/dataversion_test.go b/beacon/goclient/dataversion_test.go index 25e664a6ae..2d0ca39255 100644 --- a/beacon/goclient/dataversion_test.go +++ b/beacon/goclient/dataversion_test.go @@ -145,7 +145,7 @@ func TestCheckForkValues(t *testing.T) { expectedBellatrix: phase0.Epoch(20), expectedCapella: phase0.Epoch(30), expectedDeneb: phase0.Epoch(40), - expectedElectra: phase0.Epoch(FarFutureEpoch), + expectedElectra: FarFutureEpoch, }, { name: "optional ELECTRA provided and can't change",