diff --git a/pkg/builderapi/epbs/handler.go b/pkg/builderapi/epbs/handler.go index 35cdd2a..9367b81 100644 --- a/pkg/builderapi/epbs/handler.go +++ b/pkg/builderapi/epbs/handler.go @@ -105,7 +105,7 @@ type Handler struct { lastBidMu sync.Mutex lastBids map[phase0.Slot]recordedBid // dedupe of repeated identical bid records - builderIndex atomic.Uint64 // builder index used in Gloas bids; set after lifecycle registration + builderIndex atomic.Uint64 // builder index used in Gloas bids; resolved at construction, refreshed on lifecycle registration enabled atomic.Bool bidsRequested atomic.Uint64 // count of getExecutionPayloadBid requests received blocksAccepted atomic.Uint64 // count of accepted signed beacon blocks @@ -118,7 +118,7 @@ type Handler struct { func NewHandler(cfg *config.BuilderAPIConfig, log logrus.FieldLogger, chainSvc chain.Service, planSvc *action_plan.PlanService, payloadCache *payload_builder.PayloadCache, blsSigner *signer.BLSSigner) *Handler { - return &Handler{ + h := &Handler{ cfg: cfg, log: log.WithField("component", "builderapi-epbs"), chainSvc: chainSvc, @@ -129,6 +129,20 @@ func NewHandler(cfg *config.BuilderAPIConfig, log logrus.FieldLogger, chainSvc c prefsStore: NewBuilderPreferencesStore(), lastBids: make(map[phase0.Slot]recordedBid, maxRecordedBidSlots), } + + // Resolve the builder index from chain state up front, independent of + // lifecycle registration. Without this, a builder already registered + // on-chain before this process started would sign every bid with the + // zero-value index until (if ever) the lifecycle callback fires. + // SetBuilderIndex still overrides this on a live registration event. + if chainSvc != nil && blsSigner != nil { + if builderInfo := chainSvc.GetBuilderByPubkey(blsSigner.PublicKey()); builderInfo != nil { + h.builderIndex.Store(builderInfo.Index) + h.log.WithField("builder_index", builderInfo.Index).Info("Resolved builder index from chain state") + } + } + + return h } // SetResultRecorder wires the optional per-slot result recorder. diff --git a/pkg/builderapi/epbs/handler_test.go b/pkg/builderapi/epbs/handler_test.go index f7e3110..26fea2f 100644 --- a/pkg/builderapi/epbs/handler_test.go +++ b/pkg/builderapi/epbs/handler_test.go @@ -301,6 +301,34 @@ func postBeaconBlock(h *Handler, body []byte) *httptest.ResponseRecorder { return rec } +// TestNewHandler_ResolvesBuilderIndexFromChainState guards against a builder +// already registered on-chain before this process started signing every bid +// with the zero-value index. NewHandler must resolve the real index from +// chain state itself, without waiting for a lifecycle registration callback +// that may never fire in this process. +func TestNewHandler_ResolvesBuilderIndexFromChainState(t *testing.T) { + log := logrus.New() + log.SetLevel(logrus.PanicLevel) + + blsSigner, err := signer.NewBLSSigner("0x0000000000000000000000000000000000000000000000000000000000000001") + require.NoError(t, err) + + chainSvc := &stubChainService{ + genesisTime: time.Now().Add(-4 * time.Second), + slotDuration: 4 * time.Second, + currentFork: version.DataVersionGloas, + builderInfo: &chain.BuilderInfo{Index: 7}, + } + + cfg := &config.Config{} + planSvc := action_plan.NewPlanService(cfg, chainSvc, log) + + h := NewHandler(&cfg.BuilderAPI, log, chainSvc, planSvc, payload_builder.NewPayloadCache(10), blsSigner) + + assert.Equal(t, uint64(7), h.builderIndex.Load(), + "NewHandler must resolve the builder index from chain state, not leave it at the zero-value default") +} + // TestHandleSubmitBeaconBlock_Success broadcasts the block immediately, // returns 202 without publishing anything during the request, and reveals the // envelope exactly once via the RevealService after the reveal time. diff --git a/pkg/payload_bidder/mockchain_test.go b/pkg/payload_bidder/mockchain_test.go index 99ded49..2049aec 100644 --- a/pkg/payload_bidder/mockchain_test.go +++ b/pkg/payload_bidder/mockchain_test.go @@ -29,6 +29,7 @@ type stubChainService struct { currentEpoch phase0.Epoch genesis beacon.Genesis headTracker *chain.HeadTracker + builderInfo *chain.BuilderInfo epochStatsDispatch utils.Dispatcher[*chain.EpochStats] } @@ -91,9 +92,11 @@ func (m *stubChainService) GetHeadVoteTracker() *chain.HeadVoteTracker { return func (m *stubChainService) GetHeadTracker() *chain.HeadTracker { return m.headTracker } func (m *stubChainService) GetFinalizedEpoch() phase0.Epoch { return 0 } -func (m *stubChainService) GetBuilderByIndex(uint64) *chain.BuilderInfo { return nil } -func (m *stubChainService) GetBuilderByPubkey(phase0.BLSPubKey) *chain.BuilderInfo { return nil } -func (m *stubChainService) GetBuilders() []*chain.BuilderInfo { return nil } +func (m *stubChainService) GetBuilderByIndex(uint64) *chain.BuilderInfo { return nil } +func (m *stubChainService) GetBuilderByPubkey(phase0.BLSPubKey) *chain.BuilderInfo { + return m.builderInfo +} +func (m *stubChainService) GetBuilders() []*chain.BuilderInfo { return nil } func (m *stubChainService) GetValidatorPubkeyByIndex(phase0.ValidatorIndex) *phase0.BLSPubKey { return nil diff --git a/pkg/payload_bidder/reveal_service.go b/pkg/payload_bidder/reveal_service.go index c3faff1..483739b 100644 --- a/pkg/payload_bidder/reveal_service.go +++ b/pkg/payload_bidder/reveal_service.go @@ -200,6 +200,16 @@ func NewRevealService( func (s *RevealService) Start(ctx context.Context) error { s.ctx, s.cancel = context.WithCancel(ctx) + // Resolve the builder index from chain state up front, independent of + // lifecycle registration. Without this, a builder already registered + // on-chain before this process started would sign every envelope with + // the zero-value index until (if ever) the lifecycle callback fires. + // SetBuilderIndex still overrides this on a live registration event. + if builderInfo := s.chainSvc.GetBuilderByPubkey(s.signer.PublicKey()); builderInfo != nil { + s.builderIndex.Store(builderInfo.Index) + s.log.WithField("builder_index", builderInfo.Index).Info("Resolved builder index from chain state") + } + s.wg.Add(1) go s.run() diff --git a/pkg/payload_bidder/reveal_service_test.go b/pkg/payload_bidder/reveal_service_test.go index df16036..c830e5f 100644 --- a/pkg/payload_bidder/reveal_service_test.go +++ b/pkg/payload_bidder/reveal_service_test.go @@ -196,6 +196,44 @@ func waitForResult(t *testing.T, ch <-chan *RevealResult, timeout time.Duration) } } +// TestRevealService_ResolvesBuilderIndexFromChainStateOnStart guards against +// a builder already registered on-chain before this process started signing +// every envelope with the zero-value index. Start must resolve the real +// index from chain state itself, without waiting for a lifecycle +// registration callback that may never fire in this process. +func TestRevealService_ResolvesBuilderIndexFromChainStateOnStart(t *testing.T) { + log := logrus.New() + log.SetLevel(logrus.PanicLevel) + + blsSigner, err := signer.NewBLSSigner("0x0000000000000000000000000000000000000000000000000000000000000001") + require.NoError(t, err) + + chainSvc := &stubChainService{ + genesisTime: time.Now().Add(-4 * time.Second), + slotDuration: 4 * time.Second, + currentFork: version.DataVersionGloas, + builderInfo: &chain.BuilderInfo{Index: 7}, + } + + builderSvc := newTestBuilderSvc(chainSvc) + payments := NewPaymentTracker(chainSvc, log) + publisher := &mockEnvelopePublisher{} + cfg := &config.Config{} + planSvc := action_plan.NewPlanService(cfg, chainSvc, log) + votes := newStubVoteSource() + + svc := NewRevealService(cfg, NewSigner(blsSigner), publisher, chainSvc, builderSvc, + payments, planSvc, votes, log) + + require.Equal(t, uint64(0), svc.builderIndex.Load(), "precondition: index unset before Start") + + require.NoError(t, svc.Start(context.Background())) + defer svc.Stop() + + assert.Equal(t, uint64(7), svc.builderIndex.Load(), + "Start must resolve the builder index from chain state, not leave it at the zero-value default") +} + func TestRevealService_RevealsAtDueTime(t *testing.T) { env := newRevealTestEnv(t, 4*time.Second, 500) sub := env.svc.SubscribeResults(4, false) diff --git a/pkg/payload_bidder/signer.go b/pkg/payload_bidder/signer.go index 7530025..bbf5c8d 100644 --- a/pkg/payload_bidder/signer.go +++ b/pkg/payload_bidder/signer.go @@ -35,6 +35,11 @@ func NewSigner(blsSigner *signer.BLSSigner) *Signer { return &Signer{blsSigner: blsSigner} } +// PublicKey returns the builder's BLS public key. +func (s *Signer) PublicKey() phase0.BLSPubKey { + return s.blsSigner.PublicKey() +} + // SignBid signs an execution payload bid. forkVersion must be the fork version // the consensus client verifies against (the Gloas fork version). func (s *Signer) SignBid(