Skip to content

Commit ca1f777

Browse files
Inphiajsutton
andauthored
op-supervisor: Implement supervisor_superRootAtTimestamp RPC (#13736)
* supervisor: Early start of RPC * op-supervisor: Add supervisor_superRootAtTimstamp RPC Add a new RPC to get the super root at a given timestamp. * op-supervisor: add chainID to SuperRootResponse * op-supervisor: fix comment * op-supervisor: Add super root to response * return stubbed canonical output for interop_pendingOutputV0AtTimestamp * add ref to reorg PR --------- Co-authored-by: Adrian Sutton <[email protected]>
1 parent 395c01d commit ca1f777

File tree

9 files changed

+201
-0
lines changed

9 files changed

+201
-0
lines changed

op-node/rollup/interop/managed/api.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,14 @@ func (ib *InteropAPI) ChainID(ctx context.Context) (supervisortypes.ChainID, err
5555
return ib.backend.ChainID(ctx)
5656
}
5757

58+
func (ib *InteropAPI) OutputV0AtTimestamp(ctx context.Context, timestamp uint64) (*eth.OutputV0, error) {
59+
return ib.backend.OutputV0AtTimestamp(ctx, timestamp)
60+
}
61+
62+
func (ib *InteropAPI) PendingOutputV0ATTimestamp(ctx context.Context, timestamp uint64) (*eth.OutputV0, error) {
63+
return ib.backend.PendingOutputV0AtTimestamp(ctx, timestamp)
64+
}
65+
5866
func (ib *InteropAPI) ProvideL1(ctx context.Context, nextL1 eth.BlockRef) error {
5967
return ib.backend.ProvideL1(ctx, nextL1)
6068
}

op-node/rollup/interop/managed/system.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ type L2Source interface {
2525
L2BlockRefByNumber(ctx context.Context, num uint64) (eth.L2BlockRef, error)
2626
BlockRefByNumber(ctx context.Context, num uint64) (eth.BlockRef, error)
2727
FetchReceipts(ctx context.Context, blockHash common.Hash) (eth.BlockInfo, types.Receipts, error)
28+
OutputV0AtBlock(ctx context.Context, blockHash common.Hash) (*eth.OutputV0, error)
2829
}
2930

3031
type L1Source interface {
@@ -275,3 +276,30 @@ func (m *ManagedMode) BlockRefByNumber(ctx context.Context, num uint64) (eth.Blo
275276
func (m *ManagedMode) ChainID(ctx context.Context) (supervisortypes.ChainID, error) {
276277
return supervisortypes.ChainIDFromBig(m.cfg.L2ChainID), nil
277278
}
279+
280+
func (m *ManagedMode) OutputV0AtTimestamp(ctx context.Context, timestamp uint64) (*eth.OutputV0, error) {
281+
num, err := m.cfg.TargetBlockNumber(timestamp)
282+
if err != nil {
283+
return nil, err
284+
}
285+
ref, err := m.l2.L2BlockRefByNumber(ctx, num)
286+
if err != nil {
287+
return nil, err
288+
}
289+
return m.l2.OutputV0AtBlock(ctx, ref.Hash)
290+
}
291+
292+
func (m *ManagedMode) PendingOutputV0AtTimestamp(ctx context.Context, timestamp uint64) (*eth.OutputV0, error) {
293+
num, err := m.cfg.TargetBlockNumber(timestamp)
294+
if err != nil {
295+
return nil, err
296+
}
297+
ref, err := m.l2.L2BlockRefByNumber(ctx, num)
298+
if err != nil {
299+
return nil, err
300+
}
301+
// TODO: Once interop reorgs are supported (see #13645), replace with the output root preimage of an actual pending
302+
// block contained in the optimistic block deposited transaction - https://github.com/ethereum-optimism/specs/pull/489
303+
// For now, we use the output at timestamp as-if it didn't contain invalid messages for happy path testing.
304+
return m.l2.OutputV0AtBlock(ctx, ref.Hash)
305+
}

op-service/sources/supervisor_client.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/ethereum-optimism/optimism/op-service/eth"
99
"github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types"
1010
"github.com/ethereum/go-ethereum/common"
11+
"github.com/ethereum/go-ethereum/common/hexutil"
1112
)
1213

1314
type SupervisorClient struct {
@@ -153,6 +154,16 @@ func (cl *SupervisorClient) UpdateLocalSafe(ctx context.Context, chainID types.C
153154
lastDerived)
154155
}
155156

157+
func (cl *SupervisorClient) SuperRootAtTimestamp(ctx context.Context, timestamp hexutil.Uint64) (types.SuperRootResponse, error) {
158+
var result types.SuperRootResponse
159+
err := cl.client.CallContext(
160+
ctx,
161+
&result,
162+
"supervisor_superRootAtTimestamp",
163+
timestamp)
164+
return result, err
165+
}
166+
156167
func (cl *SupervisorClient) Close() {
157168
cl.client.Close()
158169
}

op-supervisor/supervisor/backend/backend.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"slices"
78
"sync/atomic"
89

910
"github.com/ethereum/go-ethereum/common"
11+
"github.com/ethereum/go-ethereum/common/hexutil"
1012
"github.com/ethereum/go-ethereum/log"
1113

1214
"github.com/ethereum-optimism/optimism/op-service/client"
@@ -45,6 +47,7 @@ type SupervisorBackend struct {
4547
// crossProcessors are used to index cross-chain dependency validity data once the log events are indexed
4648
crossSafeProcessors locks.RWMap[types.ChainID, *cross.Worker]
4749
crossUnsafeProcessors locks.RWMap[types.ChainID, *cross.Worker]
50+
syncSources locks.RWMap[types.ChainID, syncnode.SyncSource]
4851

4952
// syncNodesController controls the derivation or reset of the sync nodes
5053
syncNodesController *syncnode.SyncNodesController
@@ -151,6 +154,10 @@ func (su *SupervisorBackend) initResources(ctx context.Context, cfg *config.Conf
151154
chainProcessor := processors.NewChainProcessor(su.logger, chainID, logProcessor, su.chainDBs, su.onIndexedLocalUnsafeData)
152155
su.chainProcessors.Set(chainID, chainProcessor)
153156
}
157+
// initialize sync sources
158+
for _, chainID := range chains {
159+
su.syncSources.Set(chainID, nil)
160+
}
154161

155162
if cfg.L1RPC != "" {
156163
if err := su.attachL1RPC(ctx, cfg.L1RPC); err != nil {
@@ -251,6 +258,10 @@ func (su *SupervisorBackend) AttachSyncNode(ctx context.Context, src syncnode.Sy
251258
if err != nil {
252259
return nil, fmt.Errorf("failed to attach sync source to processor: %w", err)
253260
}
261+
err = su.AttachSyncSource(chainID, src)
262+
if err != nil {
263+
return nil, fmt.Errorf("failed to attach sync source to node: %w", err)
264+
}
254265
return su.syncNodesController.AttachNodeController(chainID, src, noSubscribe)
255266
}
256267

@@ -263,6 +274,15 @@ func (su *SupervisorBackend) AttachProcessorSource(chainID types.ChainID, src pr
263274
return nil
264275
}
265276

277+
func (su *SupervisorBackend) AttachSyncSource(chainID types.ChainID, src syncnode.SyncSource) error {
278+
_, ok := su.syncSources.Get(chainID)
279+
if !ok {
280+
return fmt.Errorf("unknown chain %s, cannot attach RPC to sync source", chainID)
281+
}
282+
su.syncSources.Set(chainID, src)
283+
return nil
284+
}
285+
266286
func (su *SupervisorBackend) attachL1RPC(ctx context.Context, l1RPCAddr string) error {
267287
su.logger.Info("attaching L1 RPC to L1 processor", "rpc", l1RPCAddr)
268288

@@ -497,6 +517,46 @@ func (su *SupervisorBackend) L1BlockRefByNumber(ctx context.Context, number uint
497517
return su.l1Accessor.L1BlockRefByNumber(ctx, number)
498518
}
499519

520+
func (su *SupervisorBackend) SuperRootAtTimestamp(ctx context.Context, timestamp hexutil.Uint64) (types.SuperRootResponse, error) {
521+
chains := su.depSet.Chains()
522+
slices.SortFunc(chains, func(a, b types.ChainID) int {
523+
return a.Cmp(b)
524+
})
525+
chainInfos := make([]types.ChainRootInfo, len(chains))
526+
superRootChains := make([]eth.ChainIDAndOutput, len(chains))
527+
for i, chainID := range chains {
528+
src, ok := su.syncSources.Get(chainID)
529+
if !ok {
530+
su.logger.Error("bug: unknown chain %s, cannot get sync source", chainID)
531+
return types.SuperRootResponse{}, fmt.Errorf("unknown chain %s, cannot get sync source", chainID)
532+
}
533+
output, err := src.OutputV0AtTimestamp(ctx, uint64(timestamp))
534+
if err != nil {
535+
return types.SuperRootResponse{}, err
536+
}
537+
pending, err := src.PendingOutputV0AtTimestamp(ctx, uint64(timestamp))
538+
if err != nil {
539+
return types.SuperRootResponse{}, err
540+
}
541+
canonicalRoot := eth.OutputRoot(output)
542+
chainInfos[i] = types.ChainRootInfo{
543+
ChainID: chainID,
544+
Canonical: canonicalRoot,
545+
Pending: pending.Marshal(),
546+
}
547+
superRootChains[i] = eth.ChainIDAndOutput{ChainID: chainID.ToBig().Uint64(), Output: canonicalRoot}
548+
}
549+
superRoot := eth.SuperRoot(&eth.SuperV1{
550+
Timestamp: uint64(timestamp),
551+
Chains: superRootChains,
552+
})
553+
return types.SuperRootResponse{
554+
Timestamp: uint64(timestamp),
555+
SuperRoot: superRoot,
556+
Chains: chainInfos,
557+
}, nil
558+
}
559+
500560
// Update methods
501561
// ----------------------------
502562

op-supervisor/supervisor/backend/mock.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/ethereum-optimism/optimism/op-supervisor/supervisor/frontend"
1111
"github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types"
1212
"github.com/ethereum/go-ethereum/common"
13+
"github.com/ethereum/go-ethereum/common/hexutil"
1314
)
1415

1516
type MockBackend struct {
@@ -70,6 +71,10 @@ func (m *MockBackend) CrossDerivedFrom(ctx context.Context, chainID types.ChainI
7071
return eth.BlockRef{}, nil
7172
}
7273

74+
func (m *MockBackend) SuperRootAtTimestamp(ctx context.Context, timestamp hexutil.Uint64) (types.SuperRootResponse, error) {
75+
return types.SuperRootResponse{}, nil
76+
}
77+
7378
func (m *MockBackend) Close() error {
7479
return nil
7580
}

op-supervisor/supervisor/backend/syncnode/iface.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ type SyncSource interface {
2525
BlockRefByNumber(ctx context.Context, number uint64) (eth.BlockRef, error)
2626
FetchReceipts(ctx context.Context, blockHash common.Hash) (gethtypes.Receipts, error)
2727
ChainID(ctx context.Context) (types.ChainID, error)
28+
OutputV0AtTimestamp(ctx context.Context, timestamp uint64) (*eth.OutputV0, error)
29+
PendingOutputV0AtTimestamp(ctx context.Context, timestamp uint64) (*eth.OutputV0, error)
2830
// String identifies the sync source
2931
String() string
3032
}

op-supervisor/supervisor/backend/syncnode/rpc.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,18 @@ func (rs *RPCSyncNode) ChainID(ctx context.Context) (types.ChainID, error) {
6969
return chainID, err
7070
}
7171

72+
func (rs *RPCSyncNode) OutputV0AtTimestamp(ctx context.Context, timestamp uint64) (*eth.OutputV0, error) {
73+
var out *eth.OutputV0
74+
err := rs.cl.CallContext(ctx, &out, "interop_outputV0AtTimestamp", timestamp)
75+
return out, err
76+
}
77+
78+
func (rs *RPCSyncNode) PendingOutputV0AtTimestamp(ctx context.Context, timestamp uint64) (*eth.OutputV0, error) {
79+
var out *eth.OutputV0
80+
err := rs.cl.CallContext(ctx, &out, "interop_pendingOutputV0AtTimestamp", timestamp)
81+
return out, err
82+
}
83+
7284
func (rs *RPCSyncNode) String() string {
7385
return rs.name
7486
}

op-supervisor/supervisor/frontend/frontend.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"github.com/ethereum-optimism/optimism/op-service/eth"
77
"github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types"
88
"github.com/ethereum/go-ethereum/common"
9+
"github.com/ethereum/go-ethereum/common/hexutil"
910
)
1011

1112
type AdminBackend interface {
@@ -22,6 +23,7 @@ type QueryBackend interface {
2223
CrossSafe(ctx context.Context, chainID types.ChainID) (types.DerivedIDPair, error)
2324
Finalized(ctx context.Context, chainID types.ChainID) (eth.BlockID, error)
2425
FinalizedL1() eth.BlockRef
26+
SuperRootAtTimestamp(ctx context.Context, timestamp hexutil.Uint64) (types.SuperRootResponse, error)
2527
}
2628

2729
type Backend interface {
@@ -69,6 +71,10 @@ func (q *QueryFrontend) CrossDerivedFrom(ctx context.Context, chainID types.Chai
6971
return q.Supervisor.CrossDerivedFrom(ctx, chainID, derived)
7072
}
7173

74+
func (q *QueryFrontend) SuperRootAtTimestamp(ctx context.Context, timestamp hexutil.Uint64) (types.SuperRootResponse, error) {
75+
return q.Supervisor.SuperRootAtTimestamp(ctx, timestamp)
76+
}
77+
7278
type AdminFrontend struct {
7379
Supervisor Backend
7480
}

op-supervisor/supervisor/types/types.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,3 +352,72 @@ type ManagedEvent struct {
352352
DerivationUpdate *DerivedBlockRefPair `json:"derivationUpdate,omitempty"`
353353
ExhaustL1 *DerivedBlockRefPair `json:"exhaustL1,omitempty"`
354354
}
355+
356+
type ChainRootInfo struct {
357+
ChainID ChainID `json:"chainID"`
358+
// Canonical is the output root of the latest canonical block at a particular Timestamp.
359+
Canonical eth.Bytes32 `json:"canonical"`
360+
// Pending is the output root preimage for the latest block at a particular Timestamp prior to validation of
361+
// executing messages. If the original block was valid, this will be the preimage of the
362+
// output root from the Canonical array. If it was invalid, it will be the output root preimage from the
363+
// Optimistic Block Deposited Transaction added to the deposit-only block.
364+
Pending []byte `json:"pending"`
365+
}
366+
367+
type chainRootInfoMarshalling struct {
368+
ChainID ChainID `json:"chainID"`
369+
Canonical common.Hash `json:"canonical"`
370+
Pending hexutil.Bytes `json:"pending"`
371+
}
372+
373+
func (i ChainRootInfo) MarshalJSON() ([]byte, error) {
374+
return json.Marshal(&chainRootInfoMarshalling{
375+
ChainID: i.ChainID,
376+
Canonical: common.Hash(i.Canonical),
377+
Pending: i.Pending,
378+
})
379+
}
380+
381+
func (i *ChainRootInfo) UnmarshalJSON(input []byte) error {
382+
var dec chainRootInfoMarshalling
383+
if err := json.Unmarshal(input, &dec); err != nil {
384+
return err
385+
}
386+
i.ChainID = dec.ChainID
387+
i.Canonical = eth.Bytes32(dec.Canonical)
388+
i.Pending = dec.Pending
389+
return nil
390+
}
391+
392+
type SuperRootResponse struct {
393+
Timestamp uint64 `json:"timestamp"`
394+
SuperRoot eth.Bytes32 `json:"superRoot"`
395+
// Chains is the list of ChainRootInfo for each chain in the dependency set.
396+
// It represents the state of the chain at or before the Timestamp.
397+
Chains []ChainRootInfo `json:"chains"`
398+
}
399+
400+
type superRootResponseMarshalling struct {
401+
Timestamp hexutil.Uint64 `json:"timestamp"`
402+
SuperRoot common.Hash `json:"superRoot"`
403+
Chains []ChainRootInfo `json:"chains"`
404+
}
405+
406+
func (r SuperRootResponse) MarshalJSON() ([]byte, error) {
407+
return json.Marshal(&superRootResponseMarshalling{
408+
Timestamp: hexutil.Uint64(r.Timestamp),
409+
SuperRoot: common.Hash(r.SuperRoot),
410+
Chains: r.Chains,
411+
})
412+
}
413+
414+
func (r *SuperRootResponse) UnmarshalJSON(input []byte) error {
415+
var dec superRootResponseMarshalling
416+
if err := json.Unmarshal(input, &dec); err != nil {
417+
return err
418+
}
419+
r.Timestamp = uint64(dec.Timestamp)
420+
r.SuperRoot = eth.Bytes32(dec.SuperRoot)
421+
r.Chains = dec.Chains
422+
return nil
423+
}

0 commit comments

Comments
 (0)