Skip to content
Draft
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
5 changes: 1 addition & 4 deletions op-devstack/dsl/supernode.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ func (s *Supernode) AwaitBackfillCompleted() {
// (the first seal is at most one block before activation; when activation
// is not aligned to a block boundary, the block representing the chain
// state as of activation is the correct pairing anchor and is sealed).
// 2. firstSealed.Timestamp < BackfillEndTimestamp()+1
// 2. firstSealed.Timestamp < FirstVerifiableTimestamp()
// (the post-backfill handoff happens strictly after the backfilled range)
// 3. firstSealed.Timestamp <= max(ActivationTimestamp, latestSealed.Timestamp - depth)
// + blockTime (backfill reached ~depth back,
Expand All @@ -202,9 +202,6 @@ func (s *Supernode) AssertBackfillCovers(depth time.Duration, blockTime uint64,

activation := ia.ActivationTimestamp()
backfillHandoff := ia.FirstVerifiableTimestamp()
if backfillEnd := ia.BackfillEndTimestamp(); backfillEnd != 0 {
backfillHandoff = backfillEnd + 1
}
depthSec := uint64(depth / time.Second)

for _, chainID := range chains {
Expand Down
2 changes: 1 addition & 1 deletion op-devstack/stack/supernode.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type Supernode interface {
// InteropActivity; see op-supernode/supernode/activity/interop for the
// methods available on the returned pointer (PauseAt, Resume,
// BackfillAttempts, BackfillCompleted, ActivationTimestamp,
// BackfillEndTimestamp, FirstVerifiableTimestamp, FirstSealedBlock,
// VerificationStartTimestamp, FirstVerifiableTimestamp, FirstSealedBlock,
// LatestSealedBlock, ...).
type InteropTestControl interface {
// InteropActivity returns the current interop activity, or nil if the
Expand Down
3 changes: 3 additions & 0 deletions op-node/node/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ type driverClient interface {

type SafeDBReader interface {
SafeHeadAtL1(ctx context.Context, l1BlockNum uint64) (l1 eth.BlockID, l2 eth.BlockID, err error)
// FirstEntry returns the lowest recorded (L1, L2 safe head) pair.
// Returns ErrNotFound when no entries exist yet.
FirstEntry(ctx context.Context) (l1 eth.BlockID, l2 eth.BlockID, err error)
}

type adminAPI struct {
Expand Down
5 changes: 5 additions & 0 deletions op-node/node/safedb/disabled.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ func (d *DisabledDB) SafeHeadAtL1(_ context.Context, _ uint64) (l1 eth.BlockID,
return
}

func (d *DisabledDB) FirstEntry(_ context.Context) (l1 eth.BlockID, safeHead eth.BlockID, err error) {
err = ErrNotEnabled
return
}

func (d *DisabledDB) SafeHeadReset(_ eth.L2BlockRef) error {
return nil
}
Expand Down
24 changes: 24 additions & 0 deletions op-node/node/safedb/safedb.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,30 @@ func (d *SafeDB) SafeHeadReset(safeHead eth.L2BlockRef) error {
}
}

func (d *SafeDB) FirstEntry(ctx context.Context) (l1Block eth.BlockID, safeHead eth.BlockID, err error) {
d.m.RLock()
defer d.m.RUnlock()
if d.closed {
err = ErrClosed
return
}
iter, err := d.db.NewIterWithContext(ctx, safeByL1BlockNumKey.IterRange())
if err != nil {
return
}
defer iter.Close()
if valid := iter.First(); !valid {
err = ErrNotFound
return
}
val, err := iter.ValueAndErr()
if err != nil {
return
}
l1Block, safeHead, err = decodeSafeByL1BlockNum(iter.Key(), val)
return
}

func (d *SafeDB) SafeHeadAtL1(ctx context.Context, l1BlockNum uint64) (l1Block eth.BlockID, safeHead eth.BlockID, err error) {
d.m.RLock()
defer d.m.RUnlock()
Expand Down
58 changes: 58 additions & 0 deletions op-node/node/safedb/safedb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,64 @@ func TestSafeHeadAtL1_EmptyDatabase(t *testing.T) {
require.ErrorIs(t, err, ErrNotFound)
}

func TestFirstEntry_EmptyDatabase(t *testing.T) {
logger := testlog.Logger(t, log.LvlInfo)
dir := t.TempDir()
db, err := NewSafeDB(logger, dir)
require.NoError(t, err)
defer db.Close()
_, _, err = db.FirstEntry(context.Background())
require.ErrorIs(t, err, ErrNotFound)
}

func TestFirstEntry_ReturnsLowestL1(t *testing.T) {
logger := testlog.Logger(t, log.LvlInfo)
dir := t.TempDir()
db, err := NewSafeDB(logger, dir)
require.NoError(t, err)
defer db.Close()

l2a := eth.L2BlockRef{Hash: common.Hash{0x02, 0xaa}, Number: 20}
l2b := eth.L2BlockRef{Hash: common.Hash{0x02, 0xbb}, Number: 25}
l1a := eth.BlockID{Hash: common.Hash{0x01, 0xaa}, Number: 100}
l1b := eth.BlockID{Hash: common.Hash{0x01, 0xbb}, Number: 150}

// Insert out of order to confirm we return the lowest L1 block, not the
// first-inserted entry.
require.NoError(t, db.SafeHeadUpdated(l2b, l1b))
require.NoError(t, db.SafeHeadUpdated(l2a, l1a))

actualL1, actualL2, err := db.FirstEntry(context.Background())
require.NoError(t, err)
require.Equal(t, l1a, actualL1)
require.Equal(t, l2a.ID(), actualL2)
}

func TestFirstEntry_StableAfterResetAhead(t *testing.T) {
logger := testlog.Logger(t, log.LvlInfo)
dir := t.TempDir()
db, err := NewSafeDB(logger, dir)
require.NoError(t, err)
defer db.Close()

l1a := eth.BlockID{Hash: common.Hash{0x01, 0xaa}, Number: 100}
l1b := eth.BlockID{Hash: common.Hash{0x01, 0xbb}, Number: 150}
l2a := eth.L2BlockRef{Hash: common.Hash{0x02, 0xaa}, Number: 20, L1Origin: l1a}
l2b := eth.L2BlockRef{Hash: common.Hash{0x02, 0xbb}, Number: 25, L1Origin: l1b}

require.NoError(t, db.SafeHeadUpdated(l2a, l1a))
require.NoError(t, db.SafeHeadUpdated(l2b, l1b))

// Reset to l2b truncates entries at or after l2b; the l2a entry remains
// and must still be the first.
require.NoError(t, db.SafeHeadReset(l2b))

actualL1, actualL2, err := db.FirstEntry(context.Background())
require.NoError(t, err)
require.Equal(t, l1a, actualL1)
require.Equal(t, l2a.ID(), actualL2)
}

func TestTruncateOnSafeHeadReset(t *testing.T) {
logger := testlog.Logger(t, log.LvlInfo)
dir := t.TempDir()
Expand Down
9 changes: 9 additions & 0 deletions op-node/node/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -344,3 +344,12 @@ func (m *mockSafeDBReader) SafeHeadAtL1(ctx context.Context, l1BlockNum uint64)
func (m *mockSafeDBReader) ExpectSafeHeadAtL1(l1BlockNum uint64, l1 eth.BlockID, safeHead eth.BlockID, err error) {
m.Mock.On("SafeHeadAtL1", l1BlockNum).Return(l1, safeHead, &err)
}

func (m *mockSafeDBReader) FirstEntry(ctx context.Context) (l1 eth.BlockID, l2 eth.BlockID, err error) {
r := m.Mock.MethodCalled("FirstEntry")
return r[0].(eth.BlockID), r[1].(eth.BlockID), *r[2].(*error)
}

func (m *mockSafeDBReader) ExpectFirstEntry(l1 eth.BlockID, safeHead eth.BlockID, err error) {
m.Mock.On("FirstEntry").Return(l1, safeHead, &err)
}
3 changes: 3 additions & 0 deletions op-supernode/supernode/activity/interop/algo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1073,6 +1073,9 @@ type algoMockChain struct {
func (m *algoMockChain) BlockNumberToTimestamp(ctx context.Context, blocknum uint64) (uint64, error) {
return 0, nil
}
func (m *algoMockChain) FirstSafeHeadTimestamp(ctx context.Context) (uint64, error) {
return 0, cc.ErrSafeDBEmpty
}
func (m *algoMockChain) ID() eth.ChainID { return m.id }
func (m *algoMockChain) Start(ctx context.Context) error { return nil }
func (m *algoMockChain) Stop(ctx context.Context) error { return nil }
Expand Down
Loading