Skip to content

Commit

Permalink
op-supervisor: logs-db empty-db edge-case fix (#12097)
Browse files Browse the repository at this point in the history
  • Loading branch information
protolambda authored Sep 25, 2024
1 parent b55f4d7 commit 10a16aa
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 5 deletions.
12 changes: 7 additions & 5 deletions op-supervisor/supervisor/backend/db/logs/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,12 +210,11 @@ func (db *DB) FindSealedBlock(block eth.BlockID) (nextEntry entrydb.EntryIdx, er
func (db *DB) LatestSealedBlockNum() (n uint64, ok bool) {
db.rwLock.RLock()
defer db.rwLock.RUnlock()
if db.lastEntryContext.nextEntryIndex == 0 {
return 0, false // empty DB, time to add the first seal
}
if !db.lastEntryContext.hasCompleteBlock() {
if db.lastEntryContext.blockNum == 0 {
db.log.Debug("No DB contents yet")
} else {
db.log.Debug("New block is already in progress", "num", db.lastEntryContext.blockNum)
}
db.log.Debug("New block is already in progress", "num", db.lastEntryContext.blockNum)
}
return db.lastEntryContext.blockNum, true
}
Expand Down Expand Up @@ -381,6 +380,9 @@ func (db *DB) newIterator(index entrydb.EntryIdx) *iterator {
// to find the closest one with an equal or lower block number and equal or lower amount of seen logs.
// Returns the index of the searchCheckpoint to begin reading from or an error.
func (db *DB) searchCheckpoint(sealedBlockNum uint64, logsSince uint32) (entrydb.EntryIdx, error) {
if db.lastEntryContext.nextEntryIndex == 0 {
return 0, ErrFuture // empty DB, everything is in the future
}
n := (db.lastEntryIdx() / searchCheckpointFrequency) + 1
// Define: x is the array of known checkpoints
// Invariant: x[i] <= target, x[j] > target.
Expand Down
86 changes: 86 additions & 0 deletions op-supervisor/supervisor/backend/db/logs/db_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,92 @@ func TestEmptyDbDoesNotFindEntry(t *testing.T) {
})
}

func TestLatestSealedBlockNum(t *testing.T) {
t.Run("Empty case", func(t *testing.T) {
runDBTest(t,
func(t *testing.T, db *DB, m *stubMetrics) {},
func(t *testing.T, db *DB, m *stubMetrics) {
n, ok := db.LatestSealedBlockNum()
require.False(t, ok, "empty db expected")
require.Zero(t, n)
idx, err := db.searchCheckpoint(0, 0)
require.ErrorIs(t, err, ErrFuture, "no checkpoint in empty db")
require.Zero(t, idx)
})
})
t.Run("Zero case", func(t *testing.T) {
genesis := eth.BlockID{Hash: createHash(0), Number: 0}
runDBTest(t,
func(t *testing.T, db *DB, m *stubMetrics) {
require.NoError(t, db.SealBlock(common.Hash{}, genesis, 5000), "seal genesis")
},
func(t *testing.T, db *DB, m *stubMetrics) {
n, ok := db.LatestSealedBlockNum()
require.True(t, ok, "genesis block expected")
require.Equal(t, genesis.Number, n)
idx, err := db.searchCheckpoint(0, 0)
require.NoError(t, err)
require.Zero(t, idx, "genesis block as checkpoint 0")
})
})
t.Run("Later genesis case", func(t *testing.T) {
genesis := eth.BlockID{Hash: createHash(10), Number: 10}
runDBTest(t,
func(t *testing.T, db *DB, m *stubMetrics) {
require.NoError(t, db.SealBlock(common.Hash{}, genesis, 5000), "seal genesis")
},
func(t *testing.T, db *DB, m *stubMetrics) {
n, ok := db.LatestSealedBlockNum()
require.True(t, ok, "genesis block expected")
require.Equal(t, genesis.Number, n)
idx, err := db.searchCheckpoint(genesis.Number, 0)
require.NoError(t, err)
require.Zero(t, idx, "anchor block as checkpoint 0")
_, err = db.searchCheckpoint(0, 0)
require.ErrorIs(t, err, ErrSkipped, "no checkpoint before genesis")
})
})
t.Run("Block 1 case", func(t *testing.T) {
genesis := eth.BlockID{Hash: createHash(0), Number: 0}
block1 := eth.BlockID{Hash: createHash(1), Number: 1}
runDBTest(t,
func(t *testing.T, db *DB, m *stubMetrics) {
require.NoError(t, db.SealBlock(common.Hash{}, genesis, 5000), "seal genesis")
require.NoError(t, db.SealBlock(genesis.Hash, block1, 5001), "seal block 1")
},
func(t *testing.T, db *DB, m *stubMetrics) {
n, ok := db.LatestSealedBlockNum()
require.True(t, ok, "block 1 expected")
require.Equal(t, block1.Number, n)
idx, err := db.searchCheckpoint(block1.Number, 0)
require.NoError(t, err)
require.Equal(t, entrydb.EntryIdx(0), idx, "checkpoint 0 still for block 1")
})
})
t.Run("Using checkpoint case", func(t *testing.T) {
genesis := eth.BlockID{Hash: createHash(0), Number: 0}
runDBTest(t,
func(t *testing.T, db *DB, m *stubMetrics) {
require.NoError(t, db.SealBlock(common.Hash{}, genesis, 5000), "seal genesis")
for i := 1; i <= 260; i++ {
id := eth.BlockID{Hash: createHash(i), Number: uint64(i)}
require.NoError(t, db.SealBlock(createHash(i-1), id, 5001), "seal block %d", i)
}
},
func(t *testing.T, db *DB, m *stubMetrics) {
n, ok := db.LatestSealedBlockNum()
require.True(t, ok, "latest block expected")
expected := uint64(260)
require.Equal(t, expected, n)
idx, err := db.searchCheckpoint(expected, 0)
require.NoError(t, err)
// It costs 2 entries per block, so if we add more than 1 checkpoint worth of blocks,
// then we get to checkpoint 2
require.Equal(t, entrydb.EntryIdx(searchCheckpointFrequency*2), idx, "checkpoint 1 reached")
})
})
}

func TestAddLog(t *testing.T) {
t.Run("BlockZero", func(t *testing.T) {
// There are no logs in the genesis block so recording an entry for block 0 should be rejected.
Expand Down

0 comments on commit 10a16aa

Please sign in to comment.