Skip to content

Commit 8a6ce74

Browse files
authored
1/n feat: add preconfirmation support (#3005)
1 parent b193bed commit 8a6ce74

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+3019
-55
lines changed

beacon/blockchain/finalize_block.go

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,29 @@ import (
3636
sdk "github.com/cosmos/cosmos-sdk/types"
3737
)
3838

39+
// sequencerFinalizeOptimisticBuild triggers an optimistic payload build from FinalizeBlock
40+
// when ProcessProposal was skipped (e.g., proposal arrived after the CometBFT propose timeout).
41+
func (s *Service) sequencerFinalizeOptimisticBuild(
42+
ctx context.Context,
43+
blk *ctypes.BeaconBlock,
44+
consensusTime math.U64,
45+
) {
46+
s.logger.Warn("ProcessProposal was skipped, triggering optimistic build from FinalizeBlock",
47+
"current_slot", blk.GetSlot().Base10(),
48+
)
49+
50+
st := s.storageBackend.StateFromContext(ctx)
51+
ephemeralState := st.Protect(ctx)
52+
53+
buildData, err := s.preFetchBuildData(ephemeralState, consensusTime)
54+
if err != nil {
55+
s.logger.Error("Failed to prepare optimistic build data from FinalizeBlock", "error", err)
56+
return
57+
}
58+
59+
s.handleOptimisticPayloadBuild(ctx, buildData)
60+
}
61+
3962
func (s *Service) FinalizeBlock(
4063
ctx sdk.Context,
4164
req *cmtabci.FinalizeBlockRequest,
@@ -80,7 +103,20 @@ func (s *Service) FinalizeBlock(
80103
}
81104

82105
// STEP 4: Post Finalizations cleanups.
83-
return valUpdates, s.PostFinalizeBlockOps(ctx, blk)
106+
if err = s.PostFinalizeBlockOps(ctx, blk); err != nil {
107+
return valUpdates, err
108+
}
109+
110+
// STEP 5: Sequencer fallback — trigger optimistic build if ProcessProposal was skipped.
111+
if s.preconfCfg.IsSequencer() && s.localBuilder.Enabled() {
112+
skipped := !s.optimisticBuildTriggered.Load()
113+
s.optimisticBuildTriggered.Store(false)
114+
if skipped {
115+
s.sequencerFinalizeOptimisticBuild(ctx, blk, math.U64(req.GetTime().Unix())) //#nosec: G115
116+
}
117+
}
118+
119+
return valUpdates, nil
84120
}
85121

86122
func (s *Service) FinalizeSidecars(

beacon/blockchain/payload_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ func TestOptimisticBlockBuildingRejectedBlockStateChecks(t *testing.T) {
134134
ctx.ConsensusCtx(),
135135
types.NewConsensusBlock(invalidBlk, proposerAddress, consensusTime),
136136
true, // this block is next block proposer
137+
proposerAddress,
137138
)
138139
require.ErrorIs(t, err, core.ErrProposerMismatch)
139140

@@ -241,6 +242,7 @@ func TestOptimisticBlockBuildingVerifiedBlockStateChecks(t *testing.T) {
241242
ctx.ConsensusCtx(),
242243
types.NewConsensusBlock(validBlk, ctx.ProposerAddress(), consensusTime),
243244
true, // this block is next block proposer
245+
ctx.ProposerAddress(),
244246
)
245247
require.NoError(t, err)
246248

@@ -282,6 +284,8 @@ func setupOptimisticPayloadTests(t *testing.T, cs chain.Spec) (
282284
b,
283285
sp,
284286
ts,
287+
nil, // preconf.Config unused in this test
288+
nil, // preconf.Whitelist unused in this test
285289
)
286290
return chain, st, cms, ctx, sp, b, sb, eng, depStore
287291
}

beacon/blockchain/process_proposal.go

Lines changed: 61 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ package blockchain
2323
import (
2424
"bytes"
2525
"context"
26+
"encoding/hex"
2627
"fmt"
2728
"time"
2829

@@ -155,6 +156,7 @@ func (s *Service) ProcessProposal(
155156
ctx,
156157
consensusBlk,
157158
bytes.Equal(thisNodeAddress, req.NextProposerAddress),
159+
req.NextProposerAddress,
158160
)
159161
if err != nil {
160162
s.logger.Error("failed to verify incoming block", "error", err)
@@ -254,11 +256,12 @@ func (s *Service) VerifyIncomingBlobSidecars(
254256
// VerifyIncomingBlock verifies the state root of an incoming block
255257
// and logs the process.
256258
//
257-
//nolint:funlen // abundantly commented
259+
//nolint:funlen,gocognit // abundantly commented
258260
func (s *Service) VerifyIncomingBlock(
259261
ctx context.Context,
260262
blk *types.ConsensusBlock,
261263
isNextBlockProposer bool,
264+
nextProposerAddress []byte,
262265
) (transition.ValidatorUpdates, error) {
263266
beaconBlk := blk.GetBeaconBlock()
264267
state := s.storageBackend.StateFromContext(ctx)
@@ -299,24 +302,50 @@ func (s *Service) VerifyIncomingBlock(
299302
return nil, ErrUnexpectedBlockSlot
300303
}
301304

302-
var (
303-
nextBlockData *builder.RequestPayloadData
304-
errFetch error
305-
shouldBuildNextPayload = s.shouldBuildNextPayload(isNextBlockProposer)
306-
)
305+
// Determine if we should optimistically build a payload for the next slot.
306+
var shouldBuildNextPayload bool
307+
if s.localBuilder.Enabled() {
308+
switch {
309+
case isNextBlockProposer && !s.preconfCfg.ShouldFetchFromSequencer():
310+
// We're the next proposer and not fetching from sequencer,
311+
// build optimistically on local EL for ourselves.
312+
shouldBuildNextPayload = true
313+
s.logger.Info("Next proposer is this node, building optimistically",
314+
"current_block_slot", blkSlot.Base10(),
315+
"target_build_slot", (blkSlot + 1).Base10(),
316+
)
317+
case s.preconfCfg.IsSequencer():
318+
// We're the sequencer, build for whitelisted validators.
319+
// Only check whitelist if we can resolve the next proposer pubkey.
320+
expectedProposerPubkey, pubkeyErr := s.getNextProposerPubkey(state, nextProposerAddress)
321+
if pubkeyErr != nil {
322+
s.logger.Error("Failed to get next proposer pubkey", "error", pubkeyErr)
323+
} else {
324+
shouldBuildNextPayload = s.preconfWhitelist.IsWhitelisted(expectedProposerPubkey)
325+
}
326+
s.logger.Info("Sequencer mode: determined next proposer",
327+
"current_block_slot", blkSlot.Base10(),
328+
"target_build_slot", (blkSlot + 1).Base10(),
329+
"next_proposer_address", hex.EncodeToString(nextProposerAddress),
330+
"expected_proposer_pubkey", expectedProposerPubkey.String(),
331+
"is_whitelisted", shouldBuildNextPayload,
332+
)
333+
}
334+
}
307335

336+
var nextBlockData *builder.RequestPayloadData
308337
if shouldBuildNextPayload {
309338
// makes sure that preFetchBuildData does not affect state
310339
ephemeralState := state.Protect(ctx)
311-
nextBlockData, errFetch = s.preFetchBuildData(ephemeralState, blk.GetConsensusTime())
312-
if errFetch != nil {
340+
nextBlockData, err = s.preFetchBuildData(ephemeralState, blk.GetConsensusTime())
341+
if err != nil {
313342
// We don't return with err if pre-fetch fails. Instead we log the issue
314343
// and still move to process the current block. Next block can always be
315344
// built right after current height is finalized.
316345
s.logger.Warn(
317346
"Failed pre fetching data for optimistic block building",
318-
"case", "block rejectiong",
319-
"err", errFetch,
347+
"case", "block rejection",
348+
"err", err,
320349
)
321350
}
322351
}
@@ -349,18 +378,19 @@ func (s *Service) VerifyIncomingBlock(
349378
if shouldBuildNextPayload {
350379
// makes sure that preFetchBuildDataForSuccess does not affect state
351380
ephemeralState := state.Protect(ctx)
352-
nextBlockData, errFetch = s.preFetchBuildData(ephemeralState, blk.GetConsensusTime())
353-
if errFetch != nil {
381+
nextBlockData, err = s.preFetchBuildData(ephemeralState, blk.GetConsensusTime())
382+
if err != nil {
354383
// We don't mark the block as rejected if it is valid but pre-fetch fails.
355384
// Instead we log the issue and move to process the current block.
356385
// Next block can always be built right after current height is finalized.
357386
s.logger.Warn(
358387
"Failed pre fetching data for optimistic block building",
359388
"case", "block success",
360-
"err", errFetch,
389+
"err", err,
361390
)
362391
return valUpdates, nil
363392
}
393+
s.optimisticBuildTriggered.Store(true)
364394
go s.handleOptimisticPayloadBuild(ctx, nextBlockData)
365395
}
366396

@@ -398,8 +428,22 @@ func (s *Service) verifyStateRoot(
398428
return valUpdates, err
399429
}
400430

401-
// shouldBuildNextPayload returns true if optimistic
402-
// payload builds are enabled.
403-
func (s *Service) shouldBuildNextPayload(isNextBlockProposer bool) bool {
404-
return isNextBlockProposer && s.localBuilder.Enabled()
431+
// getNextProposerPubkey retrieves the BLS public key for the next proposer given their CometBFT address.
432+
func (s *Service) getNextProposerPubkey(st *statedb.StateDB, nextProposerAddress []byte) (crypto.BLSPubkey, error) {
433+
// Get all validators and find the one matching the CometBFT address
434+
validators, err := st.GetValidators()
435+
if err != nil {
436+
return crypto.BLSPubkey{}, fmt.Errorf("failed to get validators: %w", err)
437+
}
438+
439+
// Find the validator whose BLS pubkey produces the given CometBFT address
440+
for _, validator := range validators {
441+
pubkey := validator.GetPubkey()
442+
computedAddr, _ := crypto.GetAddressFromPubKey(pubkey)
443+
if bytes.Equal(computedAddr, nextProposerAddress) {
444+
return pubkey, nil
445+
}
446+
}
447+
448+
return crypto.BLSPubkey{}, fmt.Errorf("validator not found for address: %x", nextProposerAddress)
405449
}

beacon/blockchain/service.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"sync"
2727
"sync/atomic"
2828

29+
"github.com/berachain/beacon-kit/beacon/preconf"
2930
engineprimitives "github.com/berachain/beacon-kit/engine-primitives/engine-primitives"
3031
"github.com/berachain/beacon-kit/execution/deposit"
3132
"github.com/berachain/beacon-kit/log"
@@ -69,6 +70,22 @@ type Service struct {
6970
// It helps avoid resending the same FCU data (and spares a network call)
7071
// in case optimistic block building is active
7172
latestFcuReq atomic.Pointer[engineprimitives.ForkchoiceStateV1]
73+
74+
// preconfCfg holds the preconfirmation configuration.
75+
preconfCfg *preconf.Config
76+
77+
// preconfWhitelist contains whitelisted validator pubkeys for preconfirmation.
78+
// Used by both sequencer and validators:
79+
// - Sequencer: checks if next proposer is whitelisted to trigger optimistic FCU
80+
// - Validator: checks if self is whitelisted to fetch payload from sequencer
81+
// Can be nil if preconf is disabled.
82+
preconfWhitelist preconf.Whitelist
83+
84+
// optimisticBuildTriggered tracks whether ProcessProposal triggered an
85+
// optimistic payload build for the next slot. FinalizeBlock checks this
86+
// flag: if ProcessProposal was skipped (e.g., late proposal), the flag
87+
// remains false and FinalizeBlock triggers the build as a fallback.
88+
optimisticBuildTriggered atomic.Bool
7289
}
7390

7491
// NewService creates a new validator service.
@@ -82,6 +99,8 @@ func NewService(
8299
localBuilder LocalBuilder,
83100
stateProcessor StateProcessor,
84101
telemetrySink TelemetrySink,
102+
preconfCfg *preconf.Config,
103+
preconfWhitelist preconf.Whitelist,
85104
) *Service {
86105
return &Service{
87106
storageBackend: storageBackend,
@@ -96,6 +115,8 @@ func NewService(
96115
stateProcessor: stateProcessor,
97116
metrics: newChainMetrics(telemetrySink),
98117
forceStartupSyncOnce: new(sync.Once),
118+
preconfCfg: preconfCfg,
119+
preconfWhitelist: preconfWhitelist,
99120
}
100121
}
101122

0 commit comments

Comments
 (0)