Compare commits

...

9 Commits

Author SHA1 Message Date
satushh
2018b647ea bazel run //:gazelle -- fix 2025-10-24 16:27:57 +01:00
satushh
0352e39f64 remove unwanted comment 2025-10-24 16:23:36 +01:00
satushh
82847da8a7 Merge branch 'develop' into plus-one-bug 2025-10-24 16:19:45 +01:00
satushh
accfdd0f7c lint 2025-10-24 16:19:06 +01:00
satushh
2b51e9e350 updated changelog 2025-10-24 16:12:56 +01:00
satushh
e05fec0f5f corrected test and extra test for epoch aligned pruning 2025-10-24 16:12:14 +01:00
satushh
1a904dbae3 epoch-aligned pruning to keep complete epochs rather than fractional epochs 2025-10-24 13:01:11 +01:00
satushh
37417e5905 changelog 2025-10-23 11:18:27 +01:00
satushh
7a5f4cf122 correct defaultRetentionEpochs 2025-10-22 19:58:29 +01:00
4 changed files with 177 additions and 10 deletions

View File

@@ -31,6 +31,7 @@ go_test(
"//testing/assert:go_default_library",
"//testing/require:go_default_library",
"//testing/util:go_default_library",
"//time/slots:go_default_library",
"//time/slots/testing:go_default_library",
"@com_github_sirupsen_logrus//:go_default_library",
"@com_github_sirupsen_logrus//hooks/test:go_default_library",

View File

@@ -36,7 +36,7 @@ type ServiceOption func(*Service)
// The retention period is specified in epochs, and must be >= MIN_EPOCHS_FOR_BLOCK_REQUESTS.
func WithRetentionPeriod(retentionEpochs primitives.Epoch) ServiceOption {
return func(s *Service) {
defaultRetentionEpochs := primitives.Epoch(params.BeaconConfig().MinEpochsForBlockRequests) + 1
defaultRetentionEpochs := primitives.Epoch(params.BeaconConfig().MinEpochsForBlockRequests)
if retentionEpochs < defaultRetentionEpochs {
log.WithField("userEpochs", retentionEpochs).
WithField("minRequired", defaultRetentionEpochs).
@@ -75,7 +75,7 @@ func New(ctx context.Context, db iface.Database, genesisTime time.Time, initSync
p := &Service{
ctx: ctx,
db: db,
ps: pruneStartSlotFunc(primitives.Epoch(params.BeaconConfig().MinEpochsForBlockRequests) + 1), // Default retention epochs is MIN_EPOCHS_FOR_BLOCK_REQUESTS + 1 from the current slot.
ps: pruneStartSlotFunc(primitives.Epoch(params.BeaconConfig().MinEpochsForBlockRequests)),
done: make(chan struct{}),
slotTicker: slots.NewSlotTicker(slots.UnsafeStartTime(genesisTime, 0), params.BeaconConfig().SecondsPerSlot),
initSyncWaiter: initSyncWaiter,
@@ -239,15 +239,38 @@ func (p *Service) pruneBatches(pruneUpto primitives.Slot) (int, error) {
}
// pruneStartSlotFunc returns the function to determine the start slot to start pruning.
// The pruning calculation is epoch-aligned,
// ensuring that earliestAvailableSlot is always at an epoch boundary.
// So that we prune epoch-wise.
// e.g. if retentionEpochs is 3 i.e. we should keep at least 3 epochs from current slot,
//
// current slot is 325 (=> current epoch is 10),
// then we should keep epoch 7 onwards (inclusive of epoch 7).
// So we can prune up to the last slot of 6th epoch i.e. 32 x 7 - 1 = 223
// Earliest available slot would be 224 in that case.
func pruneStartSlotFunc(retentionEpochs primitives.Epoch) func(primitives.Slot) primitives.Slot {
return func(current primitives.Slot) primitives.Slot {
if retentionEpochs > slots.MaxSafeEpoch() {
retentionEpochs = slots.MaxSafeEpoch()
}
offset := slots.UnsafeEpochStart(retentionEpochs)
if offset >= current {
// Calculate epoch-aligned minimum required slot
currentEpoch := slots.ToEpoch(current)
var minRequiredEpoch primitives.Epoch
if currentEpoch > retentionEpochs {
minRequiredEpoch = currentEpoch - retentionEpochs
} else {
minRequiredEpoch = 0
}
// Get the start slot of the minimum required epoch
minRequiredSlot, err := slots.EpochStart(minRequiredEpoch)
if err != nil || minRequiredSlot == 0 {
return 0
}
return current - offset
// Prune up to (but not including) the minimum required slot
// This ensures earliestAvailableSlot (pruneUpto + 1) is at the epoch boundary
return minRequiredSlot - 1
}
}

View File

@@ -11,6 +11,7 @@ import (
eth "github.com/OffchainLabs/prysm/v6/proto/prysm/v1alpha1"
"github.com/OffchainLabs/prysm/v6/testing/util"
"github.com/OffchainLabs/prysm/v6/time/slots"
slottest "github.com/OffchainLabs/prysm/v6/time/slots/testing"
"github.com/sirupsen/logrus"
@@ -247,12 +248,23 @@ func TestWithRetentionPeriod_EnforcesMinimum(t *testing.T) {
ctx := t.Context()
beaconDB := dbtest.SetupDB(t)
// Get the minimum required epochs (272 + 1 = 273 for minimal)
minRequiredEpochs := primitives.Epoch(params.BeaconConfig().MinEpochsForBlockRequests + 1)
// Get the minimum required epochs (272 for minimal)
minRequiredEpochs := primitives.Epoch(params.BeaconConfig().MinEpochsForBlockRequests)
// Use a slot that's guaranteed to be after the minimum retention period
currentSlot := primitives.Slot(minRequiredEpochs+100) * (params.BeaconConfig().SlotsPerEpoch)
// Calculate epoch-aligned expected prune slot
// For epoch-aligned pruning: pruneUpto = epochStart(currentEpoch - retention) - 1
currentEpoch := slots.ToEpoch(currentSlot)
// Helper function to calculate expected prune slot for a given retention
calcExpectedPruneSlot := func(retention primitives.Epoch) primitives.Slot {
minEpoch := currentEpoch - retention
minSlot, _ := slots.EpochStart(minEpoch)
return minSlot - 1
}
tests := []struct {
name string
userRetentionEpochs primitives.Epoch
@@ -262,19 +274,19 @@ func TestWithRetentionPeriod_EnforcesMinimum(t *testing.T) {
{
name: "User value below minimum - should use minimum",
userRetentionEpochs: 2, // Way below minimum
expectedPruneSlot: currentSlot - primitives.Slot(minRequiredEpochs)*params.BeaconConfig().SlotsPerEpoch,
expectedPruneSlot: calcExpectedPruneSlot(minRequiredEpochs),
description: "Should use minimum when user value is too low",
},
{
name: "User value at minimum",
userRetentionEpochs: minRequiredEpochs,
expectedPruneSlot: currentSlot - primitives.Slot(minRequiredEpochs)*params.BeaconConfig().SlotsPerEpoch,
expectedPruneSlot: calcExpectedPruneSlot(minRequiredEpochs),
description: "Should use user value when at minimum",
},
{
name: "User value above minimum",
userRetentionEpochs: minRequiredEpochs + 10,
expectedPruneSlot: currentSlot - primitives.Slot(minRequiredEpochs+10)*params.BeaconConfig().SlotsPerEpoch,
expectedPruneSlot: calcExpectedPruneSlot(minRequiredEpochs + 10),
description: "Should use user value when above minimum",
},
}
@@ -311,6 +323,133 @@ func TestWithRetentionPeriod_EnforcesMinimum(t *testing.T) {
}
}
func TestWithRetentionPeriod_AcceptsSpecMinimum(t *testing.T) {
params.SetupTestConfigCleanup(t)
config := params.MinimalSpecConfig()
params.OverrideBeaconConfig(config)
ctx := t.Context()
beaconDB := dbtest.SetupDB(t)
hook := logTest.NewGlobal()
logrus.SetLevel(logrus.WarnLevel)
// The spec minimum - this SHOULD be accepted without warning
specMinimum := primitives.Epoch(params.BeaconConfig().MinEpochsForBlockRequests)
// Use a slot that's guaranteed to be after the minimum retention period
currentSlot := primitives.Slot(specMinimum+100) * (params.BeaconConfig().SlotsPerEpoch)
mockCustody := &mockCustodyUpdater{}
p, err := New(
ctx,
beaconDB,
time.Now(),
nil,
nil,
mockCustody,
WithRetentionPeriod(specMinimum),
)
require.NoError(t, err)
// Test the pruning calculation
pruneUptoSlot := p.ps(currentSlot)
// The expected prune slot should use epoch-aligned calculation
// pruneUpto = epochStart(currentEpoch - retention) - 1
currentEpoch := slots.ToEpoch(currentSlot)
minRequiredEpoch := currentEpoch - specMinimum
minRequiredSlot, err := slots.EpochStart(minRequiredEpoch)
require.NoError(t, err)
expectedPruneSlot := minRequiredSlot - 1
assert.Equal(t, expectedPruneSlot, pruneUptoSlot,
"Pruner should accept and use MIN_EPOCHS_FOR_BLOCK_REQUESTS without adding 1")
for _, entry := range hook.AllEntries() {
if entry.Level == logrus.WarnLevel {
t.Errorf("Unexpected warning when using spec minimum: %s", entry.Message)
}
}
}
func TestPruneStartSlotFunc_EpochAlignment(t *testing.T) {
// This test verifies that the pruning calculation is epoch-aligned.
params.SetupTestConfigCleanup(t)
config := params.MinimalSpecConfig()
params.OverrideBeaconConfig(config)
slotsPerEpoch := params.BeaconConfig().SlotsPerEpoch // 8 for minimal config
retentionEpochs := primitives.Epoch(3)
tests := []struct {
name string
currentSlot primitives.Slot
expectedEpochAlignment bool
expectedMinRequiredSlot primitives.Slot
description string
}{
{
name: "Pruning at epoch boundary",
currentSlot: primitives.Slot(4 * slotsPerEpoch), // Slot 32 (epoch 4, slot 0 of epoch)
expectedEpochAlignment: true,
expectedMinRequiredSlot: primitives.Slot(1 * slotsPerEpoch), // Epoch 1 start = slot 8
description: "When pruning at epoch boundary, earliestAvailableSlot should be at epoch boundary",
},
{
name: "Pruning at middle of epoch",
currentSlot: primitives.Slot(4*slotsPerEpoch + 4), // Slot 36 (epoch 4, slot 4 of epoch)
expectedEpochAlignment: true,
expectedMinRequiredSlot: primitives.Slot(1 * slotsPerEpoch), // Epoch 1 start = slot 8
description: "When pruning mid-epoch, earliestAvailableSlot must still be at epoch boundary",
},
{
name: "Pruning at end of epoch",
currentSlot: primitives.Slot(5*slotsPerEpoch - 1), // Slot 39 (epoch 4, last slot)
expectedEpochAlignment: true,
expectedMinRequiredSlot: primitives.Slot(1 * slotsPerEpoch), // Epoch 1 start = slot 8
description: "When pruning at epoch end, earliestAvailableSlot must be at epoch boundary",
},
{
name: "Pruning at various epoch positions",
currentSlot: primitives.Slot(8*slotsPerEpoch + 5), // Slot 69 (epoch 8, slot 5 of epoch)
expectedEpochAlignment: true,
expectedMinRequiredSlot: primitives.Slot(5 * slotsPerEpoch), // Epoch 5 start = slot 40
description: "EarliestAvailableSlot should always align to epoch boundary",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create the prune start slot function
ps := pruneStartSlotFunc(retentionEpochs)
// Calculate pruneUpto slot
pruneUpto := ps(tt.currentSlot)
// EarliestAvailableSlot is pruneUpto + 1
earliestAvailableSlot := pruneUpto + 1
// Verify epoch alignment: earliestAvailableSlot should be at an epoch boundary
if tt.expectedEpochAlignment {
// Check if earliestAvailableSlot is at the start of an epoch
epoch := slots.ToEpoch(earliestAvailableSlot)
epochStartSlot, err := slots.EpochStart(epoch)
require.NoError(t, err)
assert.Equal(t, epochStartSlot, earliestAvailableSlot,
"%s: earliestAvailableSlot (%d) should be at epoch boundary (slot %d of epoch %d)",
tt.description, earliestAvailableSlot, epochStartSlot, epoch)
}
// Verify it matches the expected minimum required slot for custody validation
assert.Equal(t, tt.expectedMinRequiredSlot, earliestAvailableSlot,
"%s: earliestAvailableSlot should match custody minimum required slot",
tt.description)
})
}
}
func TestPruner_UpdateEarliestSlotError(t *testing.T) {
params.SetupTestConfigCleanup(t)
config := params.BeaconConfig()

View File

@@ -0,0 +1,4 @@
### Fixed
- corrected defaultRetentionEpochs in pruner
- epoch aligned pruning: pruning should be epoch-wise. No fractional epoch pruning.