Skip to content

Commit cbd5ff3

Browse files
kaseyfernantho
authored andcommitted
Clean up block-slot-indices on block deletion (OffchainLabs#15011)
* clean up block-slot-indices on block deletion * also remove parent root index entry * treat parent root index as packed key (like slot idx) * fix bug where input slice is modified, with test --------- Co-authored-by: Kasey Kirkham <[email protected]>
1 parent d8dce70 commit cbd5ff3

File tree

5 files changed

+218
-12
lines changed

5 files changed

+218
-12
lines changed

beacon-chain/db/kv/blocks.go

Lines changed: 81 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,22 +30,32 @@ var errInvalidSlotRange = errors.New("invalid end slot and start slot provided")
3030
func (s *Store) Block(ctx context.Context, blockRoot [32]byte) (interfaces.ReadOnlySignedBeaconBlock, error) {
3131
ctx, span := trace.StartSpan(ctx, "BeaconDB.Block")
3232
defer span.End()
33-
// Return block from cache if it exists.
33+
blk, err := s.getBlock(ctx, blockRoot, nil)
34+
if errors.Is(err, ErrNotFound) {
35+
return nil, nil
36+
}
37+
return blk, err
38+
}
39+
40+
func (s *Store) getBlock(ctx context.Context, blockRoot [32]byte, tx *bolt.Tx) (interfaces.ReadOnlySignedBeaconBlock, error) {
3441
if v, ok := s.blockCache.Get(string(blockRoot[:])); v != nil && ok {
3542
return v.(interfaces.ReadOnlySignedBeaconBlock), nil
3643
}
37-
var blk interfaces.ReadOnlySignedBeaconBlock
38-
err := s.db.View(func(tx *bolt.Tx) error {
39-
bkt := tx.Bucket(blocksBucket)
40-
enc := bkt.Get(blockRoot[:])
41-
if enc == nil {
42-
return nil
43-
}
44+
// This method allows the caller to pass in its tx if one is already open.
45+
// Or if a nil value is used, a transaction will be managed intenally.
46+
if tx == nil {
4447
var err error
45-
blk, err = unmarshalBlock(ctx, enc)
46-
return err
47-
})
48-
return blk, err
48+
tx, err = s.db.Begin(false)
49+
if err != nil {
50+
return nil, err
51+
}
52+
defer func() {
53+
if err := tx.Rollback(); err != nil {
54+
log.WithError(err).Error("could not rollback read-only getBlock transaction")
55+
}
56+
}()
57+
}
58+
return unmarshalBlock(ctx, tx.Bucket(blocksBucket).Get(blockRoot[:]))
4959
}
5060

5161
// OriginCheckpointBlockRoot returns the value written to the db in SaveOriginCheckpointBlockRoot
@@ -227,6 +237,21 @@ func (s *Store) DeleteBlock(ctx context.Context, root [32]byte) error {
227237
return ErrDeleteJustifiedAndFinalized
228238
}
229239

240+
// Look up the block to find its slot; needed to remove the slot index entry.
241+
blk, err := s.getBlock(ctx, root, tx)
242+
if err != nil {
243+
// getBlock can return ErrNotFound, in which case we won't even try to delete it.
244+
if errors.Is(err, ErrNotFound) {
245+
return nil
246+
}
247+
return err
248+
}
249+
if err := s.deleteSlotIndexEntry(tx, blk.Block().Slot(), root); err != nil {
250+
return err
251+
}
252+
if err := s.deleteMatchingParentIndex(tx, blk.Block().ParentRoot(), root); err != nil {
253+
return err
254+
}
230255
if err := s.deleteBlock(tx, root[:]); err != nil {
231256
return err
232257
}
@@ -899,6 +924,9 @@ func createBlockIndicesFromFilters(ctx context.Context, f *filters.QueryFilter)
899924

900925
// unmarshal block from marshaled proto beacon block bytes to versioned beacon block struct type.
901926
func unmarshalBlock(_ context.Context, enc []byte) (interfaces.ReadOnlySignedBeaconBlock, error) {
927+
if len(enc) == 0 {
928+
return nil, errors.Wrap(ErrNotFound, "empty block bytes in db")
929+
}
902930
var err error
903931
enc, err = snappy.Decode(nil, enc)
904932
if err != nil {
@@ -1050,6 +1078,47 @@ func (s *Store) deleteBlock(tx *bolt.Tx, root []byte) error {
10501078
return nil
10511079
}
10521080

1081+
func (s *Store) deleteMatchingParentIndex(tx *bolt.Tx, parent, child [32]byte) error {
1082+
bkt := tx.Bucket(blockParentRootIndicesBucket)
1083+
if err := deleteRootIndexEntry(bkt, parent[:], child); err != nil {
1084+
return errors.Wrap(err, "could not delete parent root index entry")
1085+
}
1086+
return nil
1087+
}
1088+
1089+
func (s *Store) deleteSlotIndexEntry(tx *bolt.Tx, slot primitives.Slot, root [32]byte) error {
1090+
key := bytesutil.SlotToBytesBigEndian(slot)
1091+
bkt := tx.Bucket(blockSlotIndicesBucket)
1092+
if err := deleteRootIndexEntry(bkt, key, root); err != nil {
1093+
return errors.Wrap(err, "could not delete slot index entry")
1094+
}
1095+
return nil
1096+
}
1097+
1098+
func deleteRootIndexEntry(bkt *bolt.Bucket, key []byte, root [32]byte) error {
1099+
packed := bkt.Get(key)
1100+
if len(packed) == 0 {
1101+
return nil
1102+
}
1103+
updated, err := removeRoot(packed, root)
1104+
if err != nil {
1105+
return err
1106+
}
1107+
// Don't update the value if the root was not found.
1108+
if bytes.Equal(updated, packed) {
1109+
return nil
1110+
}
1111+
// If there are no other roots in the key, just delete it.
1112+
if len(updated) == 0 {
1113+
if err := bkt.Delete(key); err != nil {
1114+
return err
1115+
}
1116+
return nil
1117+
}
1118+
// Update the key with the root removed.
1119+
return bkt.Put(key, updated)
1120+
}
1121+
10531122
func (s *Store) deleteValidatorHashes(tx *bolt.Tx, root []byte) error {
10541123
ok, err := s.isStateValidatorMigrationOver()
10551124
if err != nil {

beacon-chain/db/kv/blocks_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,9 +196,13 @@ func TestStore_BlocksCRUD(t *testing.T) {
196196
blockRoot, err := blk.Block().HashTreeRoot()
197197
require.NoError(t, err)
198198

199+
_, err = db.getBlock(ctx, blockRoot, nil)
200+
require.ErrorIs(t, err, ErrNotFound)
199201
retrievedBlock, err := db.Block(ctx, blockRoot)
200202
require.NoError(t, err)
201203
assert.DeepEqual(t, nil, retrievedBlock, "Expected nil block")
204+
_, err = db.getBlock(ctx, blockRoot, nil)
205+
require.ErrorIs(t, err, ErrNotFound)
202206

203207
require.NoError(t, db.SaveBlock(ctx, blk))
204208
assert.Equal(t, true, db.HasBlock(ctx, blockRoot), "Expected block to exist in the db")
@@ -214,10 +218,34 @@ func TestStore_BlocksCRUD(t *testing.T) {
214218
retrievedPb, err := retrievedBlock.Proto()
215219
require.NoError(t, err)
216220
assert.Equal(t, true, proto.Equal(wantedPb, retrievedPb), "Wanted: %v, received: %v", wanted, retrievedBlock)
221+
// Check that the block is in the slot->block index
222+
found, roots, err := db.BlockRootsBySlot(ctx, blk.Block().Slot())
223+
require.NoError(t, err)
224+
require.Equal(t, true, found)
225+
require.Equal(t, 1, len(roots))
226+
require.Equal(t, blockRoot, roots[0])
227+
// Delete the block, then check that it is no longer in the index.
228+
229+
parent := blk.Block().ParentRoot()
230+
testCheckParentIndices(t, db.db, parent, true)
231+
require.NoError(t, db.DeleteBlock(ctx, blockRoot))
232+
require.NoError(t, err)
233+
testCheckParentIndices(t, db.db, parent, false)
234+
found, roots, err = db.BlockRootsBySlot(ctx, blk.Block().Slot())
235+
require.NoError(t, err)
236+
require.Equal(t, false, found)
237+
require.Equal(t, 0, len(roots))
217238
})
218239
}
219240
}
220241

242+
func testCheckParentIndices(t *testing.T, db *bolt.DB, parent [32]byte, expected bool) {
243+
require.NoError(t, db.View(func(tx *bolt.Tx) error {
244+
require.Equal(t, expected, tx.Bucket(blockParentRootIndicesBucket).Get(parent[:]) != nil)
245+
return nil
246+
}))
247+
}
248+
221249
func TestStore_BlocksHandleZeroCase(t *testing.T) {
222250
for _, tt := range blockTests {
223251
t.Run(tt.name, func(t *testing.T) {

beacon-chain/db/kv/utils.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,27 @@ func splitRoots(b []byte) ([][32]byte, error) {
114114
}
115115
return rl, nil
116116
}
117+
118+
func removeRoot(roots []byte, root [32]byte) ([]byte, error) {
119+
if len(roots) == 0 {
120+
return []byte{}, nil
121+
}
122+
if len(roots) == 32 && bytes.Equal(roots, root[:]) {
123+
return []byte{}, nil
124+
}
125+
if len(roots)%32 != 0 {
126+
return nil, errors.Wrapf(errMisalignedRootList, "root list len=%d", len(roots))
127+
}
128+
129+
search := root[:]
130+
for i := 0; i <= len(roots)-32; i += 32 {
131+
if bytes.Equal(roots[i:i+32], search) {
132+
result := make([]byte, len(roots)-32)
133+
copy(result, roots[:i])
134+
copy(result[i:], roots[i+32:])
135+
return result, nil
136+
}
137+
}
138+
139+
return roots, nil
140+
}

beacon-chain/db/kv/utils_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package kv
22

33
import (
4+
"bytes"
45
"context"
56
"crypto/rand"
67
"testing"
@@ -195,3 +196,85 @@ func TestSplitRoots(t *testing.T) {
195196
})
196197
}
197198
}
199+
200+
func tPad(p ...[]byte) []byte {
201+
r := make([]byte, 32*len(p))
202+
for i, b := range p {
203+
copy(r[i*32:], b)
204+
}
205+
return r
206+
}
207+
208+
func TestRemoveRoot(t *testing.T) {
209+
cases := []struct {
210+
name string
211+
roots []byte
212+
root [32]byte
213+
expect []byte
214+
err error
215+
}{
216+
{
217+
name: "empty",
218+
roots: []byte{},
219+
root: [32]byte{0xde, 0xad, 0xbe, 0xef},
220+
expect: []byte{},
221+
},
222+
{
223+
name: "single",
224+
roots: tPad([]byte{0xde, 0xad, 0xbe, 0xef}),
225+
root: [32]byte{0xde, 0xad, 0xbe, 0xef},
226+
expect: []byte{},
227+
},
228+
{
229+
name: "single, different",
230+
roots: tPad([]byte{0xde, 0xad, 0xbe, 0xef}),
231+
root: [32]byte{0xde, 0xad, 0xbe, 0xee},
232+
expect: tPad([]byte{0xde, 0xad, 0xbe, 0xef}),
233+
},
234+
{
235+
name: "multi",
236+
roots: tPad([]byte{0xde, 0xad, 0xbe, 0xef}, []byte{0xac, 0x1d, 0xfa, 0xce}),
237+
root: [32]byte{0xac, 0x1d, 0xfa, 0xce},
238+
expect: tPad([]byte{0xde, 0xad, 0xbe, 0xef}),
239+
},
240+
{
241+
name: "multi, reordered",
242+
roots: tPad([]byte{0xac, 0x1d, 0xfa, 0xce}, []byte{0xde, 0xad, 0xbe, 0xef}),
243+
root: [32]byte{0xac, 0x1d, 0xfa, 0xce},
244+
expect: tPad([]byte{0xde, 0xad, 0xbe, 0xef}),
245+
},
246+
{
247+
name: "multi, 3",
248+
roots: tPad([]byte{0xac, 0x1d, 0xfa, 0xce}, []byte{0xbe, 0xef, 0xca, 0xb5}, []byte{0xde, 0xad, 0xbe, 0xef}),
249+
root: [32]byte{0xac, 0x1d, 0xfa, 0xce},
250+
expect: tPad([]byte{0xbe, 0xef, 0xca, 0xb5}, []byte{0xde, 0xad, 0xbe, 0xef}),
251+
},
252+
{
253+
name: "multi, different",
254+
roots: tPad([]byte{0xde, 0xad, 0xbe, 0xef}, []byte{0xac, 0x1d, 0xfa, 0xce}),
255+
root: [32]byte{0xac, 0x1d, 0xbe, 0xa7},
256+
expect: tPad([]byte{0xde, 0xad, 0xbe, 0xef}, []byte{0xac, 0x1d, 0xfa, 0xce}),
257+
},
258+
{
259+
name: "misaligned",
260+
roots: make([]byte, 61),
261+
root: [32]byte{0xac, 0x1d, 0xbe, 0xa7},
262+
err: errMisalignedRootList,
263+
},
264+
}
265+
for _, c := range cases {
266+
t.Run(c.name, func(t *testing.T) {
267+
before := make([]byte, len(c.roots))
268+
copy(before, c.roots)
269+
r, err := removeRoot(c.roots, c.root)
270+
if c.err != nil {
271+
require.ErrorIs(t, err, c.err)
272+
return
273+
}
274+
require.NoError(t, err)
275+
require.Equal(t, len(c.expect), len(r))
276+
require.Equal(t, true, bytes.Equal(c.expect, r))
277+
require.Equal(t, true, bytes.Equal(before, c.roots))
278+
})
279+
}
280+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
### Fixed
2+
- Ensure that deleting a block from the database clears its entry in the slot->root db index.

0 commit comments

Comments
 (0)