Files
prysm/beacon-chain/verification/data_column_test.go
Potuz 6f90101364 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>
2026-01-02 17:01:53 +00:00

927 lines
27 KiB
Go

package verification
import (
"reflect"
"testing"
"time"
"github.com/OffchainLabs/prysm/v7/beacon-chain/blockchain/kzg"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/peerdas"
forkchoicetypes "github.com/OffchainLabs/prysm/v7/beacon-chain/forkchoice/types"
"github.com/OffchainLabs/prysm/v7/beacon-chain/startup"
fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams"
"github.com/OffchainLabs/prysm/v7/config/params"
"github.com/OffchainLabs/prysm/v7/consensus-types/blocks"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
"github.com/OffchainLabs/prysm/v7/testing/require"
"github.com/OffchainLabs/prysm/v7/testing/util"
"github.com/OffchainLabs/prysm/v7/time/slots"
"github.com/pkg/errors"
)
func GenerateTestDataColumns(t *testing.T, parent [fieldparams.RootLength]byte, slot primitives.Slot, blobCount int) []blocks.RODataColumn {
roBlock, roBlobs := util.GenerateTestDenebBlockWithSidecar(t, parent, slot, blobCount)
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 TestColumnSatisfyRequirement(t *testing.T) {
const (
columnSlot = 1
blobCount = 1
)
parentRoot := [fieldparams.RootLength]byte{}
columns := GenerateTestDataColumns(t, parentRoot, columnSlot, blobCount)
intializer := Initializer{}
v := intializer.NewDataColumnsVerifier(columns, GossipDataColumnSidecarRequirements)
require.Equal(t, false, v.results.executed(RequireValidProposerSignature))
v.SatisfyRequirement(RequireValidProposerSignature)
require.Equal(t, true, v.results.executed(RequireValidProposerSignature))
}
func TestValid(t *testing.T) {
var initializer Initializer
t.Run("one invalid column", func(t *testing.T) {
columns := GenerateTestDataColumns(t, [fieldparams.RootLength]byte{}, 1, 1)
verifier := initializer.NewDataColumnsVerifier(columns, GossipDataColumnSidecarRequirements)
err := verifier.ValidFields()
require.NotNil(t, err)
require.NotNil(t, verifier.results.result(RequireValidFields))
})
t.Run("nominal", func(t *testing.T) {
const maxBlobsPerBlock = 2
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig()
cfg.FuluForkEpoch = 0
cfg.BlobSchedule = []params.BlobScheduleEntry{{Epoch: 0, MaxBlobsPerBlock: maxBlobsPerBlock}}
params.OverrideBeaconConfig(cfg)
columns := GenerateTestDataColumns(t, [fieldparams.RootLength]byte{}, 1, 1)
verifier := initializer.NewDataColumnsVerifier(columns, GossipDataColumnSidecarRequirements)
err := verifier.ValidFields()
require.NoError(t, err)
require.IsNil(t, verifier.results.result(RequireValidFields))
err = verifier.ValidFields()
require.NoError(t, err)
})
}
func TestCorrectSubnet(t *testing.T) {
const dataColumnSidecarSubTopic = "/data_column_sidecar_%d/"
var initializer Initializer
t.Run("lengths mismatch", func(t *testing.T) {
columns := GenerateTestDataColumns(t, [fieldparams.RootLength]byte{}, 1, 1)
verifier := initializer.NewDataColumnsVerifier(columns, GossipDataColumnSidecarRequirements)
err := verifier.CorrectSubnet(dataColumnSidecarSubTopic, []string{})
require.ErrorIs(t, err, errBadTopicLength)
require.NotNil(t, verifier.results.result(RequireCorrectSubnet))
})
t.Run("wrong topic", func(t *testing.T) {
columns := GenerateTestDataColumns(t, [fieldparams.RootLength]byte{}, 1, 1)
verifier := initializer.NewDataColumnsVerifier(columns[:2], GossipDataColumnSidecarRequirements)
err := verifier.CorrectSubnet(
dataColumnSidecarSubTopic,
[]string{
"/eth2/9dc47cc6/data_column_sidecar_1/ssz_snappy",
"/eth2/9dc47cc6/data_column_sidecar_0/ssz_snappy",
})
require.ErrorIs(t, err, errBadTopic)
require.NotNil(t, verifier.results.result(RequireCorrectSubnet))
})
t.Run("nominal", func(t *testing.T) {
subnets := []string{
"/eth2/9dc47cc6/data_column_sidecar_0/ssz_snappy",
"/eth2/9dc47cc6/data_column_sidecar_1",
}
columns := GenerateTestDataColumns(t, [fieldparams.RootLength]byte{}, 1, 1)
verifier := initializer.NewDataColumnsVerifier(columns[:2], GossipDataColumnSidecarRequirements)
err := verifier.CorrectSubnet(dataColumnSidecarSubTopic, subnets)
require.NoError(t, err)
require.IsNil(t, verifier.results.result(RequireCorrectSubnet))
err = verifier.CorrectSubnet(dataColumnSidecarSubTopic, subnets)
require.NoError(t, err)
})
}
func TestNotFromFutureSlot(t *testing.T) {
maximumGossipClockDisparity := params.BeaconConfig().MaximumGossipClockDisparityDuration()
testCases := []struct {
name string
currentSlot, columnSlot primitives.Slot
timeBeforeCurrentSlot time.Duration
isError bool
}{
{
name: "column slot == current slot",
currentSlot: 42,
columnSlot: 42,
timeBeforeCurrentSlot: 0,
isError: false,
},
{
name: "within maximum gossip clock disparity",
currentSlot: 42,
columnSlot: 42,
timeBeforeCurrentSlot: maximumGossipClockDisparity / 2,
isError: false,
},
{
name: "outside maximum gossip clock disparity",
currentSlot: 42,
columnSlot: 42,
timeBeforeCurrentSlot: maximumGossipClockDisparity * 2,
isError: true,
},
{
name: "too far in the future",
currentSlot: 10,
columnSlot: 42,
timeBeforeCurrentSlot: 0,
isError: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
const blobCount = 1
now := time.Now()
secondsPerSlot := time.Duration(params.BeaconConfig().SecondsPerSlot)
genesis := now.Add(-time.Duration(tc.currentSlot) * secondsPerSlot * time.Second)
clock := startup.NewClock(
genesis,
[fieldparams.RootLength]byte{},
startup.WithNower(func() time.Time {
return now.Add(-tc.timeBeforeCurrentSlot)
}),
)
parentRoot := [fieldparams.RootLength]byte{}
initializer := Initializer{shared: &sharedResources{clock: clock}}
columns := GenerateTestDataColumns(t, parentRoot, tc.columnSlot, blobCount)
verifier := initializer.NewDataColumnsVerifier(columns, GossipDataColumnSidecarRequirements)
err := verifier.NotFromFutureSlot()
require.Equal(t, true, verifier.results.executed(RequireNotFromFutureSlot))
if tc.isError {
require.ErrorIs(t, err, errFromFutureSlot)
require.NotNil(t, verifier.results.result(RequireNotFromFutureSlot))
return
}
require.NoError(t, err)
require.NoError(t, verifier.results.result(RequireNotFromFutureSlot))
err = verifier.NotFromFutureSlot()
require.NoError(t, err)
})
}
}
func TestColumnSlotAboveFinalized(t *testing.T) {
testCases := []struct {
name string
finalizedSlot, columnSlot primitives.Slot
isErr bool
}{
{
name: "finalized epoch < column epoch",
finalizedSlot: 10,
columnSlot: 96,
isErr: false,
},
{
name: "finalized slot < column slot (same epoch)",
finalizedSlot: 32,
columnSlot: 33,
isErr: false,
},
{
name: "finalized slot == column slot",
finalizedSlot: 64,
columnSlot: 64,
isErr: true,
},
{
name: "finalized epoch > column epoch",
finalizedSlot: 32,
columnSlot: 31,
isErr: true,
},
}
for _, tc := range testCases {
const blobCount = 1
t.Run(tc.name, func(t *testing.T) {
finalizedCheckpoint := func() *forkchoicetypes.Checkpoint {
return &forkchoicetypes.Checkpoint{
Epoch: slots.ToEpoch(tc.finalizedSlot),
Root: [fieldparams.RootLength]byte{},
}
}
parentRoot := [fieldparams.RootLength]byte{}
initializer := &Initializer{shared: &sharedResources{
fc: &mockForkchoicer{FinalizedCheckpointCB: finalizedCheckpoint},
}}
columns := GenerateTestDataColumns(t, parentRoot, tc.columnSlot, blobCount)
v := initializer.NewDataColumnsVerifier(columns, GossipDataColumnSidecarRequirements)
err := v.SlotAboveFinalized()
require.Equal(t, true, v.results.executed(RequireSlotAboveFinalized))
if tc.isErr {
require.ErrorIs(t, err, errSlotNotAfterFinalized)
require.NotNil(t, v.results.result(RequireSlotAboveFinalized))
return
}
require.NoError(t, err)
require.NoError(t, v.results.result(RequireSlotAboveFinalized))
err = v.SlotAboveFinalized()
require.NoError(t, err)
})
}
}
func TestValidProposerSignature(t *testing.T) {
const (
columnSlot = 0
blobCount = 1
)
parentRoot := [fieldparams.RootLength]byte{}
validator := &ethpb.Validator{}
columns := GenerateTestDataColumns(t, parentRoot, columnSlot, blobCount)
firstColumn := columns[0]
// The signature data does not depend on the data column itself, so we can use the first one.
expectedSignatureData := columnToSignatureData(firstColumn)
testCases := []struct {
isError bool
vscbShouldError bool
svcbReturn bool
stateByRooter StateByRooter
vscbError error
svcbError error
name string
}{
{
name: "cache hit - success",
svcbReturn: true,
svcbError: nil,
vscbShouldError: true,
vscbError: nil,
stateByRooter: &mockStateByRooter{sbr: sbrErrorIfCalled(t)},
isError: false,
},
{
name: "cache hit - error",
svcbReturn: true,
svcbError: errors.New("derp"),
vscbShouldError: true,
vscbError: nil,
stateByRooter: &mockStateByRooter{sbr: sbrErrorIfCalled(t)},
isError: true,
},
{
name: "cache miss - success",
svcbReturn: false,
svcbError: nil,
vscbShouldError: false,
vscbError: nil,
stateByRooter: sbrForValOverrideWithT(t, firstColumn.ProposerIndex(), validator),
isError: false,
},
{
name: "cache miss - state not found",
svcbReturn: false,
svcbError: nil,
vscbShouldError: false,
vscbError: nil,
stateByRooter: sbrNotFound(t, expectedSignatureData.Parent),
isError: true,
},
{
name: "cache miss - signature failure",
svcbReturn: false,
svcbError: nil,
vscbShouldError: false,
vscbError: errors.New("signature, not so good!"),
stateByRooter: sbrForValOverrideWithT(t, firstColumn.ProposerIndex(), validator),
isError: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
signatureCache := &mockSignatureCache{
svcb: func(signatureData signatureData) (bool, error) {
if signatureData != expectedSignatureData {
t.Error("Did not see expected SignatureData")
}
return tc.svcbReturn, tc.svcbError
},
vscb: func(signatureData signatureData, _ validatorAtIndexer) (err error) {
if tc.vscbShouldError {
t.Error("VerifySignature should not be called if the result is cached")
return nil
}
if expectedSignatureData != signatureData {
t.Error("unexpected signature data")
}
return tc.vscbError
},
}
initializer := Initializer{
shared: &sharedResources{
sc: signatureCache,
sr: tc.stateByRooter,
hsp: &mockHeadStateProvider{},
fc: &mockForkchoicer{
TargetRootForEpochCB: fcReturnsTargetRoot([fieldparams.RootLength]byte{}),
},
},
}
verifier := initializer.NewDataColumnsVerifier(columns, GossipDataColumnSidecarRequirements)
err := verifier.ValidProposerSignature(t.Context())
require.Equal(t, true, verifier.results.executed(RequireValidProposerSignature))
if tc.isError {
require.NotNil(t, err)
require.NotNil(t, verifier.results.result(RequireValidProposerSignature))
return
}
require.NoError(t, err)
require.NoError(t, verifier.results.result(RequireValidProposerSignature))
err = verifier.ValidProposerSignature(t.Context())
require.NoError(t, err)
})
}
}
func TestDataColumnsSidecarParentSeen(t *testing.T) {
const (
columnSlot = 0
blobCount = 1
)
parentRoot := [fieldparams.RootLength]byte{}
columns := GenerateTestDataColumns(t, parentRoot, columnSlot, blobCount)
firstColumn := columns[0]
fcHas := &mockForkchoicer{
HasNodeCB: func(parent [fieldparams.RootLength]byte) bool {
if parent != firstColumn.ParentRoot() {
t.Error("forkchoice.HasNode called with unexpected parent root")
}
return true
},
}
fcLacks := &mockForkchoicer{
HasNodeCB: func(parent [fieldparams.RootLength]byte) bool {
if parent != firstColumn.ParentRoot() {
t.Error("forkchoice.HasNode called with unexpected parent root")
}
return false
},
}
testCases := []struct {
name string
forkChoicer Forkchoicer
parentSeen func([fieldparams.RootLength]byte) bool
isError bool
}{
{
name: "happy path",
forkChoicer: fcHas,
parentSeen: nil,
isError: false,
},
{
name: "HasNode false, no badParent cb, expected error",
forkChoicer: fcLacks,
parentSeen: nil,
isError: true,
},
{
name: "HasNode false, badParent true",
forkChoicer: fcLacks,
parentSeen: badParentCb(t, firstColumn.ParentRoot(), true),
isError: false,
},
{
name: "HasNode false, badParent false",
forkChoicer: fcLacks,
parentSeen: badParentCb(t, firstColumn.ParentRoot(), false),
isError: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
initializer := Initializer{shared: &sharedResources{fc: tc.forkChoicer}}
verifier := initializer.NewDataColumnsVerifier(columns, GossipDataColumnSidecarRequirements)
err := verifier.SidecarParentSeen(tc.parentSeen)
require.Equal(t, true, verifier.results.executed(RequireSidecarParentSeen))
if tc.isError {
require.ErrorIs(t, err, errSidecarParentNotSeen)
require.NotNil(t, verifier.results.result(RequireSidecarParentSeen))
return
}
require.NoError(t, err)
require.NoError(t, verifier.results.result(RequireSidecarParentSeen))
err = verifier.SidecarParentSeen(tc.parentSeen)
require.NoError(t, err)
})
}
}
func TestDataColumnsSidecarParentValid(t *testing.T) {
testCases := []struct {
name string
badParentCbReturn bool
isError bool
}{
{
name: "parent valid",
badParentCbReturn: false,
isError: false,
},
{
name: "parent not valid",
badParentCbReturn: true,
isError: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
const (
columnSlot = 0
blobCount = 1
)
parentRoot := [fieldparams.RootLength]byte{}
columns := GenerateTestDataColumns(t, parentRoot, columnSlot, blobCount)
firstColumn := columns[0]
initializer := Initializer{shared: &sharedResources{}}
verifier := initializer.NewDataColumnsVerifier(columns, GossipDataColumnSidecarRequirements)
err := verifier.SidecarParentValid(badParentCb(t, firstColumn.ParentRoot(), tc.badParentCbReturn))
require.Equal(t, true, verifier.results.executed(RequireSidecarParentValid))
if tc.isError {
require.ErrorIs(t, err, errSidecarParentInvalid)
require.NotNil(t, verifier.results.result(RequireSidecarParentValid))
return
}
require.NoError(t, err)
require.NoError(t, verifier.results.result(RequireSidecarParentValid))
err = verifier.SidecarParentValid(badParentCb(t, firstColumn.ParentRoot(), tc.badParentCbReturn))
require.NoError(t, err)
})
}
}
func TestColumnSidecarParentSlotLower(t *testing.T) {
columns := GenerateTestDataColumns(t, [32]byte{}, 1, 1)
firstColumn := columns[0]
cases := []struct {
name string
forkChoiceSlot primitives.Slot
forkChoiceError, err error
errCheckValue bool
}{
{
name: "Not in forkchoice",
forkChoiceError: errors.New("not in forkchoice"),
err: errSlotNotAfterParent,
},
{
name: "In forkchoice, slot lower",
forkChoiceSlot: firstColumn.Slot() - 1,
},
{
name: "In forkchoice, slot equal",
forkChoiceSlot: firstColumn.Slot(),
err: errSlotNotAfterParent,
errCheckValue: true,
},
{
name: "In forkchoice, slot higher",
forkChoiceSlot: firstColumn.Slot() + 1,
err: errSlotNotAfterParent,
errCheckValue: true,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
initializer := Initializer{
shared: &sharedResources{fc: &mockForkchoicer{
SlotCB: func(r [32]byte) (primitives.Slot, error) {
if firstColumn.ParentRoot() != r {
t.Error("forkchoice.Slot called with unexpected parent root")
}
return c.forkChoiceSlot, c.forkChoiceError
},
}},
}
verifier := initializer.NewDataColumnsVerifier(columns, GossipDataColumnSidecarRequirements)
err := verifier.SidecarParentSlotLower()
require.Equal(t, true, verifier.results.executed(RequireSidecarParentSlotLower))
if c.err == nil {
require.NoError(t, err)
require.NoError(t, verifier.results.result(RequireSidecarParentSlotLower))
err = verifier.SidecarParentSlotLower()
require.NoError(t, err)
return
}
require.NotNil(t, err)
require.NotNil(t, verifier.results.result(RequireSidecarParentSlotLower))
if c.errCheckValue {
require.ErrorIs(t, err, c.err)
}
})
}
}
func TestDataColumnsSidecarDescendsFromFinalized(t *testing.T) {
testCases := []struct {
name string
hasNodeCBReturn bool
isError bool
}{
{
name: "Not canonical",
hasNodeCBReturn: false,
isError: true,
},
{
name: "Canonical",
hasNodeCBReturn: true,
isError: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
const (
columnSlot = 0
blobCount = 1
)
parentRoot := [fieldparams.RootLength]byte{}
columns := GenerateTestDataColumns(t, parentRoot, columnSlot, blobCount)
firstColumn := columns[0]
initializer := Initializer{
shared: &sharedResources{
fc: &mockForkchoicer{
HasNodeCB: func(r [fieldparams.RootLength]byte) bool {
if firstColumn.ParentRoot() != r {
t.Error("forkchoice.Slot called with unexpected parent root")
}
return tc.hasNodeCBReturn
},
},
},
}
verifier := initializer.NewDataColumnsVerifier(columns, GossipDataColumnSidecarRequirements)
err := verifier.SidecarDescendsFromFinalized()
require.Equal(t, true, verifier.results.executed(RequireSidecarDescendsFromFinalized))
if tc.isError {
require.ErrorIs(t, err, errSidecarNotFinalizedDescendent)
require.NotNil(t, verifier.results.result(RequireSidecarDescendsFromFinalized))
return
}
require.NoError(t, err)
require.NoError(t, verifier.results.result(RequireSidecarDescendsFromFinalized))
err = verifier.SidecarDescendsFromFinalized()
require.NoError(t, err)
})
}
}
func TestDataColumnsSidecarInclusionProven(t *testing.T) {
testCases := []struct {
name string
alterate bool
isError bool
}{
{
name: "Inclusion proven",
alterate: false,
isError: false,
},
{
name: "Inclusion not proven",
alterate: true,
isError: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
const (
columnSlot = 0
blobCount = 1
)
parentRoot := [fieldparams.RootLength]byte{}
columns := GenerateTestDataColumns(t, parentRoot, columnSlot, blobCount)
if tc.alterate {
firstColumn := columns[0]
byte0 := firstColumn.SignedBlockHeader.Header.BodyRoot[0]
firstColumn.SignedBlockHeader.Header.BodyRoot[0] = byte0 ^ 255
}
initializer := Initializer{
shared: &sharedResources{ic: newInclusionProofCache(1)},
}
verifier := initializer.NewDataColumnsVerifier(columns, GossipDataColumnSidecarRequirements)
err := verifier.SidecarInclusionProven()
require.Equal(t, true, verifier.results.executed(RequireSidecarInclusionProven))
if tc.isError {
require.ErrorIs(t, err, ErrSidecarInclusionProofInvalid)
require.NotNil(t, verifier.results.result(RequireSidecarInclusionProven))
return
}
require.NoError(t, err)
require.NoError(t, verifier.results.result(RequireSidecarInclusionProven))
err = verifier.SidecarInclusionProven()
require.NoError(t, err)
})
}
}
func TestDataColumnsSidecarKzgProofVerified(t *testing.T) {
testCases := []struct {
isError bool
verifyDataColumnsCommitmentError error
name string
}{
{
name: "KZG proof verified",
verifyDataColumnsCommitmentError: nil,
isError: false,
},
{
name: "KZG proof not verified",
verifyDataColumnsCommitmentError: errors.New("KZG proof error"),
isError: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
const (
columnSlot = 0
blobCount = 1
)
parentRoot := [fieldparams.RootLength]byte{}
columns := GenerateTestDataColumns(t, parentRoot, columnSlot, blobCount)
firstColumn := columns[0]
verifyDataColumnsCommitment := func(roDataColumns []blocks.RODataColumn) error {
for _, roDataColumn := range roDataColumns {
require.Equal(t, true, reflect.DeepEqual(firstColumn.KzgCommitments, roDataColumn.KzgCommitments))
}
return tc.verifyDataColumnsCommitmentError
}
verifier := &RODataColumnsVerifier{
results: newResults(),
dataColumns: columns,
verifyDataColumnsCommitment: verifyDataColumnsCommitment,
}
err := verifier.SidecarKzgProofVerified()
require.Equal(t, true, verifier.results.executed(RequireSidecarKzgProofVerified))
if tc.isError {
require.NotNil(t, err)
require.NotNil(t, verifier.results.result(RequireSidecarKzgProofVerified))
return
}
require.NoError(t, err)
require.NoError(t, verifier.results.result(RequireSidecarKzgProofVerified))
err = verifier.SidecarKzgProofVerified()
require.NoError(t, err)
})
}
}
func TestDataColumnsSidecarProposerExpected(t *testing.T) {
const (
columnSlot = 1
blobCount = 1
)
ctx := t.Context()
parentRoot := [fieldparams.RootLength]byte{}
// Create a Fulu state to get the expected proposer from the lookahead.
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{
shared: &sharedResources{
sr: sbrReturnsState(fuluState),
hsp: &mockHeadStateProvider{
headRoot: parentRoot[:],
headSlot: columnSlot, // Same epoch so HeadStateReadOnly is used
headStateReadOnly: fuluState,
},
fc: &mockForkchoicer{},
},
}
verifier := initializer.NewDataColumnsVerifier(matchingColumns, GossipDataColumnSidecarRequirements)
err := verifier.SidecarProposerExpected(ctx)
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) {
const (
columnSlot = 1
blobCount = 1
)
parentRoot := [fieldparams.RootLength]byte{}
columns := GenerateTestDataColumns(t, parentRoot, columnSlot, blobCount)
initializer := Initializer{}
verifier := initializer.NewDataColumnsVerifier(columns, GossipDataColumnSidecarRequirements)
// We haven't performed any verification, VerifiedRODataColumns should error.
_, err := verifier.VerifiedRODataColumns()
require.ErrorIs(t, err, errColumnsInvalid)
var me VerificationMultiError
ok := errors.As(err, &me)
require.Equal(t, true, ok)
fails := me.Failures()
// We haven't performed any verification, so all the results should be this type.
for _, v := range fails {
require.ErrorIs(t, v, ErrMissingVerification)
}
// Satisfy everything but the first requirement through the backdoor.
for _, r := range GossipDataColumnSidecarRequirements[1:] {
verifier.results.record(r, nil)
}
// One requirement is missing, VerifiedRODataColumns should still error.
_, err = verifier.VerifiedRODataColumns()
require.ErrorIs(t, err, errColumnsInvalid)
// Now, satisfy the first requirement.
verifier.results.record(GossipDataColumnSidecarRequirements[0], nil)
// VerifiedRODataColumns should now succeed.
require.Equal(t, true, verifier.results.allSatisfied())
_, err = verifier.VerifiedRODataColumns()
require.NoError(t, err)
}