Save LC Finality and Optimistic Updates in LC Store When Processing Block (#15124)

* add lcStore to Node

* changelog entry

* add atomic getters and setters for the store

* add lcstore to the blockchain package

* save lc finality update to store

* save lc optimistic update to store

* changelog entry

* change store fields visibility to private

* refactor method names and add tests

* add lcstore to the blockchain package

* save lc finality update to store

* save lc optimistic update to store

* changelog entry

* refactor method names

* setup tests

* remove get from getters

* add lcstore to the blockchain package

* save lc finality update to store

* save lc optimistic update to store

* changelog entry

* refactor method names

* setup tests

* rename methods

* temp

* temp

* add tests

* fixing tests

* stash

* refactor setUpAltair

* remove debug code

* refactor bellatrix setup

* refactor capella setup

* refactor rest - core tests remain

* refactor tests to use new functional options utils

* use the options

* add noFinalizedCheckpoint and finalizedCheckpointInPrevFork options

* add tests

* changelog entry

* refactor tests

* deps

* fix tests

* Update testing/util/lightclient.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update testing/util/lightclient.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update testing/util/lightclient.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* address comments

* address comments

* address comments

* go mod tidy

* fix annoying conflicts

* go mod tidy

* fix conflicts

* cleanup tests

* add SetupTestConfigCleanup

* commit to restart CI checks

* address comments

* address comments

* address comments

---------

Co-authored-by: Radosław Kapka <rkapka@wp.pl>
This commit is contained in:
Bastin
2025-04-15 12:38:20 +02:00
committed by GitHub
parent 215dbb8e40
commit c9e8701987
9 changed files with 397 additions and 5 deletions

View File

@@ -3244,3 +3244,338 @@ func TestSaveLightClientBootstrap(t *testing.T) {
reset()
}
func setupLightClientTestRequirements(ctx context.Context, t *testing.T, s *Service, v int, options ...util.LightClientOption) (*util.TestLightClient, *postBlockProcessConfig) {
var l *util.TestLightClient
switch v {
case version.Altair:
l = util.NewTestLightClient(t, version.Altair, options...)
case version.Bellatrix:
l = util.NewTestLightClient(t, version.Bellatrix, options...)
case version.Capella:
l = util.NewTestLightClient(t, version.Capella, options...)
case version.Deneb:
l = util.NewTestLightClient(t, version.Deneb, options...)
case version.Electra:
l = util.NewTestLightClient(t, version.Electra, options...)
default:
t.Errorf("Unsupported fork version %s", version.String(v))
return nil, nil
}
err := s.cfg.BeaconDB.SaveBlock(ctx, l.AttestedBlock)
require.NoError(t, err)
attestedBlockRoot, err := l.AttestedBlock.Block().HashTreeRoot()
require.NoError(t, err)
err = s.cfg.BeaconDB.SaveState(ctx, l.AttestedState, attestedBlockRoot)
require.NoError(t, err)
currentBlockRoot, err := l.Block.Block().HashTreeRoot()
require.NoError(t, err)
roblock, err := consensusblocks.NewROBlockWithRoot(l.Block, currentBlockRoot)
require.NoError(t, err)
err = s.cfg.BeaconDB.SaveBlock(ctx, roblock)
require.NoError(t, err)
err = s.cfg.BeaconDB.SaveState(ctx, l.State, currentBlockRoot)
require.NoError(t, err)
err = s.cfg.BeaconDB.SaveBlock(ctx, l.FinalizedBlock)
require.NoError(t, err)
cfg := &postBlockProcessConfig{
ctx: ctx,
roblock: roblock,
postState: l.State,
isValidPayload: true,
}
return l, cfg
}
func TestProcessLightClientOptimisticUpdate(t *testing.T) {
featCfg := &features.Flags{}
featCfg.EnableLightClient = true
reset := features.InitWithReset(featCfg)
defer reset()
params.SetupTestConfigCleanup(t)
beaconCfg := params.BeaconConfig()
beaconCfg.AltairForkEpoch = 1
beaconCfg.BellatrixForkEpoch = 2
beaconCfg.CapellaForkEpoch = 3
beaconCfg.DenebForkEpoch = 4
beaconCfg.ElectraForkEpoch = 5
params.OverrideBeaconConfig(beaconCfg)
s, tr := minimalTestService(t)
ctx := tr.ctx
testCases := []struct {
name string
oldOptions []util.LightClientOption
newOptions []util.LightClientOption
expectReplace bool
}{
{
name: "No old update",
oldOptions: nil,
newOptions: []util.LightClientOption{},
expectReplace: true,
},
{
name: "Same age",
oldOptions: []util.LightClientOption{},
newOptions: []util.LightClientOption{util.WithSupermajority()}, // supermajority does not matter here and is only added to result in two different updates
expectReplace: false,
},
{
name: "Old update is better - age",
oldOptions: []util.LightClientOption{util.WithIncreasedAttestedSlot(1)},
newOptions: []util.LightClientOption{},
expectReplace: false,
},
{
name: "New update is better - age",
oldOptions: []util.LightClientOption{},
newOptions: []util.LightClientOption{util.WithIncreasedAttestedSlot(1)},
expectReplace: true,
},
}
for _, tc := range testCases {
for testVersion := 1; testVersion < 6; testVersion++ { // test all forks
var forkEpoch uint64
var expectedVersion int
switch testVersion {
case 1:
forkEpoch = uint64(params.BeaconConfig().AltairForkEpoch)
expectedVersion = version.Altair
case 2:
forkEpoch = uint64(params.BeaconConfig().BellatrixForkEpoch)
expectedVersion = version.Altair
case 3:
forkEpoch = uint64(params.BeaconConfig().CapellaForkEpoch)
expectedVersion = version.Capella
case 4:
forkEpoch = uint64(params.BeaconConfig().DenebForkEpoch)
expectedVersion = version.Deneb
case 5:
forkEpoch = uint64(params.BeaconConfig().ElectraForkEpoch)
expectedVersion = version.Deneb
default:
t.Errorf("Unsupported fork version %s", version.String(testVersion))
}
t.Run(version.String(testVersion)+"_"+tc.name, func(t *testing.T) {
s.genesisTime = time.Unix(time.Now().Unix()-(int64(forkEpoch)*int64(params.BeaconConfig().SlotsPerEpoch)*int64(params.BeaconConfig().SecondsPerSlot)), 0)
s.lcStore = &lightClient.Store{}
var oldActualUpdate interfaces.LightClientOptimisticUpdate
var err error
if tc.oldOptions != nil {
// config for old update
lOld, cfgOld := setupLightClientTestRequirements(ctx, t, s, testVersion, tc.oldOptions...)
require.NoError(t, s.processLightClientOptimisticUpdate(cfgOld.ctx, cfgOld.roblock, cfgOld.postState))
oldActualUpdate, err = lightClient.NewLightClientOptimisticUpdateFromBeaconState(
lOld.Ctx,
lOld.State.Slot(),
lOld.State,
lOld.Block,
lOld.AttestedState,
lOld.AttestedBlock,
)
require.NoError(t, err)
// check that the old update is saved
oldUpdate := s.lcStore.LastOptimisticUpdate()
require.NotNil(t, oldUpdate)
require.DeepEqual(t, oldUpdate, oldActualUpdate, "old update should be saved")
}
// config for new update
lNew, cfgNew := setupLightClientTestRequirements(ctx, t, s, testVersion, tc.newOptions...)
require.NoError(t, s.processLightClientOptimisticUpdate(cfgNew.ctx, cfgNew.roblock, cfgNew.postState))
newActualUpdate, err := lightClient.NewLightClientOptimisticUpdateFromBeaconState(
lNew.Ctx,
lNew.State.Slot(),
lNew.State,
lNew.Block,
lNew.AttestedState,
lNew.AttestedBlock,
)
require.NoError(t, err)
require.DeepNotEqual(t, newActualUpdate, oldActualUpdate, "new update should not be equal to old update")
// check that the new update is saved or skipped
newUpdate := s.lcStore.LastOptimisticUpdate()
require.NotNil(t, newUpdate)
if tc.expectReplace {
require.DeepEqual(t, newActualUpdate, newUpdate)
require.Equal(t, expectedVersion, newUpdate.Version())
} else {
require.DeepEqual(t, oldActualUpdate, newUpdate)
require.Equal(t, expectedVersion, newUpdate.Version())
}
})
}
}
}
func TestProcessLightClientFinalityUpdate(t *testing.T) {
featCfg := &features.Flags{}
featCfg.EnableLightClient = true
reset := features.InitWithReset(featCfg)
defer reset()
params.SetupTestConfigCleanup(t)
beaconCfg := params.BeaconConfig()
beaconCfg.AltairForkEpoch = 1
beaconCfg.BellatrixForkEpoch = 2
beaconCfg.CapellaForkEpoch = 3
beaconCfg.DenebForkEpoch = 4
beaconCfg.ElectraForkEpoch = 5
params.OverrideBeaconConfig(beaconCfg)
s, tr := minimalTestService(t)
ctx := tr.ctx
testCases := []struct {
name string
oldOptions []util.LightClientOption
newOptions []util.LightClientOption
expectReplace bool
}{
{
name: "No old update",
oldOptions: nil,
newOptions: []util.LightClientOption{},
expectReplace: true,
},
{
name: "Old update is better - age - no supermajority",
oldOptions: []util.LightClientOption{util.WithIncreasedFinalizedSlot(1)},
newOptions: []util.LightClientOption{},
expectReplace: false,
},
{
name: "Old update is better - age - both supermajority",
oldOptions: []util.LightClientOption{util.WithIncreasedFinalizedSlot(1), util.WithSupermajority()},
newOptions: []util.LightClientOption{util.WithSupermajority()},
expectReplace: false,
},
{
name: "Old update is better - supermajority",
oldOptions: []util.LightClientOption{util.WithSupermajority()},
newOptions: []util.LightClientOption{},
expectReplace: false,
},
{
name: "New update is better - age - both supermajority",
oldOptions: []util.LightClientOption{util.WithSupermajority()},
newOptions: []util.LightClientOption{util.WithIncreasedFinalizedSlot(1), util.WithSupermajority()},
expectReplace: true,
},
{
name: "New update is better - age - no supermajority",
oldOptions: []util.LightClientOption{},
newOptions: []util.LightClientOption{util.WithIncreasedFinalizedSlot(1)},
expectReplace: true,
},
{
name: "New update is better - supermajority",
oldOptions: []util.LightClientOption{},
newOptions: []util.LightClientOption{util.WithSupermajority()},
expectReplace: true,
},
}
for _, tc := range testCases {
for testVersion := 1; testVersion < 6; testVersion++ { // test all forks
var forkEpoch uint64
var expectedVersion int
switch testVersion {
case 1:
forkEpoch = uint64(params.BeaconConfig().AltairForkEpoch)
expectedVersion = version.Altair
case 2:
forkEpoch = uint64(params.BeaconConfig().BellatrixForkEpoch)
expectedVersion = version.Altair
case 3:
forkEpoch = uint64(params.BeaconConfig().CapellaForkEpoch)
expectedVersion = version.Capella
case 4:
forkEpoch = uint64(params.BeaconConfig().DenebForkEpoch)
expectedVersion = version.Deneb
case 5:
forkEpoch = uint64(params.BeaconConfig().ElectraForkEpoch)
expectedVersion = version.Electra
default:
t.Errorf("Unsupported fork version %s", version.String(testVersion))
}
t.Run(version.String(testVersion)+"_"+tc.name, func(t *testing.T) {
s.genesisTime = time.Unix(time.Now().Unix()-(int64(forkEpoch)*int64(params.BeaconConfig().SlotsPerEpoch)*int64(params.BeaconConfig().SecondsPerSlot)), 0)
s.lcStore = &lightClient.Store{}
var actualOldUpdate, actualNewUpdate interfaces.LightClientFinalityUpdate
var err error
if tc.oldOptions != nil {
// config for old update
lOld, cfgOld := setupLightClientTestRequirements(ctx, t, s, testVersion, tc.oldOptions...)
require.NoError(t, s.processLightClientFinalityUpdate(cfgOld.ctx, cfgOld.roblock, cfgOld.postState))
// check that the old update is saved
actualOldUpdate, err = lightClient.NewLightClientFinalityUpdateFromBeaconState(
ctx,
cfgOld.postState.Slot(),
cfgOld.postState,
cfgOld.roblock,
lOld.AttestedState,
lOld.AttestedBlock,
lOld.FinalizedBlock,
)
require.NoError(t, err)
oldUpdate := s.lcStore.LastFinalityUpdate()
require.DeepEqual(t, actualOldUpdate, oldUpdate)
}
// config for new update
lNew, cfgNew := setupLightClientTestRequirements(ctx, t, s, testVersion, tc.newOptions...)
require.NoError(t, s.processLightClientFinalityUpdate(cfgNew.ctx, cfgNew.roblock, cfgNew.postState))
// check that the actual old update and the actual new update are different
actualNewUpdate, err = lightClient.NewLightClientFinalityUpdateFromBeaconState(
ctx,
cfgNew.postState.Slot(),
cfgNew.postState,
cfgNew.roblock,
lNew.AttestedState,
lNew.AttestedBlock,
lNew.FinalizedBlock,
)
require.NoError(t, err)
require.DeepNotEqual(t, actualOldUpdate, actualNewUpdate)
// check that the new update is saved or skipped
newUpdate := s.lcStore.LastFinalityUpdate()
if tc.expectReplace {
require.DeepEqual(t, actualNewUpdate, newUpdate)
require.Equal(t, expectedVersion, newUpdate.Version())
} else {
require.DeepEqual(t, actualOldUpdate, newUpdate)
require.Equal(t, expectedVersion, newUpdate.Version())
}
})
}
}
}