Commit f5fc73db authored by Adrian Sutton's avatar Adrian Sutton Committed by GitHub

op-program: Transition to invalid state when L1Head reached before claimed block (#13743)

parent 9b8b5281
......@@ -7,6 +7,7 @@ import (
fpHelpers "github.com/ethereum-optimism/optimism/op-e2e/actions/proofs/helpers"
"github.com/ethereum-optimism/optimism/op-program/client/claim"
"github.com/ethereum-optimism/optimism/op-program/client/interop"
"github.com/ethereum-optimism/optimism/op-program/client/interop/types"
"github.com/ethereum-optimism/optimism/op-service/testlog"
"github.com/ethereum/go-ethereum/common"
......@@ -351,6 +352,43 @@ func TestInteropFaultProofs(gt *testing.T) {
expectValid: true,
skip: true,
},
{
name: "FirstChainReachesL1Head",
startTimestamp: startTimestamp,
agreedClaim: start.Marshal(),
disputedClaim: interop.InvalidTransition,
// The derivation reaches the L1 head before the next block can be created
l1Head: actors.L1Miner.L1Chain().Genesis().Hash(),
expectValid: true,
},
{
name: "SecondChainReachesL1Head",
startTimestamp: startTimestamp,
agreedClaim: step1Expected,
disputedClaim: interop.InvalidTransition,
// The derivation reaches the L1 head before the next block can be created
l1Head: actors.L1Miner.L1Chain().Genesis().Hash(),
expectValid: true,
},
{
name: "SuperRootInvalidIfUnsupportedByL1Data",
startTimestamp: startTimestamp,
agreedClaim: step1Expected,
disputedClaim: step2Expected,
// The derivation reaches the L1 head before the next block can be created
l1Head: actors.L1Miner.L1Chain().Genesis().Hash(),
expectValid: false,
},
{
name: "FromInvalidTransitionHash",
startTimestamp: startTimestamp,
agreedClaim: interop.InvalidTransition,
disputedClaim: interop.InvalidTransition,
// The derivation reaches the L1 head before the next block can be created
l1Head: actors.L1Miner.L1Chain().Genesis().Hash(),
expectValid: true,
},
}
for _, test := range tests {
......@@ -366,12 +404,17 @@ func TestInteropFaultProofs(gt *testing.T) {
if !test.expectValid {
checkResult = fpHelpers.ExpectError(claim.ErrClaimNotValid)
}
l1Head := test.l1Head
if l1Head == (common.Hash{}) {
l1Head = actors.L1Miner.L1Chain().CurrentBlock().Hash()
}
fpHelpers.RunFaultProofProgram(
t,
logger,
actors.L1Miner,
checkResult,
WithInteropEnabled(actors, test.agreedClaim, crypto.Keccak256Hash(test.disputedClaim), endTimestamp),
fpHelpers.WithL1Head(l1Head),
)
})
}
......@@ -404,6 +447,7 @@ type transitionTest struct {
startTimestamp uint64
agreedClaim []byte
disputedClaim []byte
l1Head common.Hash // Defaults to current L1 head if not set
expectValid bool
skip bool
}
......@@ -157,6 +157,12 @@ func WithL2BlockNumber(num uint64) FixtureInputParam {
}
}
func WithL1Head(head common.Hash) FixtureInputParam {
return func(f *FixtureInputs) {
f.L1Head = head
}
}
func (env *L2FaultProofEnv) RunFaultProofProgram(t helpers.Testing, l2ClaimBlockNum uint64, checkResult CheckResult, fixtureInputParams ...FixtureInputParam) {
defaultParam := WithPreInteropDefaults(t, l2ClaimBlockNum, env.Sequencer.L2Verifier, env.Engine)
combinedParams := []FixtureInputParam{defaultParam}
......
......@@ -13,12 +13,17 @@ import (
"github.com/ethereum-optimism/optimism/op-program/client/tasks"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
)
var (
ErrIncorrectOutputRootType = errors.New("incorrect output root type")
ErrL1HeadReached = errors.New("l1 head reached")
InvalidTransition = []byte("invalid")
InvalidTransitionHash = crypto.Keccak256Hash(InvalidTransition)
)
type taskExecutor interface {
......@@ -40,36 +45,78 @@ func RunInteropProgram(logger log.Logger, bootInfo *boot.BootInfoInterop, l1Prei
func runInteropProgram(logger log.Logger, bootInfo *boot.BootInfoInterop, l1PreimageOracle l1.Oracle, l2PreimageOracle l2.Oracle, validateClaim bool, tasks taskExecutor) error {
logger.Info("Interop Program Bootstrapped", "bootInfo", bootInfo)
expected, err := stateTransition(logger, bootInfo, l1PreimageOracle, l2PreimageOracle, tasks)
if err != nil {
return err
}
if !validateClaim {
return nil
}
return claim.ValidateClaim(logger, eth.Bytes32(bootInfo.Claim), eth.Bytes32(expected))
}
func stateTransition(logger log.Logger, bootInfo *boot.BootInfoInterop, l1PreimageOracle l1.Oracle, l2PreimageOracle l2.Oracle, tasks taskExecutor) (common.Hash, error) {
if bootInfo.AgreedPrestate == InvalidTransitionHash {
return InvalidTransitionHash, nil
}
transitionState, superRoot, err := parseAgreedState(bootInfo, l2PreimageOracle)
if err != nil {
return common.Hash{}, err
}
expectedPendingProgress := transitionState.PendingProgress
if transitionState.Step < uint64(len(superRoot.Chains)) {
block, err := deriveOptimisticBlock(logger, bootInfo, l1PreimageOracle, l2PreimageOracle, superRoot, transitionState, tasks)
if errors.Is(err, ErrL1HeadReached) {
return InvalidTransitionHash, nil
} else if err != nil {
return common.Hash{}, err
}
expectedPendingProgress = append(expectedPendingProgress, block)
}
finalState := &types.TransitionState{
SuperRoot: transitionState.SuperRoot,
PendingProgress: expectedPendingProgress,
Step: transitionState.Step + 1,
}
expected, err := finalState.Hash()
if err != nil {
return common.Hash{}, err
}
return expected, nil
}
func parseAgreedState(bootInfo *boot.BootInfoInterop, l2PreimageOracle l2.Oracle) (*types.TransitionState, *eth.SuperV1, error) {
// For the first step in a timestamp, we would get a SuperRoot as the agreed claim - TransitionStateByRoot will
// automatically convert it to a TransitionState with Step: 0.
transitionState := l2PreimageOracle.TransitionStateByRoot(bootInfo.AgreedPrestate)
if transitionState.Version() != types.IntermediateTransitionVersion {
return fmt.Errorf("%w: %v", ErrIncorrectOutputRootType, transitionState.Version())
return nil, nil, fmt.Errorf("%w: %v", ErrIncorrectOutputRootType, transitionState.Version())
}
super, err := eth.UnmarshalSuperRoot(transitionState.SuperRoot)
if err != nil {
return fmt.Errorf("invalid super root: %w", err)
return nil, nil, fmt.Errorf("invalid super root: %w", err)
}
if super.Version() != eth.SuperRootVersionV1 {
return fmt.Errorf("%w: %v", ErrIncorrectOutputRootType, super.Version())
return nil, nil, fmt.Errorf("%w: %v", ErrIncorrectOutputRootType, super.Version())
}
superRoot := super.(*eth.SuperV1)
return transitionState, superRoot, nil
}
expectedPendingProgress := transitionState.PendingProgress
if transitionState.Step < uint64(len(superRoot.Chains)) {
func deriveOptimisticBlock(logger log.Logger, bootInfo *boot.BootInfoInterop, l1PreimageOracle l1.Oracle, l2PreimageOracle l2.Oracle, superRoot *eth.SuperV1, transitionState *types.TransitionState, tasks taskExecutor) (types.OptimisticBlock, error) {
chainAgreedPrestate := superRoot.Chains[transitionState.Step]
rollupCfg, err := bootInfo.Configs.RollupConfig(chainAgreedPrestate.ChainID)
if err != nil {
return fmt.Errorf("no rollup config available for chain ID %v: %w", chainAgreedPrestate.ChainID, err)
return types.OptimisticBlock{}, fmt.Errorf("no rollup config available for chain ID %v: %w", chainAgreedPrestate.ChainID, err)
}
l2ChainConfig, err := bootInfo.Configs.ChainConfig(chainAgreedPrestate.ChainID)
if err != nil {
return fmt.Errorf("no chain config available for chain ID %v: %w", chainAgreedPrestate.ChainID, err)
return types.OptimisticBlock{}, fmt.Errorf("no chain config available for chain ID %v: %w", chainAgreedPrestate.ChainID, err)
}
claimedBlockNumber, err := rollupCfg.TargetBlockNumber(superRoot.Timestamp + 1)
if err != nil {
return err
return types.OptimisticBlock{}, err
}
derivationResult, err := tasks.RunDerivation(
logger,
......@@ -82,27 +129,17 @@ func runInteropProgram(logger log.Logger, bootInfo *boot.BootInfoInterop, l1Prei
l2PreimageOracle,
)
if err != nil {
return err
return types.OptimisticBlock{}, err
}
if derivationResult.Head.Number < claimedBlockNumber {
return types.OptimisticBlock{}, ErrL1HeadReached
}
expectedPendingProgress = append(expectedPendingProgress, types.OptimisticBlock{
block := types.OptimisticBlock{
BlockHash: derivationResult.BlockHash,
OutputRoot: derivationResult.OutputRoot,
})
}
finalState := &types.TransitionState{
SuperRoot: transitionState.SuperRoot,
PendingProgress: expectedPendingProgress,
Step: transitionState.Step + 1,
}
expected, err := finalState.Hash()
if err != nil {
return err
}
if !validateClaim {
return nil
}
return claim.ValidateClaim(logger, eth.Bytes32(bootInfo.Claim), eth.Bytes32(expected))
return block, nil
}
type interopTaskExecutor struct {
......
......@@ -43,6 +43,7 @@ func setupTwoChains() (*staticConfigSource, *eth.SuperV1, stubTasks) {
chainConfigs: []*params.ChainConfig{chainCfg1, &chainCfg2},
}
tasksStub := stubTasks{
l2SafeHead: eth.L2BlockRef{Number: 918429823450218}, // Past the claimed block
blockHash: common.Hash{0x22},
outputRoot: eth.Bytes32{0x66},
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment