Compare commits

...

12 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
Bastin
9153c5a202 light client logging (#15927) 2025-10-24 14:42:27 +00:00
james-prysm
26ce94e224 removes misleading keymanager info log (#15926)
* simple change

* fixing test"
"
2025-10-24 14:28:30 +00:00
satushh
1a904dbae3 epoch-aligned pruning to keep complete epochs rather than fractional epochs 2025-10-24 13:01:11 +01:00
terence
255ea2fac1 Return optimistic response only when handling blinded blocks (#15925)
* Return optimistic response only when handling blinded blocks in proposer

* Remove blind condition
2025-10-24 03:37:32 +00:00
satushh
37417e5905 changelog 2025-10-23 11:18:27 +01:00
satushh
7a5f4cf122 correct defaultRetentionEpochs 2025-10-22 19:58:29 +01:00
13 changed files with 201 additions and 24 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

@@ -6,6 +6,7 @@ go_library(
"cache.go",
"helpers.go",
"lightclient.go",
"log.go",
"store.go",
],
importpath = "github.com/OffchainLabs/prysm/v6/beacon-chain/light-client",

View File

@@ -0,0 +1,5 @@
package light_client
import "github.com/sirupsen/logrus"
var log = logrus.WithField("prefix", "light-client")

View File

@@ -14,7 +14,6 @@ import (
"github.com/OffchainLabs/prysm/v6/consensus-types/primitives"
"github.com/OffchainLabs/prysm/v6/time/slots"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
)
var ErrLightClientBootstrapNotFound = errors.New("light client bootstrap not found")

View File

@@ -312,14 +312,14 @@ func (vs *Server) ProposeBeaconBlock(ctx context.Context, req *ethpb.GenericSign
rob, err := blocks.NewROBlockWithRoot(block, root)
if block.IsBlinded() {
block, blobSidecars, err = vs.handleBlindedBlock(ctx, block)
if errors.Is(err, builderapi.ErrBadGateway) {
log.WithError(err).Info("Optimistically proposed block - builder relay temporarily unavailable, block may arrive over P2P")
return &ethpb.ProposeResponse{BlockRoot: root[:]}, nil
}
} else if block.Version() >= version.Deneb {
blobSidecars, dataColumnSidecars, err = vs.handleUnblindedBlock(rob, req)
}
if err != nil {
if errors.Is(err, builderapi.ErrBadGateway) && block.IsBlinded() {
log.WithError(err).Info("Optimistically proposed block - builder relay temporarily unavailable, block may arrive over P2P")
return &ethpb.ProposeResponse{BlockRoot: root[:]}, nil
}
return nil, status.Errorf(codes.Internal, "%s: %v", "handle block failed", err)
}

View File

@@ -0,0 +1,3 @@
### Ignored
- Add log prefix to the light-client package.

View File

@@ -0,0 +1,3 @@
### Removed
- log mentioning removed flag `--show-deposit-data`

View File

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

View File

@@ -0,0 +1,3 @@
### Ignored
- Return optimistic response only when handling blinded blocks in proposer

View File

@@ -221,10 +221,10 @@ func TestListAccounts_LocalKeymanager(t *testing.T) {
// Expected output format definition
const prologLength = 4
const accountLength = 4
const epilogLength = 2
const nameOffset = 1
const keyOffset = 2
const privkeyOffset = 3
const epilogLength = 1
const keyOffset = 1
const privkeyOffset = 2
// Require the output has correct number of lines
lineCount := prologLength + accountLength*numAccounts + epilogLength
@@ -242,7 +242,7 @@ func TestListAccounts_LocalKeymanager(t *testing.T) {
// Assert that account names are printed on the correct lines
for i, accountName := range accountNames {
lineNumber := prologLength + accountLength*i + nameOffset
lineNumber := prologLength + accountLength*i
accountNameFound := strings.Contains(lines[lineNumber], accountName)
assert.Equal(t, true, accountNameFound, "Account Name %s not found on line number %d", accountName, lineNumber)
}

View File

@@ -402,10 +402,6 @@ func (km *Keymanager) ListKeymanagerAccounts(ctx context.Context, cfg keymanager
} else {
fmt.Printf("Showing %d validator accounts\n", numAccounts)
}
fmt.Println(
au.BrightRed("View the eth1 deposit transaction data for your accounts " +
"by running `validator accounts list --show-deposit-data`"),
)
pubKeys, err := km.FetchValidatingPublicKeys(ctx)
if err != nil {