diff --git a/cl/beacon/handler/handler.go b/cl/beacon/handler/handler.go index 9e3a75d400c..b543908e1ae 100644 --- a/cl/beacon/handler/handler.go +++ b/cl/beacon/handler/handler.go @@ -83,6 +83,7 @@ type ApiHandler struct { syncContributionAndProofsService services.SyncContributionService aggregateAndProofsService services.AggregateAndProofService voluntaryExitService services.VoluntaryExitService + blsToExecutionChangeService services.BLSToExecutionChangeService } func NewApiHandler( @@ -112,6 +113,8 @@ func NewApiHandler( syncContributionAndProofs services.SyncContributionService, aggregateAndProofs services.AggregateAndProofService, voluntaryExitService services.VoluntaryExitService, + blsToExecutionChangeService services.BLSToExecutionChangeService, + ) *ApiHandler { blobBundles, err := lru.New[common.Bytes48, BlobBundle]("blobs", maxBlobBundleCacheSize) if err != nil { @@ -149,6 +152,7 @@ func NewApiHandler( syncContributionAndProofsService: syncContributionAndProofs, aggregateAndProofsService: aggregateAndProofs, voluntaryExitService: voluntaryExitService, + blsToExecutionChangeService: blsToExecutionChangeService, } } diff --git a/cl/beacon/handler/pool.go b/cl/beacon/handler/pool.go index 3d1ce491c58..3bc66779600 100644 --- a/cl/beacon/handler/pool.go +++ b/cl/beacon/handler/pool.go @@ -226,7 +226,7 @@ func (a *ApiHandler) PostEthV1BeaconPoolBlsToExecutionChanges(w http.ResponseWri } failures := []poolingFailure{} for _, v := range req { - if err := a.forkchoiceStore.OnBlsToExecutionChange(v, false); err != nil { + if err := a.blsToExecutionChangeService.ProcessMessage(r.Context(), nil, v); err != nil && !errors.Is(err, services.ErrIgnore) { failures = append(failures, poolingFailure{Index: len(failures), Message: err.Error()}) continue } diff --git a/cl/beacon/handler/utils_test.go b/cl/beacon/handler/utils_test.go index 61f63bd19f9..64c6684c629 100644 --- a/cl/beacon/handler/utils_test.go +++ b/cl/beacon/handler/utils_test.go @@ -89,6 +89,8 @@ func setupTestingHandler(t *testing.T, v clparams.StateVersion, logger log.Logge syncContributionService := mock_services.NewMockSyncContributionService(ctrl) aggregateAndProofsService := mock_services.NewMockAggregateAndProofService(ctrl) voluntaryExitService := mock_services.NewMockVoluntaryExitService(ctrl) + blsToExecutionChangeService := mock_services.NewMockBLSToExecutionChangeService(ctrl) + // ctx context.Context, subnetID *uint64, msg *cltypes.SyncCommitteeMessage) error syncCommitteeMessagesService.EXPECT().ProcessMessage(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, subnetID *uint64, msg *cltypes.SyncCommitteeMessage) error { return h.syncMessagePool.AddSyncCommitteeMessage(postState, *subnetID, msg) @@ -105,6 +107,10 @@ func setupTestingHandler(t *testing.T, v clparams.StateVersion, logger log.Logge opPool.VoluntaryExistsPool.Insert(msg.VoluntaryExit.ValidatorIndex, msg) return nil }).AnyTimes() + blsToExecutionChangeService.EXPECT().ProcessMessage(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, subnetID *uint64, msg *cltypes.SignedBLSToExecutionChange) error { + opPool.BLSToExecutionChangesPool.Insert(msg.Signature, msg) + return nil + }).AnyTimes() vp = validator_params.NewValidatorParams() h = NewApiHandler( @@ -128,7 +134,13 @@ func setupTestingHandler(t *testing.T, v clparams.StateVersion, logger log.Logge Events: true, Validator: true, Lighthouse: true, - }, nil, blobStorage, nil, vp, nil, nil, fcu.SyncContributionPool, nil, nil, syncCommitteeMessagesService, syncContributionService, aggregateAndProofsService, voluntaryExitService) // TODO: add tests + }, nil, blobStorage, nil, vp, nil, nil, fcu.SyncContributionPool, nil, nil, + syncCommitteeMessagesService, + syncContributionService, + aggregateAndProofsService, + voluntaryExitService, + blsToExecutionChangeService, + ) // TODO: add tests h.Init() return } diff --git a/cl/beacon/handler/validator_test.go b/cl/beacon/handler/validator_test.go index a9c111e9236..6aa4c28c9ce 100644 --- a/cl/beacon/handler/validator_test.go +++ b/cl/beacon/handler/validator_test.go @@ -54,6 +54,7 @@ func (t *validatorTestSuite) SetupTest() { nil, nil, nil, + nil, ) t.gomockCtrl = gomockCtrl } diff --git a/cl/phase1/forkchoice/fork_choice_test.go b/cl/phase1/forkchoice/fork_choice_test.go index c0fd0ced9fe..e89cd423f5d 100644 --- a/cl/phase1/forkchoice/fork_choice_test.go +++ b/cl/phase1/forkchoice/fork_choice_test.go @@ -107,12 +107,6 @@ func TestForkChoiceBasic(t *testing.T) { for sd.HeadState() == nil { time.Sleep(time.Millisecond) } - // Try processing a bls execution change exit - err = store.OnBlsToExecutionChange(&cltypes.SignedBLSToExecutionChange{ - Message: &cltypes.BLSToExecutionChange{ - ValidatorIndex: 0, - }, - }, true) require.NoError(t, err) require.Equal(t, len(pool.VoluntaryExistsPool.Raw()), 1) } diff --git a/cl/phase1/forkchoice/interface.go b/cl/phase1/forkchoice/interface.go index 0331ac43eb8..f9c84307f28 100644 --- a/cl/phase1/forkchoice/interface.go +++ b/cl/phase1/forkchoice/interface.go @@ -61,7 +61,6 @@ type ForkChoiceStorageWriter interface { OnAttestation(attestation *solid.Attestation, fromBlock, insert bool) error OnAttesterSlashing(attesterSlashing *cltypes.AttesterSlashing, test bool) error OnProposerSlashing(proposerSlashing *cltypes.ProposerSlashing, test bool) error - OnBlsToExecutionChange(signedChange *cltypes.SignedBLSToExecutionChange, test bool) error OnBlock(ctx context.Context, block *cltypes.SignedBeaconBlock, newPayload bool, fullValidation bool, checkDataAvaibility bool) error AddPreverifiedBlobSidecar(blobSidecar *cltypes.BlobSidecar) error OnTick(time uint64) diff --git a/cl/phase1/forkchoice/on_operations.go b/cl/phase1/forkchoice/on_operations.go index cb2724ae4fc..8c096516dd7 100644 --- a/cl/phase1/forkchoice/on_operations.go +++ b/cl/phase1/forkchoice/on_operations.go @@ -1,7 +1,6 @@ package forkchoice import ( - "bytes" "fmt" "github.com/Giulio2002/bls" @@ -9,7 +8,6 @@ import ( "github.com/ledgerwatch/erigon/cl/fork" "github.com/ledgerwatch/erigon/cl/phase1/core/state" "github.com/ledgerwatch/erigon/cl/pool" - "github.com/ledgerwatch/erigon/cl/utils" ) // NOTE: This file implements non-official handlers for other types of iterations. what it does is,using the forkchoices @@ -90,58 +88,3 @@ func (f *ForkChoiceStore) OnProposerSlashing(proposerSlashing *cltypes.ProposerS return nil } - -func (f *ForkChoiceStore) OnBlsToExecutionChange(signedChange *cltypes.SignedBLSToExecutionChange, test bool) error { - if f.operationsPool.BLSToExecutionChangesPool.Has(signedChange.Signature) { - f.emitters.Publish("bls_to_execution_change", signedChange) - return nil - } - change := signedChange.Message - - // Take lock as we interact with state. - s := f.syncedDataManager.HeadState() - if s == nil { - return nil - } - validator, err := s.ValidatorForValidatorIndex(int(change.ValidatorIndex)) - if err != nil { - return fmt.Errorf("unable to retrieve state: %v", err) - } - wc := validator.WithdrawalCredentials() - - if wc[0] != byte(f.beaconCfg.BLSWithdrawalPrefixByte) { - return fmt.Errorf("invalid withdrawal credentials prefix") - } - genesisValidatorRoot := s.GenesisValidatorsRoot() - // Perform full validation if requested. - if !test { - // Check the validator's withdrawal credentials against the provided message. - hashedFrom := utils.Sha256(change.From[:]) - if !bytes.Equal(hashedFrom[1:], wc[1:]) { - return fmt.Errorf("invalid withdrawal credentials") - } - - // Compute the signing domain and verify the message signature. - domain, err := fork.ComputeDomain(f.beaconCfg.DomainBLSToExecutionChange[:], utils.Uint32ToBytes4(uint32(f.beaconCfg.GenesisForkVersion)), genesisValidatorRoot) - if err != nil { - return err - } - signedRoot, err := fork.ComputeSigningRoot(change, domain) - if err != nil { - return err - } - valid, err := bls.Verify(signedChange.Signature[:], signedRoot[:], change.From[:]) - if err != nil { - return err - } - if !valid { - return fmt.Errorf("invalid signature") - } - } - - f.operationsPool.BLSToExecutionChangesPool.Insert(signedChange.Signature, signedChange) - - // emit bls_to_execution_change - f.emitters.Publish("bls_to_execution_change", signedChange) - return nil -} diff --git a/cl/phase1/network/gossip_manager.go b/cl/phase1/network/gossip_manager.go index 1bbb6f7cb18..c6354e4c03e 100644 --- a/cl/phase1/network/gossip_manager.go +++ b/cl/phase1/network/gossip_manager.go @@ -44,6 +44,7 @@ type GossipManager struct { aggregateAndProofService services.AggregateAndProofService attestationService services.AttestationService voluntaryExitService services.VoluntaryExitService + blsToExecutionChangeService services.BLSToExecutionChangeService } func NewGossipReceiver( @@ -60,6 +61,7 @@ func NewGossipReceiver( aggregateAndProofService services.AggregateAndProofService, attestationService services.AttestationService, voluntaryExitService services.VoluntaryExitService, + blsToExecutionChangeService services.BLSToExecutionChangeService, ) *GossipManager { return &GossipManager{ sentinel: s, @@ -75,6 +77,7 @@ func NewGossipReceiver( aggregateAndProofService: aggregateAndProofService, attestationService: attestationService, voluntaryExitService: voluntaryExitService, + blsToExecutionChangeService: blsToExecutionChangeService, } } @@ -161,7 +164,11 @@ func (g *GossipManager) routeAndProcess(ctx context.Context, data *sentinel.Goss case gossip.TopicNameAttesterSlashing: return operationsContract[*cltypes.AttesterSlashing](ctx, g, data, int(version), "attester slashing", g.forkChoice.OnAttesterSlashing) case gossip.TopicNameBlsToExecutionChange: - return operationsContract[*cltypes.SignedBLSToExecutionChange](ctx, g, data, int(version), "bls to execution change", g.forkChoice.OnBlsToExecutionChange) + obj := &cltypes.SignedBLSToExecutionChange{} + if err := obj.DecodeSSZ(data.Data, int(version)); err != nil { + return err + } + return g.blsToExecutionChangeService.ProcessMessage(ctx, data.SubnetId, obj) case gossip.TopicNameBeaconAggregateAndProof: obj := &cltypes.SignedAggregateAndProof{} if err := obj.DecodeSSZ(data.Data, int(version)); err != nil { diff --git a/cl/phase1/network/services/BlsToExecutionChange.go b/cl/phase1/network/services/BlsToExecutionChange.go new file mode 100644 index 00000000000..dfe55ff9409 --- /dev/null +++ b/cl/phase1/network/services/BlsToExecutionChange.go @@ -0,0 +1,99 @@ +package services + +import ( + "bytes" + "context" + "fmt" + + "github.com/Giulio2002/bls" + "github.com/ledgerwatch/erigon/cl/beacon/beaconevents" + "github.com/ledgerwatch/erigon/cl/beacon/synced_data" + "github.com/ledgerwatch/erigon/cl/clparams" + "github.com/ledgerwatch/erigon/cl/cltypes" + "github.com/ledgerwatch/erigon/cl/fork" + "github.com/ledgerwatch/erigon/cl/pool" + "github.com/ledgerwatch/erigon/cl/utils" +) + +type blsToExecutionChangeService struct { + operationsPool pool.OperationsPool + emitters *beaconevents.Emitters + syncedDataManager *synced_data.SyncedDataManager + beaconCfg *clparams.BeaconChainConfig +} + +func NewBLSToExecutionChangeService( + operationsPool pool.OperationsPool, + emitters *beaconevents.Emitters, + syncedDataManager *synced_data.SyncedDataManager, + beaconCfg *clparams.BeaconChainConfig, +) BLSToExecutionChangeService { + return &blsToExecutionChangeService{ + operationsPool: operationsPool, + emitters: emitters, + syncedDataManager: syncedDataManager, + beaconCfg: beaconCfg, + } +} + +func (s *blsToExecutionChangeService) ProcessMessage(ctx context.Context, subnet *uint64, msg *cltypes.SignedBLSToExecutionChange) error { + defer s.emitters.Publish("bls_to_execution_change", msg) + + // [IGNORE] The signed_bls_to_execution_change is the first valid signed bls to execution change received + // for the validator with index signed_bls_to_execution_change.message.validator_index. + if s.operationsPool.BLSToExecutionChangesPool.Has(msg.Signature) { + return nil + } + change := msg.Message + + state := s.syncedDataManager.HeadState() + if state == nil { + return nil + } + + // [IGNORE] current_epoch >= CAPELLA_FORK_EPOCH, where current_epoch is defined by the current wall-clock time. + if !(state.Version() >= clparams.CapellaVersion) { + return ErrIgnore + } + + // ref: https://github.com/ethereum/consensus-specs/blob/dev/specs/capella/beacon-chain.md#new-process_bls_to_execution_change + // assert address_change.validator_index < len(state.validators) + validator, err := state.ValidatorForValidatorIndex(int(change.ValidatorIndex)) + if err != nil { + return fmt.Errorf("unable to retrieve state: %v", err) + } + wc := validator.WithdrawalCredentials() + + // assert validator.withdrawal_credentials[:1] == BLS_WITHDRAWAL_PREFIX + if wc[0] != byte(s.beaconCfg.BLSWithdrawalPrefixByte) { + return fmt.Errorf("invalid withdrawal credentials prefix") + } + + // assert validator.withdrawal_credentials[1:] == hash(address_change.from_bls_pubkey)[1:] + // Perform full validation if requested. + // Check the validator's withdrawal credentials against the provided message. + hashedFrom := utils.Sha256(change.From[:]) + if !bytes.Equal(hashedFrom[1:], wc[1:]) { + return fmt.Errorf("invalid withdrawal credentials") + } + + // assert bls.Verify(address_change.from_bls_pubkey, signing_root, signed_address_change.signature) + genesisValidatorRoot := state.GenesisValidatorsRoot() + domain, err := fork.ComputeDomain(s.beaconCfg.DomainBLSToExecutionChange[:], utils.Uint32ToBytes4(uint32(s.beaconCfg.GenesisForkVersion)), genesisValidatorRoot) + if err != nil { + return err + } + signedRoot, err := fork.ComputeSigningRoot(change, domain) + if err != nil { + return err + } + valid, err := bls.Verify(msg.Signature[:], signedRoot[:], change.From[:]) + if err != nil { + return err + } + if !valid { + return fmt.Errorf("invalid signature") + } + s.operationsPool.BLSToExecutionChangesPool.Insert(msg.Signature, msg) + return nil +} diff --git a/cl/phase1/network/services/interface.go b/cl/phase1/network/services/interface.go index cb3673754b0..1593ae40f68 100644 --- a/cl/phase1/network/services/interface.go +++ b/cl/phase1/network/services/interface.go @@ -33,3 +33,6 @@ type AttestationService Service[*solid.Attestation] //go:generate mockgen -destination=./mock_services/voluntary_exit_service_mock.go -package=mock_services . VoluntaryExitService type VoluntaryExitService Service[*cltypes.SignedVoluntaryExit] + +//go:generate mockgen -destination=./mock_services/bls_to_execution_change_service_mock.go -package=mock_services . BLSToExecutionChangeService +type BLSToExecutionChangeService Service[*cltypes.SignedBLSToExecutionChange] diff --git a/cl/phase1/network/services/mock_services/bls_to_execution_change_service_mock.go b/cl/phase1/network/services/mock_services/bls_to_execution_change_service_mock.go new file mode 100644 index 00000000000..3e84bfd082d --- /dev/null +++ b/cl/phase1/network/services/mock_services/bls_to_execution_change_service_mock.go @@ -0,0 +1,55 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/ledgerwatch/erigon/cl/phase1/network/services (interfaces: BLSToExecutionChangeService) +// +// Generated by this command: +// +// mockgen -destination=./mock_services/bls_to_execution_change_service_mock.go -package=mock_services . BLSToExecutionChangeService +// + +// Package mock_services is a generated GoMock package. +package mock_services + +import ( + context "context" + reflect "reflect" + + cltypes "github.com/ledgerwatch/erigon/cl/cltypes" + gomock "go.uber.org/mock/gomock" +) + +// MockBLSToExecutionChangeService is a mock of BLSToExecutionChangeService interface. +type MockBLSToExecutionChangeService struct { + ctrl *gomock.Controller + recorder *MockBLSToExecutionChangeServiceMockRecorder +} + +// MockBLSToExecutionChangeServiceMockRecorder is the mock recorder for MockBLSToExecutionChangeService. +type MockBLSToExecutionChangeServiceMockRecorder struct { + mock *MockBLSToExecutionChangeService +} + +// NewMockBLSToExecutionChangeService creates a new mock instance. +func NewMockBLSToExecutionChangeService(ctrl *gomock.Controller) *MockBLSToExecutionChangeService { + mock := &MockBLSToExecutionChangeService{ctrl: ctrl} + mock.recorder = &MockBLSToExecutionChangeServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockBLSToExecutionChangeService) EXPECT() *MockBLSToExecutionChangeServiceMockRecorder { + return m.recorder +} + +// ProcessMessage mocks base method. +func (m *MockBLSToExecutionChangeService) ProcessMessage(arg0 context.Context, arg1 *uint64, arg2 *cltypes.SignedBLSToExecutionChange) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ProcessMessage", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// ProcessMessage indicates an expected call of ProcessMessage. +func (mr *MockBLSToExecutionChangeServiceMockRecorder) ProcessMessage(arg0, arg1, arg2 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProcessMessage", reflect.TypeOf((*MockBLSToExecutionChangeService)(nil).ProcessMessage), arg0, arg1, arg2) +} diff --git a/cmd/caplin/caplin1/run.go b/cmd/caplin/caplin1/run.go index ffffb568731..d5e642eead5 100644 --- a/cmd/caplin/caplin1/run.go +++ b/cmd/caplin/caplin1/run.go @@ -183,10 +183,11 @@ func RunCaplinPhase1(ctx context.Context, engine execution_client.ExecutionEngin aggregateAndProofService := services.NewAggregateAndProofService(ctx, syncedDataManager, forkChoice, beaconConfig, aggregationPool) attestationService := services.NewAttestationService(forkChoice, committeeSub, ethClock, syncedDataManager, beaconConfig, networkConfig) voluntaryExitService := services.NewVoluntaryExitService(pool, emitters, syncedDataManager, beaconConfig, ethClock) + blsToExecutionChangeService := services.NewBLSToExecutionChangeService(pool, emitters, syncedDataManager, beaconConfig) // Create the gossip manager gossipManager := network.NewGossipReceiver(sentinel, forkChoice, beaconConfig, ethClock, emitters, committeeSub, blockService, blobService, syncCommitteeMessagesService, syncContributionService, aggregateAndProofService, - attestationService, voluntaryExitService) + attestationService, voluntaryExitService, blsToExecutionChangeService) { // start ticking forkChoice go func() { tickInterval := time.NewTicker(2 * time.Millisecond) @@ -294,6 +295,7 @@ func RunCaplinPhase1(ctx context.Context, engine execution_client.ExecutionEngin syncContributionService, aggregateAndProofService, voluntaryExitService, + blsToExecutionChangeService, ) go beacon.ListenAndServe(&beacon.LayeredBeaconHandler{ ArchiveApi: apiHandler,