Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions pkg/builderapi/epbs/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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.
Expand Down
28 changes: 28 additions & 0 deletions pkg/builderapi/epbs/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
9 changes: 6 additions & 3 deletions pkg/payload_bidder/mockchain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type stubChainService struct {
currentEpoch phase0.Epoch
genesis beacon.Genesis
headTracker *chain.HeadTracker
builderInfo *chain.BuilderInfo

epochStatsDispatch utils.Dispatcher[*chain.EpochStats]
}
Expand Down Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions pkg/payload_bidder/reveal_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
38 changes: 38 additions & 0 deletions pkg/payload_bidder/reveal_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions pkg/payload_bidder/signer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down