Skip to content

Commit 7719c5a

Browse files
authored
op-program: Implement initial block derivation step for interop fault proofs (#13675)
* op-service: Define the SuperRoot type * op-program: Add chain ID to super chain output roots * Rename * op-e2e: Add interop fault proofs actions test * op-program: Update to include chain ID * op-program: Implement initial block derivation step for interop fault proofs * op-program: Update to include chain ID * op-program: Use validate flag correctly. Rename interop env var.
1 parent e6c9bb7 commit 7719c5a

File tree

21 files changed

+386
-95
lines changed

21 files changed

+386
-95
lines changed

op-e2e/faultproofs/preimages_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ import (
66
"testing"
77

88
op_e2e "github.com/ethereum-optimism/optimism/op-e2e"
9+
"github.com/ethereum-optimism/optimism/op-program/client/boot"
910

1011
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/disputegame"
1112
preimage "github.com/ethereum-optimism/optimism/op-preimage"
12-
"github.com/ethereum-optimism/optimism/op-program/client"
1313
"github.com/ethereum/go-ethereum/common"
1414
"github.com/stretchr/testify/require"
1515
)
@@ -19,10 +19,10 @@ func TestLocalPreimages(t *testing.T) {
1919
tests := []struct {
2020
key preimage.Key
2121
}{
22-
{key: client.L1HeadLocalIndex},
23-
{key: client.L2OutputRootLocalIndex},
24-
{key: client.L2ClaimLocalIndex},
25-
{key: client.L2ClaimBlockNumberLocalIndex},
22+
{key: boot.L1HeadLocalIndex},
23+
{key: boot.L2OutputRootLocalIndex},
24+
{key: boot.L2ClaimLocalIndex},
25+
{key: boot.L2ClaimBlockNumberLocalIndex},
2626
// We don't check client.L2ChainIDLocalIndex because e2e tests use a custom chain configuration
2727
// which requires using a custom chain ID indicator so op-program will load the full rollup config and
2828
// genesis from the preimage oracle

op-program/client/boot.go renamed to op-program/client/boot/boot.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package client
1+
package boot
22

33
import (
44
"encoding/binary"

op-program/client/boot_test.go renamed to op-program/client/boot/boot_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package client
1+
package boot
22

33
import (
44
"encoding/binary"
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package interop
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
7+
"github.com/ethereum-optimism/optimism/op-node/rollup"
8+
"github.com/ethereum-optimism/optimism/op-program/client/boot"
9+
"github.com/ethereum-optimism/optimism/op-program/client/claim"
10+
"github.com/ethereum-optimism/optimism/op-program/client/interop/types"
11+
"github.com/ethereum-optimism/optimism/op-program/client/l1"
12+
"github.com/ethereum-optimism/optimism/op-program/client/l2"
13+
"github.com/ethereum-optimism/optimism/op-program/client/tasks"
14+
"github.com/ethereum-optimism/optimism/op-service/eth"
15+
"github.com/ethereum/go-ethereum/common"
16+
"github.com/ethereum/go-ethereum/log"
17+
"github.com/ethereum/go-ethereum/params"
18+
)
19+
20+
var (
21+
ErrIncorrectOutputRootType = errors.New("incorrect output root type")
22+
)
23+
24+
type taskExecutor interface {
25+
RunDerivation(
26+
logger log.Logger,
27+
rollupCfg *rollup.Config,
28+
l2ChainConfig *params.ChainConfig,
29+
l1Head common.Hash,
30+
agreedOutputRoot eth.Bytes32,
31+
claimedBlockNumber uint64,
32+
l1Oracle l1.Oracle,
33+
l2Oracle l2.Oracle) (tasks.DerivationResult, error)
34+
}
35+
36+
func RunInteropProgram(logger log.Logger, bootInfo *boot.BootInfo, l1PreimageOracle l1.Oracle, l2PreimageOracle l2.Oracle, validate bool) error {
37+
return runInteropProgram(logger, bootInfo, l1PreimageOracle, l2PreimageOracle, validate, &interopTaskExecutor{})
38+
}
39+
40+
func runInteropProgram(logger log.Logger, bootInfo *boot.BootInfo, l1PreimageOracle l1.Oracle, l2PreimageOracle l2.Oracle, validate bool, tasks taskExecutor) error {
41+
logger.Info("Interop Program Bootstrapped", "bootInfo", bootInfo)
42+
43+
// For the first step in a timestamp, we would get a SuperRoot as the agreed claim - TransitionStateByRoot will
44+
// automatically convert it to a TransitionState with Step: 0.
45+
transitionState := l2PreimageOracle.TransitionStateByRoot(bootInfo.L2OutputRoot)
46+
if transitionState.Version() != types.IntermediateTransitionVersion {
47+
return fmt.Errorf("%w: %v", ErrIncorrectOutputRootType, transitionState.Version())
48+
}
49+
50+
super, err := eth.UnmarshalSuperRoot(transitionState.SuperRoot)
51+
if err != nil {
52+
return fmt.Errorf("invalid super root: %w", err)
53+
}
54+
if super.Version() != eth.SuperRootVersionV1 {
55+
return fmt.Errorf("%w: %v", ErrIncorrectOutputRootType, super.Version())
56+
}
57+
superRoot := super.(*eth.SuperV1)
58+
claimedBlockNumber, err := bootInfo.RollupConfig.TargetBlockNumber(superRoot.Timestamp + 1)
59+
if err != nil {
60+
return err
61+
}
62+
derivationResult, err := tasks.RunDerivation(
63+
logger,
64+
bootInfo.RollupConfig,
65+
bootInfo.L2ChainConfig,
66+
bootInfo.L1Head,
67+
superRoot.Chains[0].Output,
68+
claimedBlockNumber,
69+
l1PreimageOracle,
70+
l2PreimageOracle,
71+
)
72+
if err != nil {
73+
return err
74+
}
75+
76+
newPendingProgress := make([]types.OptimisticBlock, len(transitionState.PendingProgress)+1)
77+
copy(newPendingProgress, transitionState.PendingProgress)
78+
newPendingProgress[len(newPendingProgress)-1] = types.OptimisticBlock{
79+
BlockHash: derivationResult.BlockHash,
80+
OutputRoot: derivationResult.OutputRoot,
81+
}
82+
finalState := &types.TransitionState{
83+
SuperRoot: transitionState.SuperRoot,
84+
PendingProgress: newPendingProgress,
85+
Step: transitionState.Step + 1,
86+
}
87+
expected, err := finalState.Hash()
88+
if err != nil {
89+
return err
90+
}
91+
if !validate {
92+
return nil
93+
}
94+
return claim.ValidateClaim(logger, derivationResult.SafeHead, eth.Bytes32(bootInfo.L2Claim), eth.Bytes32(expected))
95+
}
96+
97+
type interopTaskExecutor struct {
98+
}
99+
100+
func (t *interopTaskExecutor) RunDerivation(
101+
logger log.Logger,
102+
rollupCfg *rollup.Config,
103+
l2ChainConfig *params.ChainConfig,
104+
l1Head common.Hash,
105+
agreedOutputRoot eth.Bytes32,
106+
claimedBlockNumber uint64,
107+
l1Oracle l1.Oracle,
108+
l2Oracle l2.Oracle) (tasks.DerivationResult, error) {
109+
return tasks.RunDerivation(
110+
logger,
111+
rollupCfg,
112+
l2ChainConfig,
113+
l1Head,
114+
common.Hash(agreedOutputRoot),
115+
claimedBlockNumber,
116+
l1Oracle,
117+
l2Oracle)
118+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package interop
2+
3+
import (
4+
"testing"
5+
6+
"github.com/ethereum-optimism/optimism/op-node/chaincfg"
7+
"github.com/ethereum-optimism/optimism/op-node/rollup"
8+
"github.com/ethereum-optimism/optimism/op-program/client/boot"
9+
"github.com/ethereum-optimism/optimism/op-program/client/interop/types"
10+
"github.com/ethereum-optimism/optimism/op-program/client/l1"
11+
"github.com/ethereum-optimism/optimism/op-program/client/l2"
12+
"github.com/ethereum-optimism/optimism/op-program/client/l2/test"
13+
"github.com/ethereum-optimism/optimism/op-program/client/tasks"
14+
"github.com/ethereum-optimism/optimism/op-service/eth"
15+
"github.com/ethereum-optimism/optimism/op-service/testlog"
16+
"github.com/ethereum/go-ethereum/common"
17+
"github.com/ethereum/go-ethereum/log"
18+
"github.com/ethereum/go-ethereum/params"
19+
"github.com/stretchr/testify/require"
20+
)
21+
22+
func TestDeriveBlockForFirstChainFromSuperchainRoot(t *testing.T) {
23+
logger := testlog.Logger(t, log.LevelError)
24+
rollupCfg := chaincfg.OPSepolia()
25+
chain1Output := &eth.OutputV0{}
26+
agreedSuperRoot := &eth.SuperV1{
27+
Timestamp: rollupCfg.Genesis.L2Time + 1234,
28+
Chains: []eth.ChainIDAndOutput{{ChainID: rollupCfg.L2ChainID.Uint64(), Output: eth.OutputRoot(chain1Output)}},
29+
}
30+
outputRootHash := common.Hash(eth.SuperRoot(agreedSuperRoot))
31+
l2PreimageOracle, _ := test.NewStubOracle(t)
32+
l2PreimageOracle.TransitionStates[outputRootHash] = &types.TransitionState{SuperRoot: agreedSuperRoot.Marshal()}
33+
tasks := stubTasks{
34+
l2SafeHead: eth.L2BlockRef{
35+
Number: 56,
36+
Hash: common.Hash{0x11},
37+
},
38+
blockHash: common.Hash{0x22},
39+
outputRoot: eth.Bytes32{0x66},
40+
}
41+
expectedIntermediateRoot := &types.TransitionState{
42+
SuperRoot: agreedSuperRoot.Marshal(),
43+
PendingProgress: []types.OptimisticBlock{
44+
{BlockHash: tasks.blockHash, OutputRoot: tasks.outputRoot},
45+
},
46+
Step: 1,
47+
}
48+
49+
expectedClaim, err := expectedIntermediateRoot.Hash()
50+
require.NoError(t, err)
51+
bootInfo := &boot.BootInfo{
52+
L2OutputRoot: outputRootHash,
53+
RollupConfig: rollupCfg,
54+
L2ClaimBlockNumber: agreedSuperRoot.Timestamp + 1,
55+
L2Claim: expectedClaim,
56+
}
57+
err = runInteropProgram(logger, bootInfo, nil, l2PreimageOracle, true, &tasks)
58+
require.NoError(t, err)
59+
}
60+
61+
type stubTasks struct {
62+
l2SafeHead eth.L2BlockRef
63+
blockHash common.Hash
64+
outputRoot eth.Bytes32
65+
err error
66+
}
67+
68+
func (t *stubTasks) RunDerivation(
69+
_ log.Logger,
70+
_ *rollup.Config,
71+
_ *params.ChainConfig,
72+
_ common.Hash,
73+
_ eth.Bytes32,
74+
_ uint64,
75+
_ l1.Oracle,
76+
_ l2.Oracle) (tasks.DerivationResult, error) {
77+
return tasks.DerivationResult{
78+
SafeHead: t.l2SafeHead,
79+
BlockHash: t.blockHash,
80+
OutputRoot: t.outputRoot,
81+
}, t.err
82+
}

op-program/client/l2/cache.go

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package l2
22

33
import (
4+
interopTypes "github.com/ethereum-optimism/optimism/op-program/client/interop/types"
45
"github.com/ethereum-optimism/optimism/op-service/eth"
56
"github.com/ethereum/go-ethereum/common"
67
"github.com/ethereum/go-ethereum/core/types"
@@ -14,11 +15,12 @@ const nodeCacheSize = 100_000
1415
const codeCacheSize = 10_000
1516

1617
type CachingOracle struct {
17-
oracle Oracle
18-
blocks *simplelru.LRU[common.Hash, *types.Block]
19-
nodes *simplelru.LRU[common.Hash, []byte]
20-
codes *simplelru.LRU[common.Hash, []byte]
21-
outputs *simplelru.LRU[common.Hash, eth.Output]
18+
oracle Oracle
19+
blocks *simplelru.LRU[common.Hash, *types.Block]
20+
nodes *simplelru.LRU[common.Hash, []byte]
21+
codes *simplelru.LRU[common.Hash, []byte]
22+
outputs *simplelru.LRU[common.Hash, eth.Output]
23+
transitionStates *simplelru.LRU[common.Hash, *interopTypes.TransitionState]
2224
}
2325

2426
func NewCachingOracle(oracle Oracle) *CachingOracle {
@@ -81,3 +83,13 @@ func (o *CachingOracle) BlockDataByHash(agreedBlockHash, blockHash common.Hash,
8183
o.blocks.Add(blockHash, block)
8284
return block
8385
}
86+
87+
func (o *CachingOracle) TransitionStateByRoot(root common.Hash) *interopTypes.TransitionState {
88+
state, ok := o.transitionStates.Get(root)
89+
if ok {
90+
return state
91+
}
92+
state = o.oracle.TransitionStateByRoot(root)
93+
o.transitionStates.Add(root, state)
94+
return state
95+
}

op-program/client/l2/engine.go

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,20 +36,25 @@ func NewOracleEngine(rollupCfg *rollup.Config, logger log.Logger, backend engine
3636
}
3737
}
3838

39-
func (o *OracleEngine) L2OutputRoot(l2ClaimBlockNum uint64) (eth.Bytes32, error) {
39+
// L2OutputRoot returns the block hash and output root at the specified block number
40+
func (o *OracleEngine) L2OutputRoot(l2ClaimBlockNum uint64) (common.Hash, eth.Bytes32, error) {
4041
outBlock := o.backend.GetHeaderByNumber(l2ClaimBlockNum)
4142
if outBlock == nil {
42-
return eth.Bytes32{}, fmt.Errorf("failed to get L2 block at %d", l2ClaimBlockNum)
43+
return common.Hash{}, eth.Bytes32{}, fmt.Errorf("failed to get L2 block at %d", l2ClaimBlockNum)
4344
}
4445
stateDB, err := o.backend.StateAt(outBlock.Root)
4546
if err != nil {
46-
return eth.Bytes32{}, fmt.Errorf("failed to open L2 state db at block %s: %w", outBlock.Hash(), err)
47+
return common.Hash{}, eth.Bytes32{}, fmt.Errorf("failed to open L2 state db at block %s: %w", outBlock.Hash(), err)
4748
}
4849
withdrawalsTrie, err := stateDB.OpenStorageTrie(predeploys.L2ToL1MessagePasserAddr)
4950
if err != nil {
50-
return eth.Bytes32{}, fmt.Errorf("withdrawals trie unavailable at block %v: %w", outBlock.Hash(), err)
51+
return common.Hash{}, eth.Bytes32{}, fmt.Errorf("withdrawals trie unavailable at block %v: %w", outBlock.Hash(), err)
5152
}
52-
return rollup.ComputeL2OutputRootV0(eth.HeaderBlockInfo(outBlock), withdrawalsTrie.Hash())
53+
output, err := rollup.ComputeL2OutputRootV0(eth.HeaderBlockInfo(outBlock), withdrawalsTrie.Hash())
54+
if err != nil {
55+
return common.Hash{}, eth.Bytes32{}, err
56+
}
57+
return outBlock.Hash(), output, nil
5358
}
5459

5560
func (o *OracleEngine) GetPayload(ctx context.Context, payloadInfo eth.PayloadInfo) (*eth.ExecutionPayloadEnvelope, error) {

op-program/client/l2/hints.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const (
1616
HintL2StateNode = "l2-state-node"
1717
HintL2Output = "l2-output"
1818
HintL2BlockData = "l2-block-data"
19+
HintAgreedPrestate = "agreed-pre-state"
1920
)
2021

2122
type BlockHeaderHint common.Hash
@@ -73,3 +74,11 @@ func (l L2BlockDataHint) Hint() string {
7374
binary.BigEndian.PutUint64(hintBytes[64:], l.ChainID)
7475
return fmt.Sprintf("%s 0x%s", HintL2BlockData, common.Bytes2Hex(hintBytes))
7576
}
77+
78+
type AgreedPrestateHint common.Hash
79+
80+
var _ preimage.Hint = AgreedPrestateHint{}
81+
82+
func (l AgreedPrestateHint) Hint() string {
83+
return HintAgreedPrestate + " " + (common.Hash)(l).String()
84+
}

op-program/client/l2/oracle.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package l2
33
import (
44
"fmt"
55

6+
interopTypes "github.com/ethereum-optimism/optimism/op-program/client/interop/types"
67
"github.com/ethereum/go-ethereum/common"
78
"github.com/ethereum/go-ethereum/core/types"
89
"github.com/ethereum/go-ethereum/rlp"
@@ -37,6 +38,8 @@ type Oracle interface {
3738

3839
// BlockDataByHash retrieves the block, including all data used to construct it.
3940
BlockDataByHash(agreedBlockHash, blockHash common.Hash, chainID uint64) *types.Block
41+
42+
TransitionStateByRoot(root common.Hash) *interopTypes.TransitionState
4043
}
4144

4245
// PreimageOracle implements Oracle using by interfacing with the pure preimage.Oracle
@@ -117,3 +120,13 @@ func (p *PreimageOracle) BlockDataByHash(agreedBlockHash, blockHash common.Hash,
117120
txs := p.LoadTransactions(blockHash, header.TxHash)
118121
return types.NewBlockWithHeader(header).WithBody(types.Body{Transactions: txs})
119122
}
123+
124+
func (p *PreimageOracle) TransitionStateByRoot(root common.Hash) *interopTypes.TransitionState {
125+
p.hint.Hint(AgreedPrestateHint(root))
126+
data := p.oracle.Get(preimage.Keccak256Key(root))
127+
output, err := interopTypes.UnmarshalProofsState(data)
128+
if err != nil {
129+
panic(fmt.Errorf("invalid agreed prestate data for root %s: %w", root, err))
130+
}
131+
return output
132+
}

0 commit comments

Comments
 (0)