mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-01-07 22:54:17 -05:00
Use proposer lookahead for data column verification (#16202)
Replace the proposer indices cache usage in data column sidecar verification with direct state lookahead access. Since data column sidecars require the Fulu fork, the state always has a ProposerLookahead field that provides O(1) proposer index lookups for current and next epoch. This simplifies SidecarProposerExpected() by removing: - Checkpoint-based proposer cache lookup - Singleflight wrapper (not needed for O(1) access) - Target root computation for cache keys 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -687,6 +687,12 @@ func sbrNotFound(t *testing.T, expectedRoot [32]byte) *mockStateByRooter {
|
|||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sbrReturnsState(st state.BeaconState) *mockStateByRooter {
|
||||||
|
return &mockStateByRooter{sbr: func(_ context.Context, _ [32]byte) (state.BeaconState, error) {
|
||||||
|
return st, nil
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
func sbrForValOverride(idx primitives.ValidatorIndex, val *ethpb.Validator) *mockStateByRooter {
|
func sbrForValOverride(idx primitives.ValidatorIndex, val *ethpb.Validator) *mockStateByRooter {
|
||||||
return sbrForValOverrideWithT(nil, idx, val)
|
return sbrForValOverrideWithT(nil, idx, val)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,12 +11,10 @@ import (
|
|||||||
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/helpers"
|
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/helpers"
|
||||||
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/peerdas"
|
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/peerdas"
|
||||||
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/transition"
|
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/transition"
|
||||||
forkchoicetypes "github.com/OffchainLabs/prysm/v7/beacon-chain/forkchoice/types"
|
|
||||||
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
|
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
|
||||||
fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams"
|
fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams"
|
||||||
"github.com/OffchainLabs/prysm/v7/config/params"
|
"github.com/OffchainLabs/prysm/v7/config/params"
|
||||||
"github.com/OffchainLabs/prysm/v7/consensus-types/blocks"
|
"github.com/OffchainLabs/prysm/v7/consensus-types/blocks"
|
||||||
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
|
|
||||||
"github.com/OffchainLabs/prysm/v7/encoding/bytesutil"
|
"github.com/OffchainLabs/prysm/v7/encoding/bytesutil"
|
||||||
"github.com/OffchainLabs/prysm/v7/runtime/logging"
|
"github.com/OffchainLabs/prysm/v7/runtime/logging"
|
||||||
"github.com/OffchainLabs/prysm/v7/time/slots"
|
"github.com/OffchainLabs/prysm/v7/time/slots"
|
||||||
@@ -484,88 +482,19 @@ func (dv *RODataColumnsVerifier) SidecarProposerExpected(ctx context.Context) (e
|
|||||||
|
|
||||||
defer dv.recordResult(RequireSidecarProposerExpected, &err)
|
defer dv.recordResult(RequireSidecarProposerExpected, &err)
|
||||||
|
|
||||||
type slotParentRoot struct {
|
|
||||||
slot primitives.Slot
|
|
||||||
parentRoot [fieldparams.RootLength]byte
|
|
||||||
}
|
|
||||||
|
|
||||||
targetRootBySlotParentRoot := make(map[slotParentRoot][fieldparams.RootLength]byte)
|
|
||||||
|
|
||||||
var targetRootFromCache = func(slot primitives.Slot, parentRoot [fieldparams.RootLength]byte) ([fieldparams.RootLength]byte, error) {
|
|
||||||
// Use cached values if available.
|
|
||||||
slotParentRoot := slotParentRoot{slot: slot, parentRoot: parentRoot}
|
|
||||||
if root, ok := targetRootBySlotParentRoot[slotParentRoot]; ok {
|
|
||||||
return root, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute the epoch of the data column slot.
|
|
||||||
dataColumnEpoch := slots.ToEpoch(slot)
|
|
||||||
if dataColumnEpoch > 0 {
|
|
||||||
dataColumnEpoch = dataColumnEpoch - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute the target root for the epoch.
|
|
||||||
targetRoot, err := dv.fc.TargetRootForEpoch(parentRoot, dataColumnEpoch)
|
|
||||||
if err != nil {
|
|
||||||
return [fieldparams.RootLength]byte{}, columnErrBuilder(errors.Wrap(err, "target root from epoch"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store the target root in the cache.
|
|
||||||
targetRootBySlotParentRoot[slotParentRoot] = targetRoot
|
|
||||||
|
|
||||||
return targetRoot, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, dataColumn := range dv.dataColumns {
|
for _, dataColumn := range dv.dataColumns {
|
||||||
// Extract the slot of the data column.
|
|
||||||
dataColumnSlot := dataColumn.Slot()
|
dataColumnSlot := dataColumn.Slot()
|
||||||
|
|
||||||
// Extract the root of the parent block corresponding to the data column.
|
// Get the verifying state, it is guaranteed to have the correct proposer in the lookahead.
|
||||||
parentRoot := dataColumn.ParentRoot()
|
|
||||||
|
|
||||||
// Compute the target root for the data column.
|
|
||||||
targetRoot, err := targetRootFromCache(dataColumnSlot, parentRoot)
|
|
||||||
if err != nil {
|
|
||||||
return columnErrBuilder(errors.Wrap(err, "target root"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute the epoch of the data column slot.
|
|
||||||
dataColumnEpoch := slots.ToEpoch(dataColumnSlot)
|
|
||||||
if dataColumnEpoch > 0 {
|
|
||||||
dataColumnEpoch = dataColumnEpoch - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a checkpoint for the target root.
|
|
||||||
checkpoint := &forkchoicetypes.Checkpoint{Root: targetRoot, Epoch: dataColumnEpoch}
|
|
||||||
|
|
||||||
// Try to extract the proposer index from the data column in the cache.
|
|
||||||
idx, cached := dv.pc.Proposer(checkpoint, dataColumnSlot)
|
|
||||||
|
|
||||||
if !cached {
|
|
||||||
parentRoot := dataColumn.ParentRoot()
|
|
||||||
// Ensure the expensive index computation is only performed once for
|
|
||||||
// concurrent requests for the same signature data.
|
|
||||||
idxAny, err, _ := dv.sg.Do(concatRootSlot(parentRoot, dataColumnSlot), func() (any, error) {
|
|
||||||
verifyingState, err := dv.getVerifyingState(ctx, dataColumn)
|
verifyingState, err := dv.getVerifyingState(ctx, dataColumn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, columnErrBuilder(errors.Wrap(err, "verifying state"))
|
return columnErrBuilder(errors.Wrap(err, "verifying state"))
|
||||||
}
|
}
|
||||||
|
|
||||||
idx, err = helpers.BeaconProposerIndexAtSlot(ctx, verifyingState, dataColumnSlot)
|
// Use proposer lookahead directly
|
||||||
|
idx, err := helpers.BeaconProposerIndexAtSlot(ctx, verifyingState, dataColumnSlot)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, columnErrBuilder(errors.Wrap(err, "compute proposer"))
|
return columnErrBuilder(errors.Wrap(err, "proposer from lookahead"))
|
||||||
}
|
|
||||||
|
|
||||||
return idx, nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var ok bool
|
|
||||||
if idx, ok = idxAny.(primitives.ValidatorIndex); !ok {
|
|
||||||
return columnErrBuilder(errors.New("type assertion to ValidatorIndex failed"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if idx != dataColumn.ProposerIndex() {
|
if idx != dataColumn.ProposerIndex() {
|
||||||
@@ -626,7 +555,3 @@ func inclusionProofKey(c blocks.RODataColumn) ([32]byte, error) {
|
|||||||
|
|
||||||
return sha256.Sum256(unhashedKey), nil
|
return sha256.Sum256(unhashedKey), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func concatRootSlot(root [fieldparams.RootLength]byte, slot primitives.Slot) string {
|
|
||||||
return string(root[:]) + fmt.Sprintf("%d", slot)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package verification
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"sync"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -795,87 +794,90 @@ func TestDataColumnsSidecarProposerExpected(t *testing.T) {
|
|||||||
blobCount = 1
|
blobCount = 1
|
||||||
)
|
)
|
||||||
|
|
||||||
parentRoot := [fieldparams.RootLength]byte{}
|
|
||||||
columns := GenerateTestDataColumns(t, parentRoot, columnSlot, blobCount)
|
|
||||||
firstColumn := columns[0]
|
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
testCases := []struct {
|
parentRoot := [fieldparams.RootLength]byte{}
|
||||||
name string
|
|
||||||
stateByRooter StateByRooter
|
|
||||||
proposerCache proposerCache
|
|
||||||
columns []blocks.RODataColumn
|
|
||||||
error string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Cached, matches",
|
|
||||||
stateByRooter: nil,
|
|
||||||
proposerCache: &mockProposerCache{
|
|
||||||
ProposerCB: pcReturnsIdx(firstColumn.ProposerIndex()),
|
|
||||||
},
|
|
||||||
columns: columns,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Cached, does not match",
|
|
||||||
stateByRooter: nil,
|
|
||||||
proposerCache: &mockProposerCache{
|
|
||||||
ProposerCB: pcReturnsIdx(firstColumn.ProposerIndex() + 1),
|
|
||||||
},
|
|
||||||
columns: columns,
|
|
||||||
error: errSidecarUnexpectedProposer.Error(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Not cached, state lookup failure",
|
|
||||||
stateByRooter: sbrNotFound(t, firstColumn.ParentRoot()),
|
|
||||||
proposerCache: &mockProposerCache{
|
|
||||||
ProposerCB: pcReturnsNotFound(),
|
|
||||||
},
|
|
||||||
columns: columns,
|
|
||||||
error: "verifying state",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
// Create a Fulu state to get the expected proposer from the lookahead.
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
fuluState, _ := util.DeterministicGenesisStateFulu(t, 32)
|
||||||
|
expectedProposer, err := fuluState.ProposerLookahead()
|
||||||
|
require.NoError(t, err)
|
||||||
|
expectedProposerIdx := primitives.ValidatorIndex(expectedProposer[columnSlot])
|
||||||
|
|
||||||
|
// Generate data columns with the expected proposer index.
|
||||||
|
matchingColumns := generateTestDataColumnsWithProposer(t, parentRoot, columnSlot, blobCount, expectedProposerIdx)
|
||||||
|
// Generate data columns with wrong proposer index.
|
||||||
|
wrongColumns := generateTestDataColumnsWithProposer(t, parentRoot, columnSlot, blobCount, expectedProposerIdx+1)
|
||||||
|
|
||||||
|
t.Run("Proposer matches", func(t *testing.T) {
|
||||||
initializer := Initializer{
|
initializer := Initializer{
|
||||||
shared: &sharedResources{
|
shared: &sharedResources{
|
||||||
sr: tc.stateByRooter,
|
sr: sbrReturnsState(fuluState),
|
||||||
pc: tc.proposerCache,
|
hsp: &mockHeadStateProvider{
|
||||||
hsp: &mockHeadStateProvider{},
|
headRoot: parentRoot[:],
|
||||||
fc: &mockForkchoicer{
|
headSlot: columnSlot, // Same epoch so HeadStateReadOnly is used
|
||||||
TargetRootForEpochCB: fcReturnsTargetRoot([fieldparams.RootLength]byte{}),
|
headStateReadOnly: fuluState,
|
||||||
},
|
},
|
||||||
|
fc: &mockForkchoicer{},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
verifier := initializer.NewDataColumnsVerifier(tc.columns, GossipDataColumnSidecarRequirements)
|
verifier := initializer.NewDataColumnsVerifier(matchingColumns, GossipDataColumnSidecarRequirements)
|
||||||
var wg sync.WaitGroup
|
|
||||||
|
|
||||||
var err1, err2 error
|
|
||||||
wg.Go(func() {
|
|
||||||
err1 = verifier.SidecarProposerExpected(ctx)
|
|
||||||
})
|
|
||||||
wg.Go(func() {
|
|
||||||
err2 = verifier.SidecarProposerExpected(ctx)
|
|
||||||
})
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
require.Equal(t, true, verifier.results.executed(RequireSidecarProposerExpected))
|
|
||||||
|
|
||||||
if len(tc.error) > 0 {
|
|
||||||
require.ErrorContains(t, tc.error, err1)
|
|
||||||
require.ErrorContains(t, tc.error, err2)
|
|
||||||
require.NotNil(t, verifier.results.result(RequireSidecarProposerExpected))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
require.NoError(t, err1)
|
|
||||||
require.NoError(t, err2)
|
|
||||||
require.NoError(t, verifier.results.result(RequireSidecarProposerExpected))
|
|
||||||
|
|
||||||
err := verifier.SidecarProposerExpected(ctx)
|
err := verifier.SidecarProposerExpected(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, true, verifier.results.executed(RequireSidecarProposerExpected))
|
||||||
|
require.NoError(t, verifier.results.result(RequireSidecarProposerExpected))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("Proposer does not match", func(t *testing.T) {
|
||||||
|
initializer := Initializer{
|
||||||
|
shared: &sharedResources{
|
||||||
|
sr: sbrReturnsState(fuluState),
|
||||||
|
hsp: &mockHeadStateProvider{
|
||||||
|
headRoot: parentRoot[:],
|
||||||
|
headSlot: columnSlot, // Same epoch so HeadStateReadOnly is used
|
||||||
|
headStateReadOnly: fuluState,
|
||||||
|
},
|
||||||
|
fc: &mockForkchoicer{},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
verifier := initializer.NewDataColumnsVerifier(wrongColumns, GossipDataColumnSidecarRequirements)
|
||||||
|
err := verifier.SidecarProposerExpected(ctx)
|
||||||
|
require.ErrorContains(t, errSidecarUnexpectedProposer.Error(), err)
|
||||||
|
require.Equal(t, true, verifier.results.executed(RequireSidecarProposerExpected))
|
||||||
|
require.NotNil(t, verifier.results.result(RequireSidecarProposerExpected))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("State lookup failure", func(t *testing.T) {
|
||||||
|
columns := GenerateTestDataColumns(t, parentRoot, columnSlot, blobCount)
|
||||||
|
initializer := Initializer{
|
||||||
|
shared: &sharedResources{
|
||||||
|
sr: sbrNotFound(t, columns[0].ParentRoot()),
|
||||||
|
hsp: &mockHeadStateProvider{},
|
||||||
|
fc: &mockForkchoicer{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
verifier := initializer.NewDataColumnsVerifier(columns, GossipDataColumnSidecarRequirements)
|
||||||
|
err := verifier.SidecarProposerExpected(ctx)
|
||||||
|
require.ErrorContains(t, "verifying state", err)
|
||||||
|
require.Equal(t, true, verifier.results.executed(RequireSidecarProposerExpected))
|
||||||
|
require.NotNil(t, verifier.results.result(RequireSidecarProposerExpected))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateTestDataColumnsWithProposer(t *testing.T, parent [fieldparams.RootLength]byte, slot primitives.Slot, blobCount int, proposer primitives.ValidatorIndex) []blocks.RODataColumn {
|
||||||
|
roBlock, roBlobs := util.GenerateTestDenebBlockWithSidecar(t, parent, slot, blobCount, util.WithProposer(proposer))
|
||||||
|
blobs := make([]kzg.Blob, 0, len(roBlobs))
|
||||||
|
for i := range roBlobs {
|
||||||
|
blobs = append(blobs, kzg.Blob(roBlobs[i].Blob))
|
||||||
|
}
|
||||||
|
|
||||||
|
cellsPerBlob, proofsPerBlob := util.GenerateCellsAndProofs(t, blobs)
|
||||||
|
roDataColumnSidecars, err := peerdas.DataColumnSidecars(cellsPerBlob, proofsPerBlob, peerdas.PopulateFromBlock(roBlock))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return roDataColumnSidecars
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestColumnRequirementSatisfaction(t *testing.T) {
|
func TestColumnRequirementSatisfaction(t *testing.T) {
|
||||||
@@ -922,12 +924,3 @@ func TestColumnRequirementSatisfaction(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConcatRootSlot(t *testing.T) {
|
|
||||||
root := [fieldparams.RootLength]byte{1, 2, 3}
|
|
||||||
const slot = primitives.Slot(3210)
|
|
||||||
|
|
||||||
const expected = "\x01\x02\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x003210"
|
|
||||||
|
|
||||||
actual := concatRootSlot(root, slot)
|
|
||||||
require.Equal(t, expected, actual)
|
|
||||||
}
|
|
||||||
|
|||||||
2
changelog/potuz_dcs_pc_removal.md
Normal file
2
changelog/potuz_dcs_pc_removal.md
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
### Changed
|
||||||
|
- Use lookahead to validate data column sidecar proposer index.
|
||||||
@@ -44,6 +44,13 @@ func WithProposerSigning(idx primitives.ValidatorIndex, sk bls.SecretKey, valRoo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithProposer sets the proposer index for the generated block without signing.
|
||||||
|
func WithProposer(idx primitives.ValidatorIndex) DenebBlockGeneratorOption {
|
||||||
|
return func(g *denebBlockGenerator) {
|
||||||
|
g.proposer = idx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func WithPayloadSetter(p *enginev1.ExecutionPayloadDeneb) DenebBlockGeneratorOption {
|
func WithPayloadSetter(p *enginev1.ExecutionPayloadDeneb) DenebBlockGeneratorOption {
|
||||||
return func(g *denebBlockGenerator) {
|
return func(g *denebBlockGenerator) {
|
||||||
g.payload = p
|
g.payload = p
|
||||||
|
|||||||
Reference in New Issue
Block a user