Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

op-supervisor: logs-db empty-db edge-case fix #12097

Merged
merged 1 commit into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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