Skip to content

Commit 022570c

Browse files
Shota Ehrlichrezberacalberaabi87
authored
feat(fork): timestamp based forking (1/n) (#2630)
Signed-off-by: Shota Ehrlich <[email protected]> Co-authored-by: Rez <[email protected]> Co-authored-by: Cal Bera <[email protected]> Co-authored-by: abear <[email protected]>
1 parent fedc426 commit 022570c

Some content is hidden

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

69 files changed

+971
-504
lines changed

beacon/blockchain/errors.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ var (
3131
ErrNilBlk = errors.New("nil beacon block")
3232
// ErrNilBlob is an error for when the BlobSidecars is nil.
3333
ErrNilBlob = errors.New("nil blob")
34+
// ErrVersionMismatch is an error for when the fork for the block timestamp does not match the fork
35+
// for the ABCI timestamp.
36+
ErrVersionMismatch = errors.New("ABCI fork version mismatch")
3437
// ErrDataNotAvailable indicates that the required data is not available.
3538
ErrDataNotAvailable = errors.New("data not available")
3639
// ErrSidecarCommitmentMismatch indicates that the BeaconBlockBody commitments do not match the sidecars.

beacon/blockchain/execution_engine.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import (
2525
"fmt"
2626

2727
ctypes "github.com/berachain/beacon-kit/consensus-types/types"
28-
contypes "github.com/berachain/beacon-kit/consensus/types"
2928
engineprimitives "github.com/berachain/beacon-kit/engine-primitives/engine-primitives"
3029
statedb "github.com/berachain/beacon-kit/state-transition/core/state"
3130
)
@@ -38,7 +37,6 @@ import (
3837
func (s *Service) sendPostBlockFCU(
3938
ctx context.Context,
4039
st *statedb.StateDB,
41-
blk *contypes.ConsensusBlock,
4240
) error {
4341
lph, err := st.GetLatestExecutionPayloadHeader()
4442
if err != nil {
@@ -47,15 +45,14 @@ func (s *Service) sendPostBlockFCU(
4745

4846
// Send a forkchoice update without payload attributes to notify
4947
// EL of the new head.
50-
beaconBlk := blk.GetBeaconBlock()
5148
// TODO: Switch to New().
5249
req := ctypes.BuildForkchoiceUpdateRequestNoAttrs(
5350
&engineprimitives.ForkchoiceStateV1{
5451
HeadBlockHash: lph.GetBlockHash(),
5552
SafeBlockHash: lph.GetParentHash(),
5653
FinalizedBlockHash: lph.GetParentHash(),
5754
},
58-
s.chainSpec.ActiveForkVersionForSlot(beaconBlk.GetSlot()),
55+
lph.GetForkVersion(),
5956
)
6057
if _, err = s.executionEngine.NotifyForkchoiceUpdate(ctx, req); err != nil {
6158
return fmt.Errorf("failed forkchoice update, head %s: %w",

beacon/blockchain/finalize_block.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ func (s *Service) FinalizeBlock(
4343
req,
4444
BeaconBlockTxIndex,
4545
BlobSidecarsTxIndex,
46-
s.chainSpec.ActiveForkVersionForSlot(math.Slot(req.Height))) // #nosec G115
46+
// While req.GetTime() and blk.GetTimestamp() may be different, they are guaranteed
47+
// to map to the same forkVersion due to checks during ProcessProposal.
48+
s.chainSpec.ActiveForkVersionForTimestamp(math.U64(req.GetTime().Unix()))) //#nosec: G115
4749
if err != nil {
4850
s.logger.Error("Failed to decode block and blobs", "error", err)
4951
return nil, fmt.Errorf("failed to decode block and blobs: %w", err)
@@ -121,7 +123,7 @@ func (s *Service) FinalizeBlock(
121123
s.logger.Error("failed to processPruning", "error", err)
122124
}
123125

124-
if err = s.sendPostBlockFCU(ctx, st, consensusBlk); err != nil {
126+
if err = s.sendPostBlockFCU(ctx, st); err != nil {
125127
return nil, fmt.Errorf("sendPostBlockFCU failed: %w", err)
126128
}
127129

beacon/blockchain/interfaces.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,11 @@ type LocalBuilder interface {
6767
ctx context.Context,
6868
st *statedb.StateDB,
6969
slot math.Slot,
70-
timestamp uint64,
70+
timestamp math.U64,
7171
parentBlockRoot common.Root,
7272
headEth1BlockHash common.ExecutionHash,
7373
finalEth1BlockHash common.ExecutionHash,
74-
) (*engineprimitives.PayloadID, error)
74+
) (*engineprimitives.PayloadID, common.Version, error)
7575
}
7676

7777
// StateProcessor defines the interface for processing various state transitions
@@ -167,5 +167,5 @@ type PruningChainSpec interface {
167167
type ServiceChainSpec interface {
168168
PruningChainSpec
169169
chain.BlobSpec
170-
ActiveForkVersionForSlot(slot math.Slot) common.Version
170+
ActiveForkVersionForTimestamp(timestamp math.U64) common.Version
171171
}

beacon/blockchain/payload.go

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -37,19 +37,6 @@ func (s *Service) forceSyncUponProcess(
3737
ctx context.Context,
3838
st *statedb.StateDB,
3939
) {
40-
slot, err := st.GetSlot()
41-
if err != nil {
42-
s.logger.Error(
43-
"failed to get slot for force startup head",
44-
"error", err,
45-
)
46-
return
47-
}
48-
49-
// TODO: Verify if the slot number is correct here, I believe in current
50-
// form it should be +1'd. Not a big deal until hardforks are in play though.
51-
slot++
52-
5340
lph, err := st.GetLatestExecutionPayloadHeader()
5441
if err != nil {
5542
s.logger.Error(
@@ -64,7 +51,7 @@ func (s *Service) forceSyncUponProcess(
6451
"head_eth1_hash", lph.GetBlockHash(),
6552
"safe_eth1_hash", lph.GetParentHash(),
6653
"finalized_eth1_hash", lph.GetParentHash(),
67-
"for_slot", slot.Base10(),
54+
"for_slot", lph.GetNumber(),
6855
)
6956

7057
// Submit the forkchoice update to the execution client.
@@ -74,7 +61,7 @@ func (s *Service) forceSyncUponProcess(
7461
SafeBlockHash: lph.GetParentHash(),
7562
FinalizedBlockHash: lph.GetParentHash(),
7663
},
77-
s.chainSpec.ActiveForkVersionForSlot(slot),
64+
s.chainSpec.ActiveForkVersionForTimestamp(lph.GetTimestamp()),
7865
)
7966
if _, err = s.executionEngine.NotifyForkchoiceUpdate(ctx, req); err != nil {
8067
s.logger.Error(
@@ -117,7 +104,7 @@ func (s *Service) forceSyncUponFinalize(
117104
SafeBlockHash: executionPayload.GetParentHash(),
118105
FinalizedBlockHash: executionPayload.GetParentHash(),
119106
},
120-
s.chainSpec.ActiveForkVersionForSlot(beaconBlock.GetSlot()),
107+
s.chainSpec.ActiveForkVersionForTimestamp(executionPayload.GetTimestamp()),
121108
)
122109

123110
switch _, err = s.executionEngine.NotifyForkchoiceUpdate(ctx, req); {
@@ -196,12 +183,12 @@ func (s *Service) rebuildPayloadForRejectedBlock(
196183
}
197184

198185
// Submit a request for a new payload.
199-
if _, err = s.localBuilder.RequestPayloadAsync(
186+
if _, _, err = s.localBuilder.RequestPayloadAsync(
200187
ctx,
201188
st,
202189
// We are rebuilding for the current slot.
203190
stateSlot,
204-
nextPayloadTimestamp.Unwrap(),
191+
nextPayloadTimestamp,
205192
// We set the parent root to the previous block root. The HashTreeRoot
206193
// of the header is the same as the HashTreeRoot of the block.
207194
latestHeader.HashTreeRoot(),
@@ -264,10 +251,10 @@ func (s *Service) optimisticPayloadBuild(
264251

265252
// We then trigger a request for the next payload.
266253
payload := blk.GetBody().GetExecutionPayload()
267-
if _, err := s.localBuilder.RequestPayloadAsync(
254+
if _, _, err := s.localBuilder.RequestPayloadAsync(
268255
ctx, st,
269256
slot,
270-
nextPayloadTimestamp.Unwrap(),
257+
nextPayloadTimestamp,
271258
// The previous block root is simply the root of the block we just
272259
// processed.
273260
blk.HashTreeRoot(),

beacon/blockchain/process_proposal.go

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import (
3737
"github.com/berachain/beacon-kit/primitives/eip4844"
3838
"github.com/berachain/beacon-kit/primitives/math"
3939
"github.com/berachain/beacon-kit/primitives/transition"
40+
"github.com/berachain/beacon-kit/primitives/version"
4041
"github.com/berachain/beacon-kit/state-transition/core"
4142
statedb "github.com/berachain/beacon-kit/state-transition/core/state"
4243
cmtabci "github.com/cometbft/cometbft/abci/types"
@@ -67,12 +68,14 @@ func (s *Service) ProcessProposal(
6768
)
6869
}
6970

71+
forkVersion := s.chainSpec.ActiveForkVersionForTimestamp(math.U64(req.GetTime().Unix())) //#nosec: G115
7072
// Decode signed block and sidecars.
7173
signedBlk, sidecars, err := encoding.ExtractBlobsAndBlockFromRequest(
7274
req,
7375
BeaconBlockTxIndex,
7476
BlobSidecarsTxIndex,
75-
s.chainSpec.ActiveForkVersionForSlot(math.Slot(req.Height))) // #nosec G115
77+
forkVersion,
78+
)
7679
if err != nil {
7780
return err
7881
}
@@ -90,24 +93,41 @@ func (s *Service) ProcessProposal(
9093
}
9194

9295
blk := signedBlk.GetBeaconBlock()
96+
97+
// There are two different timestamps:
98+
// - The "consensus time" is determined by CometBFT consensus and can be retrieved with `req.GetTime()`
99+
// - The "block time" is determined by beacon-kit consensus and can be retrieved with `blk.GetTimestamp()`
100+
// The "consensus time" is what the network agrees the current time is based on CometBFT PBTS.
101+
// This "consensus time" is used to constrain the timestamp set as the "block time" by the
102+
// beacon-kit app, but they are not always equal in value. The "block time" is used by the
103+
// beacon-kit consensus and execution layers to determine the active fork version.
104+
//
105+
// When unmarshaling the BeaconBlock, we do not yet have access to the "block time", so we
106+
// must rely on the "consensus time" as our best estimation of the "block time" needed to
107+
// determine the current fork version. Since the two timestamps could be different, we need to
108+
// ensure that the fork version for these timestamps are the same. This may result in a failed
109+
// proposal or two at the start of the fork.
110+
blkVersion := s.chainSpec.ActiveForkVersionForTimestamp(blk.GetTimestamp())
111+
if !version.Equals(blkVersion, forkVersion) {
112+
return fmt.Errorf("CometBFT version %v, BeaconBlock version %v: %w",
113+
forkVersion, blkVersion,
114+
ErrVersionMismatch,
115+
)
116+
}
93117
// Make sure we have the right number of BlobSidecars
94118
blobKzgCommitments := blk.GetBody().GetBlobKzgCommitments()
95119
numCommitments := len(blobKzgCommitments)
96120
if numCommitments != len(sidecars) {
97-
err = fmt.Errorf("expected %d sidecars, got %d: %w",
121+
return fmt.Errorf("expected %d sidecars, got %d: %w",
98122
numCommitments, len(sidecars),
99123
ErrSidecarCommitmentMismatch,
100124
)
101-
s.logger.Warn(err.Error())
102-
return err
103125
}
104126
if uint64(numCommitments) > s.chainSpec.MaxBlobsPerBlock() {
105-
err = fmt.Errorf("expected less than %d sidecars, got %d: %w",
127+
return fmt.Errorf("expected less than %d sidecars, got %d: %w",
106128
s.chainSpec.MaxBlobsPerBlock(), numCommitments,
107129
core.ErrExceedsBlockBlobLimit,
108130
)
109-
s.logger.Warn(err.Error())
110-
return err
111131
}
112132

113133
// Verify the block and sidecar signatures. We can simply verify the block
@@ -140,7 +160,7 @@ func (s *Service) ProcessProposal(
140160
}
141161
}
142162

143-
// Process the block
163+
// Process the block.
144164
consensusBlk := types.NewConsensusBlock(
145165
blk,
146166
req.GetProposerAddress(),

beacon/validator/block_builder.go

Lines changed: 37 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,7 @@ func (s *Service) BuildBlockAndSidecars(
6262
// and safe block hashes to the execution client.
6363
st := s.sb.StateFromContext(ctx)
6464

65-
// we introduce hard forks with the expectation that the height set for the
66-
// hard fork is the first height at which new rules apply. So we need to make
67-
// sure that when building blocks, we pick the right height. blkSlots is the
68-
// height for the next block, which consensus is requesting BeaconKit to build.
65+
// blkSlot is the height for the next block, which consensus is requesting BeaconKit to build.
6966
blkSlot := slotData.GetSlot()
7067

7168
// Prepare the state such that it is ready to build a block for
@@ -74,29 +71,44 @@ func (s *Service) BuildBlockAndSidecars(
7471
return nil, nil, err
7572
}
7673

77-
// Build forkdata used for the signing root of the reveal and the sidecars
78-
forkData, err := s.buildForkData(st, blkSlot)
74+
// Grab parent block root for payload request.
75+
parentBlockRoot, err := st.GetBlockRootAtIndex(
76+
(blkSlot.Unwrap() - 1) % s.chainSpec.SlotsPerHistoricalRoot(),
77+
)
7978
if err != nil {
8079
return nil, nil, err
8180
}
8281

83-
// Build the reveal for the current slot.
84-
// TODO: We can optimize to pre-compute this in parallel?
85-
reveal, err := s.buildRandaoReveal(forkData, blkSlot)
82+
// Get the payload for the block.
83+
envelope, err := s.retrieveExecutionPayload(ctx, st, parentBlockRoot, slotData)
84+
if err != nil {
85+
return nil, nil, fmt.Errorf("failed retrieving execution payload: %w", err)
86+
}
87+
88+
// We introduce hard forks with the expectation that the first block proposed after the
89+
// hard fork timestamp is when new rules apply. When building blocks, we provide the Execution
90+
// Layer client with a timestamp, and it will create its payload based on that timestamp. We
91+
// must use this same timestamp from the payload to build the beacon block. This ensures that
92+
// we are building on the same fork version as the Execution Layer.
93+
timestamp := envelope.GetExecutionPayload().GetTimestamp()
94+
95+
// Build forkdata used for the signing root of the reveal and the sidecars
96+
forkData, err := s.buildForkData(st, timestamp)
8697
if err != nil {
8798
return nil, nil, err
8899
}
89100

90101
// Create a new empty block from the current state.
91-
blk, err := s.getEmptyBeaconBlockForSlot(st, blkSlot)
102+
blk, err := s.getEmptyBeaconBlockForSlot(st, blkSlot, forkData.CurrentVersion, parentBlockRoot)
92103
if err != nil {
93104
return nil, nil, err
94105
}
95106

96-
// Get the payload for the block.
97-
envelope, err := s.retrieveExecutionPayload(ctx, st, blk, slotData)
107+
// Build the reveal for the current slot.
108+
// TODO: We can optimize to pre-compute this in parallel?
109+
reveal, err := s.buildRandaoReveal(forkData, blkSlot)
98110
if err != nil {
99-
return nil, nil, fmt.Errorf("failed retrieving execution payload: %w", err)
111+
return nil, nil, err
100112
}
101113

102114
// We have to assemble the block body prior to producing the sidecars
@@ -150,15 +162,8 @@ func (s *Service) BuildBlockAndSidecars(
150162
// getEmptyBeaconBlockForSlot creates a new empty block.
151163
func (s *Service) getEmptyBeaconBlockForSlot(
152164
st *statedb.StateDB, requestedSlot math.Slot,
165+
forkVersion common.Version, parentBlockRoot common.Root,
153166
) (*ctypes.BeaconBlock, error) {
154-
// Create a new block.
155-
parentBlockRoot, err := st.GetBlockRootAtIndex(
156-
(requestedSlot.Unwrap() - 1) % s.chainSpec.SlotsPerHistoricalRoot(),
157-
)
158-
if err != nil {
159-
return nil, err
160-
}
161-
162167
// Get the proposer index for the slot.
163168
proposerIndex, err := st.ValidatorIndexByPubkey(
164169
s.signer.PublicKey(),
@@ -167,22 +172,23 @@ func (s *Service) getEmptyBeaconBlockForSlot(
167172
return nil, err
168173
}
169174

175+
// Create a new block.
170176
return ctypes.NewBeaconBlockWithVersion(
171177
requestedSlot,
172178
proposerIndex,
173179
parentBlockRoot,
174-
s.chainSpec.ActiveForkVersionForSlot(requestedSlot),
180+
forkVersion,
175181
)
176182
}
177183

178-
func (s *Service) buildForkData(st *statedb.StateDB, slot math.Slot) (*ctypes.ForkData, error) {
184+
func (s *Service) buildForkData(st *statedb.StateDB, timestamp math.U64) (*ctypes.ForkData, error) {
179185
genesisValidatorsRoot, err := st.GetGenesisValidatorsRoot()
180186
if err != nil {
181187
return nil, err
182188
}
183189

184190
return ctypes.NewForkData(
185-
s.chainSpec.ActiveForkVersionForSlot(slot),
191+
s.chainSpec.ActiveForkVersionForTimestamp(timestamp),
186192
genesisValidatorsRoot,
187193
), nil
188194
}
@@ -206,7 +212,7 @@ func (s *Service) buildRandaoReveal(
206212
func (s *Service) retrieveExecutionPayload(
207213
ctx context.Context,
208214
st *statedb.StateDB,
209-
blk *ctypes.BeaconBlock,
215+
parentBlockRoot common.Root,
210216
slotData *types.SlotData,
211217
) (ctypes.BuiltExecutionPayloadEnv, error) {
212218
//
@@ -215,8 +221,8 @@ func (s *Service) retrieveExecutionPayload(
215221
// Get the payload for the block.
216222
envelope, err := s.localPayloadBuilder.RetrievePayload(
217223
ctx,
218-
blk.GetSlot(),
219-
blk.GetParentBlockRoot(),
224+
slotData.GetSlot(),
225+
parentBlockRoot,
220226
)
221227
if err == nil {
222228
return envelope, nil
@@ -232,7 +238,7 @@ func (s *Service) retrieveExecutionPayload(
232238
// this less confusing.
233239

234240
s.metrics.failedToRetrievePayload(
235-
blk.GetSlot(),
241+
slotData.GetSlot(),
236242
err,
237243
)
238244

@@ -246,13 +252,13 @@ func (s *Service) retrieveExecutionPayload(
246252
return s.localPayloadBuilder.RequestPayloadSync(
247253
ctx,
248254
st,
249-
blk.GetSlot(),
255+
slotData.GetSlot(),
250256
payloadtime.Next(
251257
slotData.GetConsensusTime(),
252258
lph.GetTimestamp(),
253259
false, // buildOptimistically
254-
).Unwrap(),
255-
blk.GetParentBlockRoot(),
260+
),
261+
parentBlockRoot,
256262
lph.GetBlockHash(),
257263
lph.GetParentHash(),
258264
)

0 commit comments

Comments
 (0)