mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-01-06 22:23:56 -05:00
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>
927 lines
27 KiB
Go
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 := ðpb.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)
|
|
}
|
|
|