Skip to content

Commit d613f3a

Browse files
authored
Update Earliest available slot when pruning (#15694)
* Update Earliest available slot when pruning * bazel run //:gazelle -- fix * custodyUpdater interface to avoid import cycle * bazel run //:gazelle -- fix * simplify test * separation of concerns * debug log for updating eas * UpdateEarliestAvailableSlot function in CustodyManager * fix test * UpdateEarliestAvailableSlot function for FakeP2P * lint * UpdateEarliestAvailableSlot instead of UpdateCustodyInfo + check for Fulu * fix test and lint * bugfix: enforce minimum retention period in pruner * remove MinEpochsForBlockRequests function and use from config * remove modifying earliest_available_slot after data column pruning * correct earliestAvailableSlot validation: allow backfill decrease but prevent increase within MIN_EPOCHS_FOR_BLOCK_REQUESTS * lint * bazel run //:gazelle -- fix * lint and remove unwanted debug logs * Return a wrapped error, and let the caller decide what to do * fix tests because updateEarliestSlot returns error now * avoid re-doing computation in the test function * lint and correct changelog * custody updater should be a mandatory part of the pruner service * ensure never increase eas if we are in the block requests window * slot level granularity edge case * update the value stored in the DB * log tidy up * use errNoCustodyInfo * allow earliestAvailableSlot edit when custodyGroupCount doesnt change * undo the minimal config change * add context to CustodyGroupCount after merging from develop * cosmetic change * shift responsibility from caller to callee, protection for updateEarliestSlot. UpdateEarliestAvailableSlot returns cgc * allow increase in earliestAvailableSlot only when custodyGroupCount also increases * remove CustodyGroupCount as it is no longer needed as UpdateEarliestAvailableSlot returns cgc now * proper place for log and name refactor * test for Nil custody info * allow decreasing earliest slot in DB (just like in memory) * invert if statement to make more readable * UpdateEarliestAvailableSlot for DB (equivalent of p2p's UpdateEarliestAvailableSlot) & undo changes made to UpdateCustodyInfo * in UpdateEarliestAvailableSlot, no need to return unused values * no need to log stored group count * log.WithField instead of log.WithFields
1 parent 5751dbf commit d613f3a

File tree

19 files changed

+730
-50
lines changed

19 files changed

+730
-50
lines changed

beacon-chain/blockchain/setup_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,14 @@ func (dch *mockCustodyManager) UpdateCustodyInfo(earliestAvailableSlot primitive
130130
return earliestAvailableSlot, custodyGroupCount, nil
131131
}
132132

133+
func (dch *mockCustodyManager) UpdateEarliestAvailableSlot(earliestAvailableSlot primitives.Slot) error {
134+
dch.mut.Lock()
135+
defer dch.mut.Unlock()
136+
137+
dch.earliestAvailableSlot = earliestAvailableSlot
138+
return nil
139+
}
140+
133141
func (dch *mockCustodyManager) CustodyGroupCountFromPeer(peer.ID) uint64 {
134142
return 0
135143
}

beacon-chain/core/helpers/weak_subjectivity.go

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -201,14 +201,3 @@ func ParseWeakSubjectivityInputString(wsCheckpointString string) (*v1alpha1.Chec
201201
Root: bRoot,
202202
}, nil
203203
}
204-
205-
// MinEpochsForBlockRequests computes the number of epochs of block history that we need to maintain,
206-
// relative to the current epoch, per the p2p specs. This is used to compute the slot where backfill is complete.
207-
// value defined:
208-
// https://github.com/ethereum/consensus-specs/blob/master/specs/phase0/p2p-interface.md#configuration
209-
// MIN_VALIDATOR_WITHDRAWABILITY_DELAY + CHURN_LIMIT_QUOTIENT // 2 (= 33024, ~5 months)
210-
// detailed rationale: https://github.com/ethereum/consensus-specs/blob/master/specs/phase0/p2p-interface.md#why-are-blocksbyrange-requests-only-required-to-be-served-for-the-latest-min_epochs_for_block_requests-epochs
211-
func MinEpochsForBlockRequests() primitives.Epoch {
212-
return params.BeaconConfig().MinValidatorWithdrawabilityDelay +
213-
primitives.Epoch(params.BeaconConfig().ChurnLimitQuotient/2)
214-
}

beacon-chain/core/helpers/weak_subjectivity_test.go

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -286,20 +286,3 @@ func genState(t *testing.T, valCount, avgBalance uint64) state.BeaconState {
286286
return beaconState
287287
}
288288

289-
func TestMinEpochsForBlockRequests(t *testing.T) {
290-
helpers.ClearCache()
291-
292-
params.SetActiveTestCleanup(t, params.MainnetConfig())
293-
var expected primitives.Epoch = 33024
294-
// expected value of 33024 via spec commentary:
295-
// https://github.com/ethereum/consensus-specs/blob/master/specs/phase0/p2p-interface.md#why-are-blocksbyrange-requests-only-required-to-be-served-for-the-latest-min_epochs_for_block_requests-epochs
296-
// MIN_EPOCHS_FOR_BLOCK_REQUESTS is calculated using the arithmetic from compute_weak_subjectivity_period found in the weak subjectivity guide. Specifically to find this max epoch range, we use the worst case event of a very large validator size (>= MIN_PER_EPOCH_CHURN_LIMIT * CHURN_LIMIT_QUOTIENT).
297-
//
298-
// MIN_EPOCHS_FOR_BLOCK_REQUESTS = (
299-
// MIN_VALIDATOR_WITHDRAWABILITY_DELAY
300-
// + MAX_SAFETY_DECAY * CHURN_LIMIT_QUOTIENT // (2 * 100)
301-
// )
302-
//
303-
// Where MAX_SAFETY_DECAY = 100 and thus MIN_EPOCHS_FOR_BLOCK_REQUESTS = 33024 (~5 months).
304-
require.Equal(t, expected, helpers.MinEpochsForBlockRequests())
305-
}

beacon-chain/db/iface/interface.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ type NoHeadAccessDatabase interface {
129129
// Custody operations.
130130
UpdateSubscribedToAllDataSubnets(ctx context.Context, subscribed bool) (bool, error)
131131
UpdateCustodyInfo(ctx context.Context, earliestAvailableSlot primitives.Slot, custodyGroupCount uint64) (primitives.Slot, uint64, error)
132+
UpdateEarliestAvailableSlot(ctx context.Context, earliestAvailableSlot primitives.Slot) error
132133

133134
// P2P Metadata operations.
134135
SaveMetadataSeqNum(ctx context.Context, seqNum uint64) error

beacon-chain/db/kv/custody.go

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,19 @@ package kv
22

33
import (
44
"context"
5+
"time"
56

7+
"github.com/OffchainLabs/prysm/v6/config/params"
68
"github.com/OffchainLabs/prysm/v6/consensus-types/primitives"
79
"github.com/OffchainLabs/prysm/v6/encoding/bytesutil"
810
"github.com/OffchainLabs/prysm/v6/monitoring/tracing/trace"
11+
"github.com/OffchainLabs/prysm/v6/time/slots"
912
"github.com/pkg/errors"
1013
"github.com/sirupsen/logrus"
1114
bolt "go.etcd.io/bbolt"
1215
)
1316

14-
// UpdateCustodyInfo atomically updates the custody group count only it is greater than the stored one.
17+
// UpdateCustodyInfo atomically updates the custody group count only if it is greater than the stored one.
1518
// In this case, it also updates the earliest available slot with the provided value.
1619
// It returns the (potentially updated) custody group count and earliest available slot.
1720
func (s *Store) UpdateCustodyInfo(ctx context.Context, earliestAvailableSlot primitives.Slot, custodyGroupCount uint64) (primitives.Slot, uint64, error) {
@@ -70,6 +73,79 @@ func (s *Store) UpdateCustodyInfo(ctx context.Context, earliestAvailableSlot pri
7073
return storedEarliestAvailableSlot, storedGroupCount, nil
7174
}
7275

76+
// UpdateEarliestAvailableSlot updates the earliest available slot.
77+
func (s *Store) UpdateEarliestAvailableSlot(ctx context.Context, earliestAvailableSlot primitives.Slot) error {
78+
_, span := trace.StartSpan(ctx, "BeaconDB.UpdateEarliestAvailableSlot")
79+
defer span.End()
80+
81+
storedEarliestAvailableSlot := primitives.Slot(0)
82+
if err := s.db.Update(func(tx *bolt.Tx) error {
83+
// Retrieve the custody bucket.
84+
bucket, err := tx.CreateBucketIfNotExists(custodyBucket)
85+
if err != nil {
86+
return errors.Wrap(err, "create custody bucket")
87+
}
88+
89+
// Retrieve the stored earliest available slot.
90+
storedEarliestAvailableSlotBytes := bucket.Get(earliestAvailableSlotKey)
91+
if len(storedEarliestAvailableSlotBytes) != 0 {
92+
storedEarliestAvailableSlot = primitives.Slot(bytesutil.BytesToUint64BigEndian(storedEarliestAvailableSlotBytes))
93+
}
94+
95+
// Allow decrease (for backfill scenarios)
96+
if earliestAvailableSlot <= storedEarliestAvailableSlot {
97+
storedEarliestAvailableSlot = earliestAvailableSlot
98+
bytes := bytesutil.Uint64ToBytesBigEndian(uint64(earliestAvailableSlot))
99+
if err := bucket.Put(earliestAvailableSlotKey, bytes); err != nil {
100+
return errors.Wrap(err, "put earliest available slot")
101+
}
102+
return nil
103+
}
104+
105+
// Prevent increase within the MIN_EPOCHS_FOR_BLOCK_REQUESTS period
106+
// This ensures we don't voluntarily refuse to serve mandatory block data
107+
genesisTime := time.Unix(int64(params.BeaconConfig().MinGenesisTime+params.BeaconConfig().GenesisDelay), 0)
108+
currentSlot := slots.CurrentSlot(genesisTime)
109+
currentEpoch := slots.ToEpoch(currentSlot)
110+
minEpochsForBlocks := primitives.Epoch(params.BeaconConfig().MinEpochsForBlockRequests)
111+
112+
// Calculate the minimum required epoch (or 0 if we're early in the chain)
113+
minRequiredEpoch := primitives.Epoch(0)
114+
if currentEpoch > minEpochsForBlocks {
115+
minRequiredEpoch = currentEpoch - minEpochsForBlocks
116+
}
117+
118+
// Convert to slot to ensure we compare at slot-level granularity
119+
minRequiredSlot, err := slots.EpochStart(minRequiredEpoch)
120+
if err != nil {
121+
return errors.Wrap(err, "calculate minimum required slot")
122+
}
123+
124+
// Prevent any increase that would put earliest available slot beyond the minimum required slot
125+
if earliestAvailableSlot > minRequiredSlot {
126+
return errors.Errorf(
127+
"cannot increase earliest available slot to %d (epoch %d) as it exceeds minimum required slot %d (epoch %d)",
128+
earliestAvailableSlot, slots.ToEpoch(earliestAvailableSlot),
129+
minRequiredSlot, minRequiredEpoch,
130+
)
131+
}
132+
133+
storedEarliestAvailableSlot = earliestAvailableSlot
134+
bytes := bytesutil.Uint64ToBytesBigEndian(uint64(earliestAvailableSlot))
135+
if err := bucket.Put(earliestAvailableSlotKey, bytes); err != nil {
136+
return errors.Wrap(err, "put earliest available slot")
137+
}
138+
139+
return nil
140+
}); err != nil {
141+
return err
142+
}
143+
144+
log.WithField("earliestAvailableSlot", storedEarliestAvailableSlot).Debug("Updated earliest available slot")
145+
146+
return nil
147+
}
148+
73149
// UpdateSubscribedToAllDataSubnets updates the "subscribed to all data subnets" status in the database
74150
// only if `subscribed` is `true`.
75151
// It returns the previous subscription status.

beacon-chain/db/kv/custody_test.go

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@ package kv
33
import (
44
"context"
55
"testing"
6+
"time"
67

8+
"github.com/OffchainLabs/prysm/v6/config/params"
79
"github.com/OffchainLabs/prysm/v6/consensus-types/primitives"
810
"github.com/OffchainLabs/prysm/v6/encoding/bytesutil"
911
"github.com/OffchainLabs/prysm/v6/testing/require"
12+
"github.com/OffchainLabs/prysm/v6/time/slots"
1013
bolt "go.etcd.io/bbolt"
1114
)
1215

@@ -132,6 +135,131 @@ func TestUpdateCustodyInfo(t *testing.T) {
132135
})
133136
}
134137

138+
func TestUpdateEarliestAvailableSlot(t *testing.T) {
139+
ctx := t.Context()
140+
141+
t.Run("allow decreasing earliest slot (backfill scenario)", func(t *testing.T) {
142+
const (
143+
initialSlot = primitives.Slot(300)
144+
initialCount = uint64(10)
145+
earliestSlot = primitives.Slot(200) // Lower than initial (backfill discovered earlier blocks)
146+
)
147+
148+
db := setupDB(t)
149+
150+
// Initialize custody info
151+
_, _, err := db.UpdateCustodyInfo(ctx, initialSlot, initialCount)
152+
require.NoError(t, err)
153+
154+
// Update with a lower slot (should update for backfill)
155+
err = db.UpdateEarliestAvailableSlot(ctx, earliestSlot)
156+
require.NoError(t, err)
157+
158+
storedSlot, storedCount := getCustodyInfoFromDB(t, db)
159+
require.Equal(t, earliestSlot, storedSlot)
160+
require.Equal(t, initialCount, storedCount)
161+
})
162+
163+
t.Run("allow increasing slot within MIN_EPOCHS_FOR_BLOCK_REQUESTS (pruning scenario)", func(t *testing.T) {
164+
db := setupDB(t)
165+
166+
// Calculate the current slot and minimum required slot based on actual current time
167+
genesisTime := time.Unix(int64(params.BeaconConfig().MinGenesisTime+params.BeaconConfig().GenesisDelay), 0)
168+
currentSlot := slots.CurrentSlot(genesisTime)
169+
currentEpoch := slots.ToEpoch(currentSlot)
170+
minEpochsForBlocks := primitives.Epoch(params.BeaconConfig().MinEpochsForBlockRequests)
171+
172+
var minRequiredEpoch primitives.Epoch
173+
if currentEpoch > minEpochsForBlocks {
174+
minRequiredEpoch = currentEpoch - minEpochsForBlocks
175+
} else {
176+
minRequiredEpoch = 0
177+
}
178+
179+
minRequiredSlot, err := slots.EpochStart(minRequiredEpoch)
180+
require.NoError(t, err)
181+
182+
// Initial setup: set earliest slot well before minRequiredSlot
183+
const groupCount = uint64(5)
184+
initialSlot := primitives.Slot(1000)
185+
186+
_, _, err = db.UpdateCustodyInfo(ctx, initialSlot, groupCount)
187+
require.NoError(t, err)
188+
189+
// Try to increase to a slot that's still BEFORE minRequiredSlot (should succeed)
190+
validSlot := minRequiredSlot - 100
191+
192+
err = db.UpdateEarliestAvailableSlot(ctx, validSlot)
193+
require.NoError(t, err)
194+
195+
// Verify the database was updated
196+
storedSlot, storedCount := getCustodyInfoFromDB(t, db)
197+
require.Equal(t, validSlot, storedSlot)
198+
require.Equal(t, groupCount, storedCount)
199+
})
200+
201+
t.Run("prevent increasing slot beyond MIN_EPOCHS_FOR_BLOCK_REQUESTS", func(t *testing.T) {
202+
db := setupDB(t)
203+
204+
// Calculate the current slot and minimum required slot based on actual current time
205+
genesisTime := time.Unix(int64(params.BeaconConfig().MinGenesisTime+params.BeaconConfig().GenesisDelay), 0)
206+
currentSlot := slots.CurrentSlot(genesisTime)
207+
currentEpoch := slots.ToEpoch(currentSlot)
208+
minEpochsForBlocks := primitives.Epoch(params.BeaconConfig().MinEpochsForBlockRequests)
209+
210+
var minRequiredEpoch primitives.Epoch
211+
if currentEpoch > minEpochsForBlocks {
212+
minRequiredEpoch = currentEpoch - minEpochsForBlocks
213+
} else {
214+
minRequiredEpoch = 0
215+
}
216+
217+
minRequiredSlot, err := slots.EpochStart(minRequiredEpoch)
218+
require.NoError(t, err)
219+
220+
// Initial setup: set a valid earliest slot (well before minRequiredSlot)
221+
const initialCount = uint64(5)
222+
initialSlot := primitives.Slot(1000)
223+
224+
_, _, err = db.UpdateCustodyInfo(ctx, initialSlot, initialCount)
225+
require.NoError(t, err)
226+
227+
// Try to set earliest slot beyond the minimum required slot
228+
invalidSlot := minRequiredSlot + 100
229+
230+
// This should fail
231+
err = db.UpdateEarliestAvailableSlot(ctx, invalidSlot)
232+
require.ErrorContains(t, "cannot increase earliest available slot", err)
233+
require.ErrorContains(t, "exceeds minimum required slot", err)
234+
235+
// Verify the database wasn't updated
236+
storedSlot, storedCount := getCustodyInfoFromDB(t, db)
237+
require.Equal(t, initialSlot, storedSlot)
238+
require.Equal(t, initialCount, storedCount)
239+
})
240+
241+
t.Run("no change when slot equals current slot", func(t *testing.T) {
242+
const (
243+
initialSlot = primitives.Slot(100)
244+
initialCount = uint64(5)
245+
)
246+
247+
db := setupDB(t)
248+
249+
// Initialize custody info
250+
_, _, err := db.UpdateCustodyInfo(ctx, initialSlot, initialCount)
251+
require.NoError(t, err)
252+
253+
// Update with the same slot
254+
err = db.UpdateEarliestAvailableSlot(ctx, initialSlot)
255+
require.NoError(t, err)
256+
257+
storedSlot, storedCount := getCustodyInfoFromDB(t, db)
258+
require.Equal(t, initialSlot, storedSlot)
259+
require.Equal(t, initialCount, storedCount)
260+
})
261+
}
262+
135263
func TestUpdateSubscribedToAllDataSubnets(t *testing.T) {
136264
ctx := context.Background()
137265

beacon-chain/db/pruner/BUILD.bazel

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ go_library(
88
"//beacon-chain:__subpackages__",
99
],
1010
deps = [
11-
"//beacon-chain/core/helpers:go_default_library",
1211
"//beacon-chain/db:go_default_library",
1312
"//beacon-chain/db/iface:go_default_library",
1413
"//config/params:go_default_library",
@@ -29,6 +28,7 @@ go_test(
2928
"//consensus-types/blocks:go_default_library",
3029
"//consensus-types/primitives:go_default_library",
3130
"//proto/prysm/v1alpha1:go_default_library",
31+
"//testing/assert:go_default_library",
3232
"//testing/require:go_default_library",
3333
"//testing/util:go_default_library",
3434
"//time/slots/testing:go_default_library",

0 commit comments

Comments
 (0)