Skip to content
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
22 changes: 22 additions & 0 deletions op-service/eth/super_root.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,28 @@ func (o *SuperV1) Marshal() []byte {
return buf
}

func (o *SuperV1) MarshalJSON() ([]byte, error) {
return json.Marshal(&superV1JsonMarshalling{
Timestamp: hexutil.Uint64(o.Timestamp),
Chains: o.Chains,
})
}

func (o *SuperV1) UnmarshalJSON(input []byte) error {
var dec superV1JsonMarshalling
if err := json.Unmarshal(input, &dec); err != nil {
return err
}
o.Timestamp = uint64(dec.Timestamp)
o.Chains = dec.Chains
return nil
}

type superV1JsonMarshalling struct {
Timestamp hexutil.Uint64 `json:"timestamp"`
Chains []ChainIDAndOutput `json:"chains"`
}

func UnmarshalSuperRoot(data []byte) (Super, error) {
if len(data) < 1 {
return nil, ErrInvalidSuperRoot
Expand Down
28 changes: 28 additions & 0 deletions op-service/eth/super_root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package eth

import (
"encoding/binary"
"encoding/json"
"testing"

"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -77,6 +78,33 @@ func TestSuperRootV1Codec(t *testing.T) {
})
}

func TestSuperRootV1JSON(t *testing.T) {
t.Run("UseHexForTimestamp", func(t *testing.T) {
chainA := ChainIDAndOutput{ChainID: ChainIDFromUInt64(11), Output: Bytes32{0x01}}
superRoot := NewSuperV1(7000, chainA)
jsonData, err := json.Marshal(superRoot)
require.NoError(t, err)

values := make(map[string]any)
err = json.Unmarshal(jsonData, &values)
require.NoError(t, err)
require.Equal(t, "0x1b58", values["timestamp"])
})

t.Run("RoundTrip", func(t *testing.T) {
chainA := ChainIDAndOutput{ChainID: ChainIDFromUInt64(11), Output: Bytes32{0x01}}
chainB := ChainIDAndOutput{ChainID: ChainIDFromUInt64(12), Output: Bytes32{0x02}}
chainC := ChainIDAndOutput{ChainID: ChainIDFromUInt64(13), Output: Bytes32{0x03}}
superRoot := NewSuperV1(7000, chainA, chainB, chainC)
data, err := json.Marshal(superRoot)
require.NoError(t, err)
var actual SuperV1
err = json.Unmarshal(data, &actual)
require.NoError(t, err)
require.Equal(t, superRoot, &actual)
})
}

func TestResponseToSuper(t *testing.T) {
t.Run("SingleChain", func(t *testing.T) {
input := SuperRootResponse{
Expand Down
35 changes: 35 additions & 0 deletions op-service/eth/superroot_at_timestamp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package eth

// OutputWithRequiredL1 is the full Output and its source L1 block
type OutputWithRequiredL1 struct {
Output *OutputResponse `json:"output"`
RequiredL1 BlockID `json:"required_l1"`
}

type SuperRootResponseData struct {

// VerifiedRequiredL1 is the minimum L1 block including the required data to fully verify all blocks at this timestamp
VerifiedRequiredL1 BlockID `json:"verified_required_l1"`

// Super is the unhashed data for the superroot at the given timestamp after all verification is applied.
Super Super `json:"super"`

// SuperRoot is the superroot at the given timestamp after all verification is applied.
SuperRoot Bytes32 `json:"super_root"`
}

// AtTimestampResponse is the response superroot_atTimestamp
type SuperRootAtTimestampResponse struct {
// CurrentL1 is the highest L1 block that has been fully derived and verified by all chains.
CurrentL1 BlockID `json:"current_l1"`

// OptimisticAtTimestamp is the L2 block that would be applied if verification were assumed to be successful,
// and the minimum L1 block required to derive them. If Data is nil, some chains may be absent from this map,
// indicating that there is no optimistic block for the chain at the requested timestamp that can be derived
// from the L1 data currently processed.
OptimisticAtTimestamp map[ChainID]OutputWithRequiredL1 `json:"optimistic_at_timestamp"`

// Data provides information about the super root at the requested timestamp if present. If block data at the
// requested timestamp is not present, the data will be nil.
Data *SuperRootResponseData `json:"data,omitempty"`
}
103 changes: 36 additions & 67 deletions op-supernode/supernode/activity/superroot/superroot.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package superroot

import (
"context"
"errors"
"fmt"

"github.com/ethereum-optimism/optimism/op-service/eth"
Expand Down Expand Up @@ -32,77 +33,43 @@ func (s *Superroot) RPCService() interface{} { return &superrootAPI{s: s} }

type superrootAPI struct{ s *Superroot }

// OutputWithSource is the full Output and its source L1 block
type OutputWithSource struct {
Output *eth.OutputResponse
SourceL1 eth.BlockID
}

// L2WithRequiredL1 is a verified L2 block and the minimum L1 block at which the verification is possible
type L2WithRequiredL1 struct {
L2 eth.BlockID
MinRequiredL1 eth.BlockID
}

// atTimestampResponse is the response superroot_atTimestamp
// it contains:
// - CurrentL1Derived: the current L1 block that each chain has derived up to (without any verification)
// - CurrentL1Verified: the current L1 block that each verifier has processed up to
// - VerifiedAtTimestamp: the L2 blocks which are fully verified at the given timestamp, and the minimum L1 block at which verification is possible
// - OptimisticAtTimestamp: the L2 blocks which would be applied if verification were assumed to be successful, and their L1 sources
// - SuperRoot: the superroot at the given timestamp using verified L2 blocks
type atTimestampResponse struct {
CurrentL1Derived map[eth.ChainID]eth.BlockID
CurrentL1Verified map[string]eth.BlockID
VerifiedAtTimestamp map[eth.ChainID]L2WithRequiredL1
OptimisticAtTimestamp map[eth.ChainID]OutputWithSource
MinCurrentL1 eth.BlockID
MinVerifiedRequiredL1 eth.BlockID
SuperRoot eth.Bytes32
}

// AtTimestamp computes the super-root at the given timestamp, plus additional information about the current L1s, verified L2s, and optimistic L2s
func (api *superrootAPI) AtTimestamp(ctx context.Context, timestamp uint64) (atTimestampResponse, error) {
func (api *superrootAPI) AtTimestamp(ctx context.Context, timestamp uint64) (eth.SuperRootAtTimestampResponse, error) {
return api.s.atTimestamp(ctx, timestamp)
}

func (s *Superroot) atTimestamp(ctx context.Context, timestamp uint64) (atTimestampResponse, error) {
currentL1Derived := map[eth.ChainID]eth.BlockID{}
// there are no Verification Activities yet, so there is no call to make to collect their CurrentL1
// this will be replaced with a call to the Verification Activities when they are implemented
currentL1Verified := map[string]eth.BlockID{}
verified := map[eth.ChainID]L2WithRequiredL1{}
optimistic := map[eth.ChainID]OutputWithSource{}
func (s *Superroot) atTimestamp(ctx context.Context, timestamp uint64) (eth.SuperRootAtTimestampResponse, error) {
optimistic := map[eth.ChainID]eth.OutputWithRequiredL1{}
minCurrentL1 := eth.BlockID{}
minVerifiedRequiredL1 := eth.BlockID{}
chainOutputs := make([]eth.ChainIDAndOutput, 0, len(s.chains))

// get current l1s
// Get current l1s
// this informs callers that the chains local views have considered at least up to this L1 block
// but does not guarantee verifiers have processed this L1 block yet. This field is likely unhelpful, but I await feedback to confirm
// TODO(#18651): Currently there are no verifiers to consider, but once there are, this needs to be updated to consider if
// they have also processed the L1 data.
for chainID, chain := range s.chains {
currentL1, err := chain.CurrentL1(ctx)
if err != nil {
s.log.Warn("failed to get current L1", "chain_id", chainID.String(), "err", err)
return atTimestampResponse{}, err
return eth.SuperRootAtTimestampResponse{}, err
}
currentL1Derived[chainID] = currentL1.ID()
if currentL1.ID().Number < minCurrentL1.Number || minCurrentL1 == (eth.BlockID{}) {
minCurrentL1 = currentL1.ID()
}
}

notFound := false
// collect verified and optimistic L2 and L1 blocks at the given timestamp
for chainID, chain := range s.chains {
// verifiedAt returns the L2 block which is fully verified at the given timestamp, and the minimum L1 block at which verification is possible
verifiedL2, verifiedL1, err := chain.VerifiedAt(ctx, timestamp)
if err != nil {
s.log.Warn("failed to get verified L1", "chain_id", chainID.String(), "err", err)
return atTimestampResponse{}, fmt.Errorf("%w: %w", ethereum.NotFound, err)
}
verified[chainID] = L2WithRequiredL1{
L2: verifiedL2,
MinRequiredL1: verifiedL1,
if errors.Is(err, ethereum.NotFound) {
notFound = true
continue // To allow other chains to populate unverified blocks
} else if err != nil {
s.log.Warn("failed to get verified block", "chain_id", chainID.String(), "err", err)
return eth.SuperRootAtTimestampResponse{}, fmt.Errorf("failed to get verified block: %w", err)
}
if verifiedL1.Number < minVerifiedRequiredL1.Number || minVerifiedRequiredL1 == (eth.BlockID{}) {
minVerifiedRequiredL1 = verifiedL1
Expand All @@ -111,38 +78,40 @@ func (s *Superroot) atTimestamp(ctx context.Context, timestamp uint64) (atTimest
outRoot, err := chain.OutputRootAtL2BlockNumber(ctx, verifiedL2.Number)
if err != nil {
s.log.Warn("failed to compute output root at L2 block", "chain_id", chainID.String(), "l2_number", verifiedL2.Number, "err", err)
return atTimestampResponse{}, fmt.Errorf("%w: %w", ethereum.NotFound, err)
return eth.SuperRootAtTimestampResponse{}, fmt.Errorf("failed to compute output root at L2 block %d for chain ID %v: %w", verifiedL2.Number, chainID, err)
}
chainOutputs = append(chainOutputs, eth.ChainIDAndOutput{ChainID: chainID, Output: outRoot})
// Optimistic output is the full output at the optimistic L2 block for the timestamp
optimisticOut, err := chain.OptimisticOutputAtTimestamp(ctx, timestamp)
if err != nil {
s.log.Warn("failed to get optimistic L1", "chain_id", chainID.String(), "err", err)
return atTimestampResponse{}, fmt.Errorf("%w: %w", ethereum.NotFound, err)
s.log.Warn("failed to get optimistic block", "chain_id", chainID.String(), "err", err)
return eth.SuperRootAtTimestampResponse{}, fmt.Errorf("failed to get optimistic block at timestamp %v for chain ID %v: %w", timestamp, chainID, err)
}
// Also include the source L1 for context
_, optimisticL1, err := chain.OptimisticAt(ctx, timestamp)
if err != nil {
s.log.Warn("failed to get optimistic source L1", "chain_id", chainID.String(), "err", err)
return atTimestampResponse{}, fmt.Errorf("%w: %w", ethereum.NotFound, err)
return eth.SuperRootAtTimestampResponse{}, fmt.Errorf("failed to get optimistic source L1 at timestamp %v for chain ID %v: %w", timestamp, chainID, err)
}
optimistic[chainID] = OutputWithSource{
Output: optimisticOut,
SourceL1: optimisticL1,
optimistic[chainID] = eth.OutputWithRequiredL1{
Output: optimisticOut,
RequiredL1: optimisticL1,
}
}

// Build super root from collected outputs
superV1 := eth.NewSuperV1(timestamp, chainOutputs...)
superRoot := eth.SuperRoot(superV1)

return atTimestampResponse{
CurrentL1Derived: currentL1Derived,
CurrentL1Verified: currentL1Verified,
VerifiedAtTimestamp: verified,
response := eth.SuperRootAtTimestampResponse{
CurrentL1: minCurrentL1,
OptimisticAtTimestamp: optimistic,
MinCurrentL1: minCurrentL1,
MinVerifiedRequiredL1: minVerifiedRequiredL1,
SuperRoot: superRoot,
}, nil
}
if !notFound {
// Build super root from collected outputs
superV1 := eth.NewSuperV1(timestamp, chainOutputs...)
superRoot := eth.SuperRoot(superV1)
response.Data = &eth.SuperRootResponseData{
VerifiedRequiredL1: minVerifiedRequiredL1,
Super: superV1,
SuperRoot: superRoot,
}
}
return response, nil
}
37 changes: 29 additions & 8 deletions op-supernode/supernode/activity/superroot/superroot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/ethereum-optimism/optimism/op-service/eth"
cc "github.com/ethereum-optimism/optimism/op-supernode/supernode/chain_container"
"github.com/ethereum/go-ethereum"
gethlog "github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -97,14 +98,12 @@ func TestSuperroot_AtTimestamp_Succeeds(t *testing.T) {
api := &superrootAPI{s: s}
out, err := api.AtTimestamp(context.Background(), 123)
require.NoError(t, err)
require.Len(t, out.CurrentL1Derived, 2)
require.Len(t, out.VerifiedAtTimestamp, 2)
require.Len(t, out.OptimisticAtTimestamp, 2)
// min values
require.Equal(t, uint64(2000), out.MinCurrentL1.Number)
require.Equal(t, uint64(1000), out.MinVerifiedRequiredL1.Number)
require.Equal(t, uint64(2000), out.CurrentL1.Number)
require.Equal(t, uint64(1000), out.Data.VerifiedRequiredL1.Number)
// With zero outputs, the superroot will be deterministic, just ensure it's set
_ = out.SuperRoot
_ = out.Data.SuperRoot
}

func TestSuperroot_AtTimestamp_ComputesSuperRoot(t *testing.T) {
Expand Down Expand Up @@ -141,7 +140,7 @@ func TestSuperroot_AtTimestamp_ComputesSuperRoot(t *testing.T) {
{ChainID: eth.ChainIDFromUInt64(420), Output: out2},
}
expected := eth.SuperRoot(eth.NewSuperV1(ts, chainOutputs...))
require.Equal(t, expected, resp.SuperRoot)
require.Equal(t, expected, resp.Data.SuperRoot)
}

func TestSuperroot_AtTimestamp_ErrorOnCurrentL1(t *testing.T) {
Expand Down Expand Up @@ -170,6 +169,30 @@ func TestSuperroot_AtTimestamp_ErrorOnVerifiedAt(t *testing.T) {
require.Error(t, err)
}

func TestSuperroot_AtTimestamp_NotFoundOnVerifiedAt(t *testing.T) {
t.Parallel()
chains := map[eth.ChainID]cc.ChainContainer{
eth.ChainIDFromUInt64(10): &mockCC{
verifiedErr: fmt.Errorf("nope: %w", ethereum.NotFound),
},
eth.ChainIDFromUInt64(11): &mockCC{
verL2: eth.BlockID{Number: 200},
verL1: eth.BlockID{Number: 1100},
optL2: eth.BlockID{Number: 200},
optL1: eth.BlockID{Number: 1100},
output: eth.Bytes32{0x12},
currentL1: eth.BlockRef{Number: 2100},
},
}
s := New(gethlog.New(), chains)
api := &superrootAPI{s: s}
actual, err := api.AtTimestamp(context.Background(), 123)
require.NoError(t, err)
require.Nil(t, actual.Data)
require.NotContains(t, actual.OptimisticAtTimestamp, eth.ChainIDFromUInt64(10))
require.Contains(t, actual.OptimisticAtTimestamp, eth.ChainIDFromUInt64(11))
}

func TestSuperroot_AtTimestamp_ErrorOnOutputRoot(t *testing.T) {
t.Parallel()
chains := map[eth.ChainID]cc.ChainContainer{
Expand Down Expand Up @@ -206,8 +229,6 @@ func TestSuperroot_AtTimestamp_EmptyChains(t *testing.T) {
api := &superrootAPI{s: s}
out, err := api.AtTimestamp(context.Background(), 123)
require.NoError(t, err)
require.Len(t, out.CurrentL1Derived, 0)
require.Len(t, out.VerifiedAtTimestamp, 0)
require.Len(t, out.OptimisticAtTimestamp, 0)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ func (c *simpleChainContainer) CurrentL1(ctx context.Context) (eth.BlockRef, err
}

// VerifiedAt returns the verified L2 and L1 blocks for the given L2 timestamp.
// Must return ethereum.NotFound if there is no safe block at the specified timestamp.
func (c *simpleChainContainer) VerifiedAt(ctx context.Context, ts uint64) (l2, l1 eth.BlockID, err error) {
l2Block, err := c.SafeBlockAtTimestamp(ctx, ts)
if err != nil {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ import (
"github.com/ethereum-optimism/optimism/op-service/eth"
opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics"
"github.com/ethereum-optimism/optimism/op-service/sources"
"github.com/ethereum/go-ethereum"
gethlog "github.com/ethereum/go-ethereum/log"
)

// EngineController abstracts access to the L2 execution layer
type EngineController interface {
// SafeBlockAtTimestamp returns the L2 block ref for the block at or before the given timestamp,
// clamped to the current SAFE head.
// Must return ethereum.NotFound if there is no safe block at the specified timestamp.
SafeBlockAtTimestamp(ctx context.Context, ts uint64) (eth.L2BlockRef, error)
// OutputV0AtBlockNumber returns the output preimage for the given L2 block number.
OutputV0AtBlockNumber(ctx context.Context, num uint64) (*eth.OutputV0, error)
Expand Down Expand Up @@ -60,9 +62,10 @@ func NewEngineControllerFromConfig(ctx context.Context, log gethlog.Logger, vncf
var (
ErrNoEngineClient = errors.New("engine client not initialized")
ErrNoRollupConfig = errors.New("rollup config not available")
ErrNotFound = errors.New("not found")
)

// SafeBlockAtTimestamp returns the L2 block ref for the block at or before the given timestamp,
// clamped to the current SAFE head. Must return ethereum.NotFound if no safe block is available at the timestamp.
func (e *simpleEngineController) SafeBlockAtTimestamp(ctx context.Context, ts uint64) (eth.L2BlockRef, error) {
if e.l2 == nil {
return eth.L2BlockRef{}, ErrNoEngineClient
Expand All @@ -81,7 +84,7 @@ func (e *simpleEngineController) SafeBlockAtTimestamp(ctx context.Context, ts ui
}
if num > safeHead.Number {
e.log.Warn("engine_controller: target block number exceeds safe head", "targetBlockNumber", num, "safeHead", safeHead.Number)
return eth.L2BlockRef{}, ErrNotFound
return eth.L2BlockRef{}, ethereum.NotFound
}
e.log.Debug("engine_controller: computed safe block number from timestamp",
"timestamp", ts, "targetBlockNumber", num, "safeHead", safeHead.Number, "safeHeadErr", err)
Expand Down
Loading