Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ func optimisticBlockAtTimestamp(t devtest.T, queryAPI apis.SupernodeQueryAPI, ch
t.Require().NoError(err)
out, ok := resp.OptimisticAtTimestamp[chainID]
t.Require().Truef(ok, "no optimistic output for chain %v at timestamp %d", chainID, timestamp)
return interopTypes.OptimisticBlock{BlockHash: out.Output.BlockRef.Hash, OutputRoot: out.Output.OutputRoot}
return interopTypes.OptimisticBlock{BlockHash: out.Output.BlockHash, OutputRoot: out.OutputRoot}
}

// marshalTransition serializes a transition state with the given super root, step, and progress.
Expand Down
4 changes: 2 additions & 2 deletions op-challenger/game/fault/trace/super/provider_supernode.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,8 @@ func (s *SuperNodeTraceProvider) GetPreimageBytes(ctx context.Context, pos types
}

expectedState.PendingProgress = append(expectedState.PendingProgress, interopTypes.OptimisticBlock{
BlockHash: optimistic.Output.BlockRef.Hash,
OutputRoot: optimistic.Output.OutputRoot,
BlockHash: optimistic.Output.BlockHash,
OutputRoot: optimistic.OutputRoot,
})
}
return expectedState.Marshal(), nil
Expand Down
60 changes: 28 additions & 32 deletions op-challenger/game/fault/trace/super/provider_supernode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,13 +146,14 @@ func TestSuperNodeProvider_Get(t *testing.T) {
// Make super roots be safe only after L1 head
prev.Data.VerifiedRequiredL1 = eth.BlockID{Number: l1Head.Number, Hash: common.Hash{0xaa}}
next.Data.VerifiedRequiredL1 = eth.BlockID{Number: l1Head.Number + 1, Hash: common.Hash{0xbb}}
unsafeOutput := &eth.OutputV0{
StateRoot: eth.Bytes32{0xdf},
MessagePasserStorageRoot: eth.Bytes32{0xde},
BlockHash: common.Hash{0xcd},
}
next.OptimisticAtTimestamp[eth.ChainIDFromUInt64(1)] = eth.OutputWithRequiredL1{
Output: &eth.OutputResponse{
OutputRoot: eth.Bytes32{0xad},
BlockRef: eth.L2BlockRef{Hash: common.Hash{0xcd}},
WithdrawalStorageRoot: common.Hash{0xde},
StateRoot: common.Hash{0xdf},
},
Output: unsafeOutput,
OutputRoot: eth.OutputRoot(unsafeOutput),
RequiredL1: eth.BlockID{Number: l1Head.Number + 1, Hash: common.Hash{0xbb}},
}
stubSuperNode.Add(prev)
Expand All @@ -172,13 +173,14 @@ func TestSuperNodeProvider_Get(t *testing.T) {
// Make super roots be safe only after L1 head
prev.Data.VerifiedRequiredL1 = eth.BlockID{Number: l1Head.Number, Hash: common.Hash{0xaa}}
next.Data.VerifiedRequiredL1 = eth.BlockID{Number: l1Head.Number + 1, Hash: common.Hash{0xbb}}
unsafeOutput := &eth.OutputV0{
StateRoot: eth.Bytes32{0xdf},
MessagePasserStorageRoot: eth.Bytes32{0xde},
BlockHash: common.Hash{0xcd},
}
next.OptimisticAtTimestamp[eth.ChainIDFromUInt64(2)] = eth.OutputWithRequiredL1{
Output: &eth.OutputResponse{
OutputRoot: eth.Bytes32{0xad},
BlockRef: eth.L2BlockRef{Hash: common.Hash{0xcd}},
WithdrawalStorageRoot: common.Hash{0xde},
StateRoot: common.Hash{0xdf},
},
Output: unsafeOutput,
OutputRoot: eth.OutputRoot(unsafeOutput),
RequiredL1: eth.BlockID{Number: l1Head.Number + 1, Hash: common.Hash{0xbb}},
}
stubSuperNode.Add(prev)
Expand Down Expand Up @@ -422,18 +424,6 @@ func createSuperNodeProvider(t *testing.T) (*SuperNodeTraceProvider, *stubSuperN
return provider, stubSuperNode, l1Head
}

func toOutputResponse(output *eth.OutputV0) *eth.OutputResponse {
return &eth.OutputResponse{
Version: output.Version(),
OutputRoot: eth.OutputRoot(output),
BlockRef: eth.L2BlockRef{
Hash: output.BlockHash,
},
WithdrawalStorageRoot: common.Hash(output.MessagePasserStorageRoot),
StateRoot: common.Hash(output.StateRoot),
}
}

func createValidSuperNodeSuperRoots(l1Head eth.BlockID) (eth.SuperRootAtTimestampResponse, eth.SuperRootAtTimestampResponse) {
rng := rand.New(rand.NewSource(1))
outputA1 := testutils.RandomOutputV0(rng)
Expand All @@ -455,11 +445,13 @@ func createValidSuperNodeSuperRoots(l1Head eth.BlockID) (eth.SuperRootAtTimestam
ChainIDs: []eth.ChainID{chainID1, chainID2},
OptimisticAtTimestamp: map[eth.ChainID]eth.OutputWithRequiredL1{
chainID1: {
Output: toOutputResponse(outputA1),
Output: outputA1,
OutputRoot: eth.OutputRoot(outputA1),
RequiredL1: l1Head,
},
chainID2: {
Output: toOutputResponse(outputB1),
Output: outputB1,
OutputRoot: eth.OutputRoot(outputB1),
RequiredL1: l1Head,
},
},
Expand All @@ -474,11 +466,13 @@ func createValidSuperNodeSuperRoots(l1Head eth.BlockID) (eth.SuperRootAtTimestam
ChainIDs: []eth.ChainID{chainID1, chainID2},
OptimisticAtTimestamp: map[eth.ChainID]eth.OutputWithRequiredL1{
chainID1: {
Output: toOutputResponse(outputA2),
Output: outputA2,
OutputRoot: eth.OutputRoot(outputA2),
RequiredL1: l1Head,
},
chainID2: {
Output: toOutputResponse(outputB2),
Output: outputB2,
OutputRoot: eth.OutputRoot(outputB2),
RequiredL1: l1Head,
},
},
Expand All @@ -492,13 +486,15 @@ func createValidSuperNodeSuperRoots(l1Head eth.BlockID) (eth.SuperRootAtTimestam
}

func expectSuperNodeValidTransition(t *testing.T, provider *SuperNodeTraceProvider, prev eth.SuperRootAtTimestampResponse, next eth.SuperRootAtTimestampResponse) {
chain1 := next.OptimisticAtTimestamp[eth.ChainIDFromUInt64(1)]
chain1OptimisticBlock := interopTypes.OptimisticBlock{
BlockHash: next.OptimisticAtTimestamp[eth.ChainIDFromUInt64(1)].Output.BlockRef.Hash,
OutputRoot: next.OptimisticAtTimestamp[eth.ChainIDFromUInt64(1)].Output.OutputRoot,
BlockHash: chain1.Output.BlockHash,
OutputRoot: chain1.OutputRoot,
}
chain2 := next.OptimisticAtTimestamp[eth.ChainIDFromUInt64(2)]
chain2OptimisticBlock := interopTypes.OptimisticBlock{
BlockHash: next.OptimisticAtTimestamp[eth.ChainIDFromUInt64(2)].Output.BlockRef.Hash,
OutputRoot: next.OptimisticAtTimestamp[eth.ChainIDFromUInt64(2)].Output.OutputRoot,
BlockHash: chain2.Output.BlockHash,
OutputRoot: chain2.OutputRoot,
}
expectedFirstStep := &interopTypes.TransitionState{
SuperRoot: prev.Data.Super.Marshal(),
Expand Down
7 changes: 4 additions & 3 deletions op-service/eth/superroot_at_timestamp.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ package eth

import "encoding/json"

// OutputWithRequiredL1 is the full Output and its source L1 block
// OutputWithRequiredL1 is the OutputV0 pre-image, its hash, and its source L1 block
type OutputWithRequiredL1 struct {
Output *OutputResponse `json:"output"`
RequiredL1 BlockID `json:"required_l1"`
Output *OutputV0 `json:"output"`
OutputRoot Bytes32 `json:"output_root"`
RequiredL1 BlockID `json:"required_l1"`
}

type SuperRootResponseData struct {
Expand Down
48 changes: 14 additions & 34 deletions op-service/sources/supernode_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ func TestSuperNodeClient_SuperRootAtTimestamp(t *testing.T) {

chainA := eth.ChainIDFromUInt64(1)
chainB := eth.ChainIDFromUInt64(4)
chainAOutput := &eth.OutputV0{
StateRoot: eth.Bytes32{0xaa},
MessagePasserStorageRoot: eth.Bytes32{0xff},
BlockHash: common.Hash{0x22},
}
expected := eth.SuperRootAtTimestampResponse{
CurrentL1: eth.BlockID{
Number: 305,
Expand All @@ -32,23 +37,8 @@ func TestSuperNodeClient_SuperRootAtTimestamp(t *testing.T) {
ChainIDs: []eth.ChainID{chainA, chainB},
OptimisticAtTimestamp: map[eth.ChainID]eth.OutputWithRequiredL1{
chainA: {
Output: &eth.OutputResponse{
Version: eth.Bytes32{0x01},
OutputRoot: eth.Bytes32{0x11, 0x12},
BlockRef: eth.L2BlockRef{
Hash: common.Hash{0x22},
Number: 472,
ParentHash: common.Hash{0xdd},
Time: 9895839,
L1Origin: eth.BlockID{
Hash: common.Hash{0xee},
Number: 9802,
},
SequenceNumber: 4982,
},
WithdrawalStorageRoot: common.Hash{0xff},
StateRoot: common.Hash{0xaa},
},
Output: chainAOutput,
OutputRoot: eth.OutputRoot(chainAOutput),
RequiredL1: eth.BlockID{
Hash: common.Hash{0xbb},
Number: 7842,
Expand Down Expand Up @@ -89,6 +79,11 @@ func TestSuperNodeClient_SuperRootAtTimestamp(t *testing.T) {

chainA := eth.ChainIDFromUInt64(1)
chainB := eth.ChainIDFromUInt64(4)
chainAOutput := &eth.OutputV0{
StateRoot: eth.Bytes32{0xaa},
MessagePasserStorageRoot: eth.Bytes32{0xff},
BlockHash: common.Hash{0x22},
}
expected := eth.SuperRootAtTimestampResponse{
CurrentL1: eth.BlockID{
Number: 305,
Expand All @@ -97,23 +92,8 @@ func TestSuperNodeClient_SuperRootAtTimestamp(t *testing.T) {
ChainIDs: []eth.ChainID{chainA, chainB},
OptimisticAtTimestamp: map[eth.ChainID]eth.OutputWithRequiredL1{
chainA: {
Output: &eth.OutputResponse{
Version: eth.Bytes32{0x01},
OutputRoot: eth.Bytes32{0x11, 0x12},
BlockRef: eth.L2BlockRef{
Hash: common.Hash{0x22},
Number: 472,
ParentHash: common.Hash{0xdd},
Time: 9895839,
L1Origin: eth.BlockID{
Hash: common.Hash{0xee},
Number: 9802,
},
SequenceNumber: 4982,
},
WithdrawalStorageRoot: common.Hash{0xff},
StateRoot: common.Hash{0xaa},
},
Output: chainAOutput,
OutputRoot: eth.OutputRoot(chainAOutput),
RequiredL1: eth.BlockID{
Hash: common.Hash{0xbb},
Number: 7842,
Expand Down
2 changes: 1 addition & 1 deletion op-supernode/supernode/activity/interop/algo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -923,7 +923,7 @@ func (m *algoMockChain) OptimisticAt(ctx context.Context, ts uint64) (eth.BlockI
func (m *algoMockChain) OutputRootAtL2BlockNumber(ctx context.Context, l2BlockNum uint64) (eth.Bytes32, error) {
return eth.Bytes32{}, nil
}
func (m *algoMockChain) OptimisticOutputAtTimestamp(ctx context.Context, ts uint64) (*eth.OutputResponse, error) {
func (m *algoMockChain) OptimisticOutputAtTimestamp(ctx context.Context, ts uint64) (*eth.OutputV0, error) {
return nil, nil
}
func (m *algoMockChain) FetchReceipts(ctx context.Context, blockID eth.BlockID) (eth.BlockInfo, types.Receipts, error) {
Expand Down
2 changes: 1 addition & 1 deletion op-supernode/supernode/activity/interop/interop_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1403,7 +1403,7 @@ func (m *mockChainContainer) OptimisticAt(ctx context.Context, ts uint64) (eth.B
func (m *mockChainContainer) OutputRootAtL2BlockNumber(ctx context.Context, l2BlockNum uint64) (eth.Bytes32, error) {
return eth.Bytes32{}, nil
}
func (m *mockChainContainer) OptimisticOutputAtTimestamp(ctx context.Context, ts uint64) (*eth.OutputResponse, error) {
func (m *mockChainContainer) OptimisticOutputAtTimestamp(ctx context.Context, ts uint64) (*eth.OutputV0, error) {
return nil, nil
}
func (m *mockChainContainer) FetchReceipts(ctx context.Context, blockID eth.BlockID) (eth.BlockInfo, types.Receipts, error) {
Expand Down
4 changes: 2 additions & 2 deletions op-supernode/supernode/activity/supernode/supernode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ func (m *mockCC) OutputRootAtL2BlockNumber(ctx context.Context, l2BlockNum uint6
return eth.Bytes32{}, nil
}

func (m *mockCC) OptimisticOutputAtTimestamp(ctx context.Context, ts uint64) (*eth.OutputResponse, error) {
return &eth.OutputResponse{}, nil
func (m *mockCC) OptimisticOutputAtTimestamp(ctx context.Context, ts uint64) (*eth.OutputV0, error) {
return &eth.OutputV0{}, nil
}

func (m *mockCC) RewindEngine(ctx context.Context, timestamp uint64, invalidatedBlock eth.BlockRef) error {
Expand Down
1 change: 1 addition & 0 deletions op-supernode/supernode/activity/superroot/superroot.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ func (s *Superroot) atTimestamp(ctx context.Context, timestamp uint64) (eth.Supe
}
optimistic[chainID] = eth.OutputWithRequiredL1{
Output: optimisticOut,
OutputRoot: eth.OutputRoot(optimisticOut),
RequiredL1: optimisticL1,
}
}
Expand Down
5 changes: 2 additions & 3 deletions op-supernode/supernode/activity/superroot/superroot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,11 @@ func (m *mockCC) OutputRootAtL2BlockNumber(ctx context.Context, l2BlockNum uint6
}
return m.output, nil
}
func (m *mockCC) OptimisticOutputAtTimestamp(ctx context.Context, ts uint64) (*eth.OutputResponse, error) {
func (m *mockCC) OptimisticOutputAtTimestamp(ctx context.Context, ts uint64) (*eth.OutputV0, error) {
if m.optimisticErr != nil {
return nil, m.optimisticErr
}
// Return minimal output response; tests only assert presence/count
return &eth.OutputResponse{}, nil
return &eth.OutputV0{}, nil
}
func (m *mockCC) RewindEngine(ctx context.Context, timestamp uint64, invalidatedBlock eth.BlockRef) error {
return nil
Expand Down
34 changes: 19 additions & 15 deletions op-supernode/supernode/chain_container/chain_container.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ type ChainContainer interface {
VerifiedAt(ctx context.Context, ts uint64) (l2, l1 eth.BlockID, err error)
OptimisticAt(ctx context.Context, ts uint64) (l2, l1 eth.BlockID, err error)
OutputRootAtL2BlockNumber(ctx context.Context, l2BlockNum uint64) (eth.Bytes32, error)
OptimisticOutputAtTimestamp(ctx context.Context, ts uint64) (*eth.OutputResponse, error)
OptimisticOutputAtTimestamp(ctx context.Context, ts uint64) (*eth.OutputV0, error)
// RewindEngine rewinds the engine to the highest block with timestamp less than or equal to the given timestamp.
// invalidatedBlock is the block that triggered the rewind and is passed to reset callbacks.
// WARNING: this is a dangerous stateful operation and is intended to be called only
Expand Down Expand Up @@ -465,23 +465,27 @@ func (c *simpleChainContainer) OptimisticAt(ctx context.Context, ts uint64) (l2,
return l2Block.ID(), l1Block, nil
}

// OptimisticOutputAtTimestamp returns the full Output for the optimistic L2 block at the given timestamp.
// For now this simply calls the op-node's normal OutputAtBlock for the block number computed from the timestamp.
func (c *simpleChainContainer) OptimisticOutputAtTimestamp(ctx context.Context, ts uint64) (*eth.OutputResponse, error) {
if c.rollupClient == nil {
return nil, fmt.Errorf("rollup client not initialized")
}
// Determine the optimistic L2 block at timestamp (currently same as safe block at ts)
l2Block, err := c.LocalSafeBlockAtTimestamp(ctx, ts)
// OptimisticOutputAtTimestamp returns the OutputV0 for the "optimistic" L2 block at the given timestamp.
// If the block at this height has been denied (invalidated and replaced), the optimistic output
// is the original (pre-replacement) block's output from the deny list — because optimistically
// the block would not have been replaced. Otherwise it returns the current local safe block's output.
func (c *simpleChainContainer) OptimisticOutputAtTimestamp(ctx context.Context, ts uint64) (*eth.OutputV0, error) {
blockNum, err := c.TimestampToBlockNumber(ctx, ts)
if err != nil {
return nil, fmt.Errorf("failed to resolve L2 block at timestamp: %w", err)
return nil, fmt.Errorf("failed to convert timestamp to block number: %w", err)
}
// Call the standard OutputAtBlock RPC
out, err := c.rollupClient.OutputAtBlock(ctx, l2Block.Number)
if err != nil {
return nil, fmt.Errorf("failed to get output at block %d: %w", l2Block.Number, err)

if c.denyList != nil {
outV0, err := c.denyList.LastDeniedOutputV0(blockNum)
if err != nil {
return nil, fmt.Errorf("failed to query deny list at height %d: %w", blockNum, err)
}
if outV0 != nil {
return outV0, nil
}
}
return out, nil

return c.OutputV0AtBlockNumber(ctx, blockNum)
}

// FetchReceipts fetches the receipts for a given block by hash.
Expand Down
Loading
Loading