Commit 1752ffc5 authored by mergify[bot]'s avatar mergify[bot] Committed by GitHub

Merge branch 'develop' into runtime-config-reloading

parents ce3d79a9 db94109b
...@@ -9,7 +9,6 @@ import ( ...@@ -9,7 +9,6 @@ import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
...@@ -331,12 +330,18 @@ func Run(ctx *cli.Context) error { ...@@ -331,12 +330,18 @@ func Run(ctx *cli.Context) error {
} }
if proofAt(state) { if proofAt(state) {
preStateHash := crypto.Keccak256Hash(state.EncodeWitness()) preStateHash, err := state.EncodeWitness().StateHash()
if err != nil {
return fmt.Errorf("failed to hash prestate witness: %w", err)
}
witness, err := stepFn(true) witness, err := stepFn(true)
if err != nil { if err != nil {
return fmt.Errorf("failed at proof-gen step %d (PC: %08x): %w", step, state.PC, err) return fmt.Errorf("failed at proof-gen step %d (PC: %08x): %w", step, state.PC, err)
} }
postStateHash := crypto.Keccak256Hash(state.EncodeWitness()) postStateHash, err := state.EncodeWitness().StateHash()
if err != nil {
return fmt.Errorf("failed to hash poststate witness: %w", err)
}
proof := &Proof{ proof := &Proof{
Step: step, Step: step,
Pre: preStateHash, Pre: preStateHash,
......
...@@ -5,7 +5,6 @@ import ( ...@@ -5,7 +5,6 @@ import (
"os" "os"
"github.com/ethereum-optimism/optimism/cannon/mipsevm" "github.com/ethereum-optimism/optimism/cannon/mipsevm"
"github.com/ethereum/go-ethereum/crypto"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
...@@ -31,7 +30,10 @@ func Witness(ctx *cli.Context) error { ...@@ -31,7 +30,10 @@ func Witness(ctx *cli.Context) error {
return fmt.Errorf("invalid input state (%v): %w", input, err) return fmt.Errorf("invalid input state (%v): %w", input, err)
} }
witness := state.EncodeWitness() witness := state.EncodeWitness()
h := crypto.Keccak256Hash(witness) h, err := witness.StateHash()
if err != nil {
return fmt.Errorf("failed to compute witness hash: %w", err)
}
if output != "" { if output != "" {
if err := os.WriteFile(output, witness, 0755); err != nil { if err := os.WriteFile(output, witness, 0755); err != nil {
return fmt.Errorf("writing output to %v: %w", output, err) return fmt.Errorf("writing output to %v: %w", output, err)
......
...@@ -15,7 +15,6 @@ import ( ...@@ -15,7 +15,6 @@ import (
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/eth/tracers/logger" "github.com/ethereum/go-ethereum/eth/tracers/logger"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
...@@ -92,7 +91,10 @@ func (m *MIPSEVM) Step(t *testing.T, stepWitness *StepWitness) []byte { ...@@ -92,7 +91,10 @@ func (m *MIPSEVM) Step(t *testing.T, stepWitness *StepWitness) []byte {
logs := m.evmState.Logs() logs := m.evmState.Logs()
require.Equal(t, 1, len(logs), "expecting a log with post-state") require.Equal(t, 1, len(logs), "expecting a log with post-state")
evmPost := logs[0].Data evmPost := logs[0].Data
require.Equal(t, crypto.Keccak256Hash(evmPost), postHash, "logged state must be accurate")
stateHash, err := StateWitness(evmPost).StateHash()
require.NoError(t, err, "state hash could not be computed")
require.Equal(t, stateHash, postHash, "logged state must be accurate")
m.env.StateDB.RevertToSnapshot(snap) m.env.StateDB.RevertToSnapshot(snap)
t.Logf("EVM step took %d gas, and returned stateHash %s", startingGas-leftOverGas, postHash) t.Logf("EVM step took %d gas, and returned stateHash %s", startingGas-leftOverGas, postHash)
......
...@@ -2,11 +2,16 @@ package mipsevm ...@@ -2,11 +2,16 @@ package mipsevm
import ( import (
"encoding/binary" "encoding/binary"
"fmt"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
) )
// StateWitnessSize is the size of the state witness encoding in bytes.
var StateWitnessSize = 226
type State struct { type State struct {
Memory *Memory `json:"memory"` Memory *Memory `json:"memory"`
...@@ -37,7 +42,11 @@ type State struct { ...@@ -37,7 +42,11 @@ type State struct {
LastHint hexutil.Bytes `json:"lastHint,omitempty"` LastHint hexutil.Bytes `json:"lastHint,omitempty"`
} }
func (s *State) EncodeWitness() []byte { func (s *State) VMStatus() uint8 {
return vmStatus(s.Exited, s.ExitCode)
}
func (s *State) EncodeWitness() StateWitness {
out := make([]byte, 0) out := make([]byte, 0)
memRoot := s.Memory.MerkleRoot() memRoot := s.Memory.MerkleRoot()
out = append(out, memRoot[:]...) out = append(out, memRoot[:]...)
...@@ -60,3 +69,41 @@ func (s *State) EncodeWitness() []byte { ...@@ -60,3 +69,41 @@ func (s *State) EncodeWitness() []byte {
} }
return out return out
} }
type StateWitness []byte
const (
VMStatusValid = 0
VMStatusInvalid = 1
VMStatusPanic = 2
VMStatusUnfinished = 3
)
func (sw StateWitness) StateHash() (common.Hash, error) {
if len(sw) != 226 {
return common.Hash{}, fmt.Errorf("Invalid witness length. Got %d, expected at least 88", len(sw))
}
hash := crypto.Keccak256Hash(sw)
offset := 32*2 + 4*6
exitCode := sw[offset]
exited := sw[offset+1]
status := vmStatus(exited == 1, exitCode)
hash[0] = status
return hash, nil
}
func vmStatus(exited bool, exitCode uint8) uint8 {
if !exited {
return VMStatusUnfinished
}
switch exitCode {
case 0:
return VMStatusValid
case 1:
return VMStatusInvalid
default:
return VMStatusPanic
}
}
...@@ -82,6 +82,53 @@ func TestState(t *testing.T) { ...@@ -82,6 +82,53 @@ func TestState(t *testing.T) {
} }
} }
// Run through all permutations of `exited` / `exitCode` and ensure that the
// correct witness, state hash, and VM Status is produced.
func TestStateHash(t *testing.T) {
cases := []struct {
exited bool
exitCode uint8
}{
{exited: false, exitCode: 0},
{exited: false, exitCode: 1},
{exited: false, exitCode: 2},
{exited: false, exitCode: 3},
{exited: true, exitCode: 0},
{exited: true, exitCode: 1},
{exited: true, exitCode: 2},
{exited: true, exitCode: 3},
}
exitedOffset := 32*2 + 4*6
for _, c := range cases {
state := &State{
Memory: NewMemory(),
Exited: c.exited,
ExitCode: c.exitCode,
}
actualWitness := state.EncodeWitness()
actualStateHash, err := StateWitness(actualWitness).StateHash()
require.NoError(t, err, "Error hashing witness")
require.Equal(t, len(actualWitness), StateWitnessSize, "Incorrect witness size")
expectedWitness := make(StateWitness, 226)
memRoot := state.Memory.MerkleRoot()
copy(expectedWitness[:32], memRoot[:])
expectedWitness[exitedOffset] = c.exitCode
var exited uint8
if c.exited {
exited = 1
}
expectedWitness[exitedOffset+1] = uint8(exited)
require.Equal(t, expectedWitness[:], actualWitness[:], "Incorrect witness")
expectedStateHash := crypto.Keccak256Hash(actualWitness)
expectedStateHash[0] = vmStatus(c.exited, c.exitCode)
require.Equal(t, expectedStateHash, actualStateHash, "Incorrect state hash")
}
}
func TestHello(t *testing.T) { func TestHello(t *testing.T) {
elfProgram, err := elf.Open("../example/bin/hello.elf") elfProgram, err := elf.Open("../example/bin/hello.elf")
require.NoError(t, err, "open ELF file") require.NoError(t, err, "open ELF file")
......
...@@ -6,3 +6,4 @@ The directory layout is divided into the following sub-directories. ...@@ -6,3 +6,4 @@ The directory layout is divided into the following sub-directories.
- [`postmortems/`](./postmortems/): Timestamped post-mortem documents. - [`postmortems/`](./postmortems/): Timestamped post-mortem documents.
- [`security-reviews`](./security-reviews/): Audit summaries and other security review documents. - [`security-reviews`](./security-reviews/): Audit summaries and other security review documents.
- [`fault-proof-alpha`](./fault-proof-alpha): Information on the alpha version of the fault proof system.
## Fault Proofs Alpha
The fault proof alpha is a pre-release version of the OP Stack fault proof system.
This documentation provides an overview of the system and instructions on how to help
test the fault proof system.
The overall design of this system along with the APIs and interfaces it exposes are not
finalized and may change without notice.
### Contents
* Overview
* [Deployment Details](./deployments.md)
* [Manual Usage](./manual.md)
* [Creating Traces with Cannon](./cannon.md)
* [Automation with `op-challenger`](./run-challenger.md)
## Generate Traces with `cannon` and `op-program`
Normally, `op-challenger` handles creating the required traces as part of responding to games. However, for manual
testing it may be useful to manually generate the trace. This can be done by running `cannon` directly.
### Prerequisites
- The cannon pre-state downloaded from [Goerli deployment](./deployments.md#goerli).
- A Goerli L1 node.
- An archive node is not required.
- Public RPC providers can be used, however a significant number of requests will need to be made which may exceed
rate limits for free plans.
- An OP-Goerli L2 archive node with `debug` APIs enabled.
- An archive node is required to ensure world-state pre-images remain available.
- Public RPC providers are generally not usable as they don’t support the `debug_dbGet` RPC method.
### Compilation
To compile the required programs, in the top level of the monorepo run:
```bash
make cannon-prestate
```
This will compile the `cannon` executable to `cannon/bin/cannon` as well as the `op-program` executable used to fetch
pre-image data to `op-program/bin/op-program`.
### Run Cannon
To run cannon to generate a proof use:
```bash
mkdir -p temp/cannon/proofs temp/cannon/snapshots temp/cannon/preimages
./cannon/bin/cannon run \
--pprof.cpu \
--info-at '%10000000' \
--proof-at '=<TRACE_INDEX>' \
--stop-at '=<STOP_INDEX>' \
--proof-fmt 'temp/cannon/proofs/%d.json' \
--snapshot-at '%1000000000' \
--snapshot-fmt 'temp/cannon/snapshots/%d.json.gz' \
--input <PRESTATE> \
--output temp/cannon/stop-state.json \
-- \
./op-program/bin/op-program \
--network goerli \
--l1 <L1_URL> \
--l2 <L2_URL> \
--l1.head <L1_HEAD> \
--l2.claim <L2_CLAIM> \
--l2.head <L2_HEAD> \
--l2.blocknumber <L2_BLOCK_NUMBER> \
--datadir temp/cannon/preimages \
--log.format terminal \
--server
```
The placeholders are:
- `<TRACE_INDEX>` the index in the trace to generate a proof for
- `<STOP_INDEX>` the index to stop execution at. Typically this is one instruction after `<TRACE_INDEX>` to stop as soon
as the required proof has been generated.
- `<PRESTATE>` the prestate.json downloaded above. Note that this needs to precisely match the prestate used on-chain so
must be the downloaded version and not a version built locally.
- `<L1_URL>` the Goerli L1 JSON RPC endpoint
- `<L2_URL>` the OP-Goerli L2 archive node JSON RPC endpoint
- `<L1_HEAD>` the hash of the L1 head block used for the dispute game
- `<L2_CLAIM>` the output root immediately prior to the disputed root in the L2 output oracle
- `<L2_HEAD>` the hash of the L2 block that `<L2_CLAIM>`is from
- `<L2_BLOCK_NUMBER>` the block number that `<L2_CLAIM>` is from
The generated proof will be stored in the `temp/cannon/proofs/` directory. The hash to use as the claim value is
the `post` field of the generated proof which provides the hash of the cannon state witness after execution of the step.
Since cannon can be very slow to execute, the above command uses the `--snapshot-at` option to generate a snapshot of
the cannon state every 1000000000 instructions. Once generated, these snapshots can be used as the `--input` to begin
execution at that step rather than from the very beginning. Generated snapshots are stored in
the `temp/cannon/snapshots` directory.
See `./cannon/bin/cannon --help` for further information on the options available.
### Trace Extension
Fault dispute games always use a trace with a fixed length of `2 ^ MAX_GAME_DEPTH`. The trace generated by `cannon`
stops when the client program exits, so this trace must be extended by repeating the hash of the final state in the
actual trace for all remaining steps. Cannon does not perform this trace extension automatically.
If cannon stops execution before the trace index you requested a proof at, it simply will not generate a proof. When it
stops executing, it will write its final state to `temp/cannon/stop-state.json` (controlled by the `--output` option).
The `step` field of this state contains the last step cannon executed. Once the final step is known, rerun cannon to
generate the proof at that final step and use the `post` hash as the claim value for all later trace indices.
## Fault Proof Alpha Deployment Information
### Goerli
Information on the fault proofs alpha deployment to Goerli is not yet available.
### Local Devnet
The local devnet includes a deployment of the fault proof alpha. To start the devnet, in the top level of this repo,
run:
```bash
make devnet-up
```
| Input | Value |
|----------------------|-------------------------------------------------------------|
| Dispute Game Factory | Run `jq -r .DisputeGameFactoryProxy .devnet/addresses.json` |
| Absolute Prestate | `op-program/bin/prestate.json` |
| Max Depth | 30 |
| Max Game Duration | 1200 (20 minutes) |
See the [op-challenger README](../../op-challenger#running-with-cannon-on-local-devnet) for information on
running `op-challenger` against the local devnet.
## Manual Fault Proof Interactions
### Creating a Game
The process of disputing an output root starts by creating a new dispute game. There are conceptually three key inputs
required for a dispute game:
- The output root being disputed
- The agreed output root the derivation process will start from
- The L1 head block that defines the canonical L1 chain containing all required batch data to perform the derivation
The creator of the game selects the output root to dispute. It is identified by its L2 block number which can be used to
look up the full details in the L2 output oracle.
The agreed output root is defined as the output root immediately prior to the disputed output root in the L2 output
oracle. Therefore, a dispute game should only be created for the first invalid output root. If it is successfully
disputed, all output roots after it are considered invalid by inference.
The L1 head block can be any L1 block where the disputed output root is present in the L2 output oracle. Proposers
should therefore ensure that all batch data has been submitted to L1 before submitting a proposal. The L1 head block is
recorded in the `BlockOracle` and then referenced by its block number.
Creating a game requires two separate transactions. First the L1 head block is recorded in the `BlockOracle` by calling
its `checkpoint` function. This records the parent of the block the transaction is included in. The `BlockOracle` emits
a log `Checkpoint(blockNumber, blockHash, childTimestamp)`.
Now, using the L1 head along with output root info available in the L2 output oracle, cannon can be executed to
determine the root claim to use when creating the game. In simple cases, where the claim is expected to be incorrect, an
arbitrary hash can be used for claim values. For more advanced cases [cannon can be used](./cannon.md) to generate a
trace, including the claim values to use at specific steps. Note that it is not valid to create a game that disputes an
output root, using the final hash from a trace that confirms the output root is valid. To dispute an output root
successfully, the trace must resolve that the disputed output root is invalid.
The game can then be created by calling the `create` method on the `DisputeGameFactory` contract. This requires three
parameters:
- `gameType` - a `uint8` representing the type of game to create. For fault dispute games using cannon and op-program
traces, the game type is 0.
- `rootClaim` - a `bytes32` hash of the final state from the trace.
- `extraData` - arbitrary bytes which are used as the initial inputs for the game. For fault dispute games using cannon
and op-program traces, this is the abi encoding of `(uint256(l2_block_number), uint256(l1_checkpoint))`.
- `l2_block_number` is the L2 block number from the output root being disputed
- `l1_checkpoint` is the L1 block number recorded by the `BlockOracle` checkpoint
This emits a log event `DisputeGameCreated(gameAddress, gameType, rootClaim)` where `gameAddress` is the address of the
newly created dispute game.
The helper script, [create_game.sh](../../op-challenger#create_gamesh) can be used to easily create a new dispute
game and also acts as an example of using `cast` to manually create a game.
### Performing Moves
The dispute game progresses by actors countering existing claims via either the `attack` or `defend` methods in
the `FaultDisputeGame` contract. Note that only `attack` can be used to counter the root claim. In both cases, there are
two inputs required:
- `parentIndex` - the index in the claims array of the parent claim that is being countered.
- `claim` - a `bytes32` hash of the state at the trace index corresponding to the new claim’s position.
The helper script, [move.sh](../../op-challenger#movesh), can be used to easily perform moves and also
acts as an example of using `cast` to manually call `attack` and `defend`.
### Performing Steps
Attacking or defending are teh only available actions before the maximum depth of the game is reached. To counter claims
at the maximum depth, a step must be performed instead. Calling the `step` method in the `FaultDisputeGame` contract
counters a claim at the maximum depth by running a single step of the cannon VM on chain. The `step` method will revert
unless the cannon execution confirms the claim being countered is invalid. Note, if an actor's clock runs out at any
point, the game can be [resolved](#resolving-a-game).
The inputs for step are:
- `claimIndex` - the index in the claims array of the claim that is being countered
- `isAttack` - Similar to regular moves, steps can either be attacking or defending
- `stateData` - the full cannon state witness to use as the starting state for execution
- `proof` - the additional proof data for the state witness required by cannon to perform the step
When a step is attacking, the caller is asserting that the claim at `claimIndex` is incorrect, and the claim for
the previous trace index (made at a previous level in the game) was correct. The `stateData` must be the pre-image for
the agreed correct hash at the previous trace index. The call to `step` will revert if the post-state from cannon
matches the claim at `claimIndex` since the on-chain execution has proven the claim correct and it should not be
countered.
When a step is defending, the caller is asserting that the claim at `claimIndex` is correct, and the claim for
the next trace index (made at a previous level in the game) is incorrect. The `stateData` must be the pre-image for the
hash in the claim at `claimIndex`.
The `step` function will revert with `ValidStep()` if the cannon execution proves that the claim attempting to be
countered is correct. As a result, claims at the maximum game depth can only be countered by a valid execution of the
single instruction in cannon running on-chain.
#### Populating the Pre-image Oracle
When the instruction to be executed as part of a `step` call reads from some pre-image data, that data must be loaded
into the pre-image oracle prior to calling `step`.
For [local pre-image keys](../../specs/fault-proof.md#type-1-local-key), the pre-image must be populated via
the `FaultDisputeGame` contract by calling the `addLocalData` function.
For [global keccak256 keys](../../specs/fault-proof.md#type-2-global-keccak256-key), the data should be added directly
to the pre-image oracle contract.
### Resolving a Game
The final action required for a game is to resolve it by calling the `resolve` method in the `FaultDisputeGame`
contract. This can only be done once the clock of the left-most uncontested claim’s parent has expired. A game can only
be resolved once.
There are no inputs required for the `resolve` method. When successful, a log event is emitted with the game’s final
status.
The helper script, [resolve.sh](../../op-challenger#resolvesh), can be used to easily resolve a game and also acts as an
example of using `cast` to manually call `resolve` and understand the result.
## Running op-challenger
`op-challenger` is a program that implements the honest actor algorithm to automatically “play” the dispute games.
### Prerequisites
- The cannon pre-state downloaded from [Goerli deployment](./deployments.md#goerli).
- An account on the Goerli testnet with funds available. The amount of GöETH required depends on the number of claims
the challenger needs to post, but 0.01 ETH should be plenty to start.
- A Goerli L1 node.
- An archive node is not required.
- Public RPC providers can be used, however a significant number of requests will need to be made which may exceed
rate limits for free plans.
- An OP-Goerli L2 archive node with `debug` APIs enabled.
- An archive node is required to ensure world-state pre-images remain available.
- Public RPC providers are generally not usable as they don’t support the `debug_dbGet` RPC method.
- Approximately 3.5Gb of disk space for each game being played.
### Starting op-challenger
When executing `op-challenger`, there are a few placeholders that need to be set to concreate values:
- `<L1_URL>` the Goerli L1 JSON RPC endpoint
- `<DISPUTE_GAME_FACTORY_ADDRESS>` the address of the dispute game factory contract (see
the [Goerli deployment details](./deployments.md#goerli))
- `<PRESTATE>` the prestate.json downloaded above. Note that this needs to precisely match the prestate used on-chain so
must be the downloaded version and not a version built locally (see the [Goerli deployment details](./deployments.md#goerli))
- `<L2_URL>` the OP-Goerli L2 archive node JSON RPC endpoint
- `<PRIVATE_KEY>` the private key for a funded Goerli account. For other ways to specify the account to use
see `./op-challenger/bin/op-challenger --help`
From inside the monorepo directory, run the challenger with these placeholders set.
```bash
# Build the required components
make op-challenger op-program cannon
# Run op-challenger
./op-challenger/bin/op-challenger \
--trace-type cannon \
--l1-eth-rpc <L1_URL> \
--game-factory-address <DISPUTE_GAME_FACTORY_ADDRESS> \
--agree-with-proposed-output=true \
--datadir temp/challenger-goerli \
--cannon-network goerli \
--cannon-bin ./cannon/bin/cannon \
--cannon-server ./op-program/bin/op-program \
--cannon-prestate <PRESTATE> \
--cannon-l2 <L2_URL> \
--private-key <PRIVATE_KEY>
```
### Restricting Games to Play
By default `op-challenger` will generate traces and respond to any game created by the dispute game factory contract. On
a public testnet like Goerli, that could be a large number of games, requiring significant CPU and disk resources. To
avoid this, `op-challenger` supports specifying an allowlist of games for it to respond to with the `--game-allowlist`
option.
```bash
./op-challenger/bin/op-challenger \
... \
--game-allowlist <GAME_ADDR> <GAME_ADDR> <GAME_ADDR>...
```
...@@ -4,42 +4,114 @@ import ( ...@@ -4,42 +4,114 @@ import (
"context" "context"
"fmt" "fmt"
"net/http" "net/http"
"runtime/debug"
"sync"
"github.com/ethereum-optimism/optimism/indexer/api/routes" "github.com/ethereum-optimism/optimism/indexer/api/routes"
"github.com/ethereum-optimism/optimism/indexer/config"
"github.com/ethereum-optimism/optimism/indexer/database" "github.com/ethereum-optimism/optimism/indexer/database"
"github.com/ethereum-optimism/optimism/op-service/httputil" "github.com/ethereum-optimism/optimism/op-service/httputil"
"github.com/ethereum-optimism/optimism/op-service/metrics"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware" "github.com/go-chi/chi/v5/middleware"
"github.com/prometheus/client_golang/prometheus"
) )
const ethereumAddressRegex = `^0x[a-fA-F0-9]{40}$` const ethereumAddressRegex = `^0x[a-fA-F0-9]{40}$`
type Api struct { type Api struct {
log log.Logger log log.Logger
Router *chi.Mux Router *chi.Mux
serverConfig config.ServerConfig
metricsConfig config.ServerConfig
metricsRegistry *prometheus.Registry
} }
func NewApi(logger log.Logger, bv database.BridgeTransfersView) *Api { const (
r := chi.NewRouter() MetricsNamespace = "op_indexer"
h := routes.NewRoutes(logger, bv, r) )
func chiMetricsMiddleware(rec metrics.HTTPRecorder) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return metrics.NewHTTPRecordingMiddleware(rec, next)
}
}
func NewApi(logger log.Logger, bv database.BridgeTransfersView, serverConfig config.ServerConfig, metricsConfig config.ServerConfig) *Api {
apiRouter := chi.NewRouter()
h := routes.NewRoutes(logger, bv, apiRouter)
mr := metrics.NewRegistry()
promRecorder := metrics.NewPromHTTPRecorder(mr, MetricsNamespace)
apiRouter.Use(chiMetricsMiddleware(promRecorder))
apiRouter.Use(middleware.Recoverer)
apiRouter.Use(middleware.Heartbeat("/healthz"))
apiRouter.Get(fmt.Sprintf("/api/v0/deposits/{address:%s}", ethereumAddressRegex), h.L1DepositsHandler)
apiRouter.Get(fmt.Sprintf("/api/v0/withdrawals/{address:%s}", ethereumAddressRegex), h.L2WithdrawalsHandler)
return &Api{log: logger, Router: apiRouter, metricsRegistry: mr, serverConfig: serverConfig, metricsConfig: metricsConfig}
}
func (a *Api) Start(ctx context.Context) error {
var wg sync.WaitGroup
errCh := make(chan error, 2)
processCtx, processCancel := context.WithCancel(ctx)
runProcess := func(start func(ctx context.Context) error) {
wg.Add(1)
go func() {
defer func() {
if err := recover(); err != nil {
a.log.Error("halting api on panic", "err", err)
debug.PrintStack()
errCh <- fmt.Errorf("panic: %v", err)
}
processCancel()
wg.Done()
}()
r.Use(middleware.Heartbeat("/healthz")) errCh <- start(processCtx)
}()
}
runProcess(a.startServer)
runProcess(a.startMetricsServer)
wg.Wait()
err := <-errCh
if err != nil {
a.log.Error("api stopped", "err", err)
} else {
a.log.Info("api stopped")
}
r.Get(fmt.Sprintf("/api/v0/deposits/{address:%s}", ethereumAddressRegex), h.L1DepositsHandler) return err
r.Get(fmt.Sprintf("/api/v0/withdrawals/{address:%s}", ethereumAddressRegex), h.L2WithdrawalsHandler)
return &Api{log: logger, Router: r}
} }
func (a *Api) Listen(ctx context.Context, port int) error { func (a *Api) startServer(ctx context.Context) error {
a.log.Info("api server listening...", "port", port) a.log.Info("api server listening...", "port", a.serverConfig.Port)
server := http.Server{Addr: fmt.Sprintf(":%d", port), Handler: a.Router} server := http.Server{Addr: fmt.Sprintf(":%d", a.serverConfig.Port), Handler: a.Router}
err := httputil.ListenAndServeContext(ctx, &server) err := httputil.ListenAndServeContext(ctx, &server)
if err != nil { if err != nil {
a.log.Error("api server stopped", "err", err) a.log.Error("api server stopped", "err", err)
} else { } else {
a.log.Info("api server stopped") a.log.Info("api server stopped")
} }
return err
}
func (a *Api) startMetricsServer(ctx context.Context) error {
a.log.Info("starting metrics server...", "port", a.metricsConfig.Port)
err := metrics.ListenAndServe(ctx, a.metricsRegistry, a.metricsConfig.Host, a.metricsConfig.Port)
if err != nil {
a.log.Error("metrics server stopped", "err", err)
} else {
a.log.Info("metrics server stopped")
}
return err return err
} }
...@@ -6,6 +6,7 @@ import ( ...@@ -6,6 +6,7 @@ import (
"net/http/httptest" "net/http/httptest"
"testing" "testing"
"github.com/ethereum-optimism/optimism/indexer/config"
"github.com/ethereum-optimism/optimism/indexer/database" "github.com/ethereum-optimism/optimism/indexer/database"
"github.com/ethereum-optimism/optimism/op-node/testlog" "github.com/ethereum-optimism/optimism/op-node/testlog"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
...@@ -18,6 +19,15 @@ type MockBridgeTransfersView struct{} ...@@ -18,6 +19,15 @@ type MockBridgeTransfersView struct{}
var mockAddress = "0x4204204204204204204204204204204204204204" var mockAddress = "0x4204204204204204204204204204204204204204"
var apiConfig = config.ServerConfig{
Host: "localhost",
Port: 8080,
}
var metricsConfig = config.ServerConfig{
Host: "localhost",
Port: 7300,
}
var ( var (
deposit = database.L1BridgeDeposit{ deposit = database.L1BridgeDeposit{
TransactionSourceHash: common.HexToHash("abc"), TransactionSourceHash: common.HexToHash("abc"),
...@@ -77,7 +87,7 @@ func (mbv *MockBridgeTransfersView) L2BridgeWithdrawalsByAddress(address common. ...@@ -77,7 +87,7 @@ func (mbv *MockBridgeTransfersView) L2BridgeWithdrawalsByAddress(address common.
} }
func TestHealthz(t *testing.T) { func TestHealthz(t *testing.T) {
logger := testlog.Logger(t, log.LvlInfo) logger := testlog.Logger(t, log.LvlInfo)
api := NewApi(logger, &MockBridgeTransfersView{}) api := NewApi(logger, &MockBridgeTransfersView{}, apiConfig, metricsConfig)
request, err := http.NewRequest("GET", "/healthz", nil) request, err := http.NewRequest("GET", "/healthz", nil)
assert.Nil(t, err) assert.Nil(t, err)
...@@ -89,7 +99,7 @@ func TestHealthz(t *testing.T) { ...@@ -89,7 +99,7 @@ func TestHealthz(t *testing.T) {
func TestL1BridgeDepositsHandler(t *testing.T) { func TestL1BridgeDepositsHandler(t *testing.T) {
logger := testlog.Logger(t, log.LvlInfo) logger := testlog.Logger(t, log.LvlInfo)
api := NewApi(logger, &MockBridgeTransfersView{}) api := NewApi(logger, &MockBridgeTransfersView{}, apiConfig, metricsConfig)
request, err := http.NewRequest("GET", fmt.Sprintf("/api/v0/deposits/%s", mockAddress), nil) request, err := http.NewRequest("GET", fmt.Sprintf("/api/v0/deposits/%s", mockAddress), nil)
assert.Nil(t, err) assert.Nil(t, err)
...@@ -101,7 +111,7 @@ func TestL1BridgeDepositsHandler(t *testing.T) { ...@@ -101,7 +111,7 @@ func TestL1BridgeDepositsHandler(t *testing.T) {
func TestL2BridgeWithdrawalsByAddressHandler(t *testing.T) { func TestL2BridgeWithdrawalsByAddressHandler(t *testing.T) {
logger := testlog.Logger(t, log.LvlInfo) logger := testlog.Logger(t, log.LvlInfo)
api := NewApi(logger, &MockBridgeTransfersView{}) api := NewApi(logger, &MockBridgeTransfersView{}, apiConfig, metricsConfig)
request, err := http.NewRequest("GET", fmt.Sprintf("/api/v0/withdrawals/%s", mockAddress), nil) request, err := http.NewRequest("GET", fmt.Sprintf("/api/v0/withdrawals/%s", mockAddress), nil)
assert.Nil(t, err) assert.Nil(t, err)
......
...@@ -62,8 +62,8 @@ func runApi(ctx *cli.Context) error { ...@@ -62,8 +62,8 @@ func runApi(ctx *cli.Context) error {
} }
defer db.Close() defer db.Close()
api := api.NewApi(log, db.BridgeTransfers) api := api.NewApi(log, db.BridgeTransfers, cfg.HTTPServer, cfg.MetricsServer)
return api.Listen(ctx.Context, cfg.HTTPServer.Port) return api.Start(ctx.Context)
} }
func runAll(ctx *cli.Context) error { func runAll(ctx *cli.Context) error {
......
...@@ -233,7 +233,7 @@ func (db *blocksDB) LatestEpoch() (*Epoch, error) { ...@@ -233,7 +233,7 @@ func (db *blocksDB) LatestEpoch() (*Epoch, error) {
// L2 for a faster query. Per the protocol, the L2 block that starts a new epoch // L2 for a faster query. Per the protocol, the L2 block that starts a new epoch
// will have a matching timestamp with the L1 origin. // will have a matching timestamp with the L1 origin.
query := db.gorm.Table("l1_block_headers").Order("l1_block_headers.timestamp DESC") query := db.gorm.Table("l1_block_headers").Order("l1_block_headers.timestamp DESC")
query = query.Joins("INNER JOIN l2_block_headers ON l1_block_headers.timestamp = l2_block_headers.timestamp") query = query.Joins("INNER JOIN l2_block_headers ON l2_block_headers.timestamp = l1_block_headers.timestamp")
query = query.Select("*") query = query.Select("*")
var epoch Epoch var epoch Epoch
......
...@@ -47,7 +47,10 @@ type L2TransactionWithdrawal struct { ...@@ -47,7 +47,10 @@ type L2TransactionWithdrawal struct {
type BridgeTransactionsView interface { type BridgeTransactionsView interface {
L1TransactionDeposit(common.Hash) (*L1TransactionDeposit, error) L1TransactionDeposit(common.Hash) (*L1TransactionDeposit, error)
L1LatestBlockHeader() (*L1BlockHeader, error)
L2TransactionWithdrawal(common.Hash) (*L2TransactionWithdrawal, error) L2TransactionWithdrawal(common.Hash) (*L2TransactionWithdrawal, error)
L2LatestBlockHeader() (*L2BlockHeader, error)
} }
type BridgeTransactionsDB interface { type BridgeTransactionsDB interface {
...@@ -94,6 +97,37 @@ func (db *bridgeTransactionsDB) L1TransactionDeposit(sourceHash common.Hash) (*L ...@@ -94,6 +97,37 @@ func (db *bridgeTransactionsDB) L1TransactionDeposit(sourceHash common.Hash) (*L
return &deposit, nil return &deposit, nil
} }
func (db *bridgeTransactionsDB) L1LatestBlockHeader() (*L1BlockHeader, error) {
// Markers for an indexed bridge event
// L1: Latest Transaction Deposit, Latest Proven/Finalized Withdrawal
l1DepositQuery := db.gorm.Table("l1_transaction_deposits").Order("l1_transaction_deposits.timestamp DESC").Limit(1)
l1DepositQuery = l1DepositQuery.Joins("INNER JOIN l1_contract_events ON l1_contract_events.guid = l1_transaction_deposits.initiated_l1_event_guid")
l1DepositQuery = l1DepositQuery.Select("l1_contract_events.*")
l1ProvenQuery := db.gorm.Table("l2_transaction_withdrawals")
l1ProvenQuery = l1ProvenQuery.Joins("INNER JOIN l1_contract_events ON l1_contract_events.guid = l2_transaction_withdrawals.proven_l1_event_guid")
l1ProvenQuery = l1ProvenQuery.Order("l1_contract_events.timestamp DESC").Select("l1_contract_events.*").Limit(1)
l1FinalizedQuery := db.gorm.Table("l2_transaction_withdrawals")
l1FinalizedQuery = l1FinalizedQuery.Joins("INNER JOIN l1_contract_events ON l1_contract_events.guid = l2_transaction_withdrawals.proven_l1_event_guid")
l1FinalizedQuery = l1FinalizedQuery.Order("l1_contract_events.timestamp DESC").Select("l1_contract_events.*").Limit(1)
l1Query := db.gorm.Table("((?) UNION (?) UNION (?)) AS latest_bridge_events", l1DepositQuery.Limit(1), l1ProvenQuery, l1FinalizedQuery)
l1Query = l1Query.Joins("INNER JOIN l1_block_headers ON l1_block_headers.hash = latest_bridge_events.block_hash")
l1Query = l1Query.Order("l1_block_headers.number DESC").Select("l1_block_headers.*")
var l1Header L1BlockHeader
result := l1Query.Take(&l1Header)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, result.Error
}
return &l1Header, nil
}
/** /**
* Transactions withdrawn from L2 * Transactions withdrawn from L2
*/ */
...@@ -149,3 +183,25 @@ func (db *bridgeTransactionsDB) MarkL2TransactionWithdrawalFinalizedEvent(withdr ...@@ -149,3 +183,25 @@ func (db *bridgeTransactionsDB) MarkL2TransactionWithdrawalFinalizedEvent(withdr
result := db.gorm.Save(&withdrawal) result := db.gorm.Save(&withdrawal)
return result.Error return result.Error
} }
func (db *bridgeTransactionsDB) L2LatestBlockHeader() (*L2BlockHeader, error) {
// L2: Inclusion of the latest deposit
l1DepositQuery := db.gorm.Table("l1_transaction_deposits").Order("l1_transaction_deposits.timestamp DESC")
l1DepositQuery = l1DepositQuery.Joins("INNER JOIN l1_contract_events ON l1_contract_events.guid = l1_transaction_deposits.initiated_l1_event_guid")
l1DepositQuery = l1DepositQuery.Select("l1_contract_events.*")
l2Query := db.gorm.Table("(?) AS l1_deposit_events", l1DepositQuery)
l2Query = l2Query.Joins("INNER JOIN l2_block_headers ON l2_block_headers.timestamp = l1_deposit_events.timestamp")
l2Query = l2Query.Order("l2_block_headers.timestamp DESC").Select("l2_block_headers.*")
var l2Header L2BlockHeader
result := l2Query.Take(&l2Header)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, result.Error
}
return &l2Header, nil
}
...@@ -44,7 +44,7 @@ func NewL1ETL(cfg Config, log log.Logger, db *database.DB, metrics Metricer, cli ...@@ -44,7 +44,7 @@ func NewL1ETL(cfg Config, log log.Logger, db *database.DB, metrics Metricer, cli
fromHeader = latestHeader.RLPHeader.Header() fromHeader = latestHeader.RLPHeader.Header()
} else if cfg.StartHeight.BitLen() > 0 { } else if cfg.StartHeight.BitLen() > 0 {
log.Info("no indexed state in storage, starting from supplied L1 height", "height", cfg.StartHeight.String()) log.Info("no indexed state starting from supplied L1 height", "height", cfg.StartHeight.String())
header, err := client.BlockHeaderByNumber(cfg.StartHeight) header, err := client.BlockHeaderByNumber(cfg.StartHeight)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not fetch starting block header: %w", err) return nil, fmt.Errorf("could not fetch starting block header: %w", err)
...@@ -53,7 +53,7 @@ func NewL1ETL(cfg Config, log log.Logger, db *database.DB, metrics Metricer, cli ...@@ -53,7 +53,7 @@ func NewL1ETL(cfg Config, log log.Logger, db *database.DB, metrics Metricer, cli
fromHeader = header fromHeader = header
} else { } else {
log.Info("no indexed state in storage, starting from L1 genesis") log.Info("no indexed state, starting from genesis")
} }
// NOTE - The use of un-buffered channel here assumes that downstream consumers // NOTE - The use of un-buffered channel here assumes that downstream consumers
......
...@@ -22,11 +22,15 @@ l1-rpc = "${INDEXER_RPC_URL_L1}" ...@@ -22,11 +22,15 @@ l1-rpc = "${INDEXER_RPC_URL_L1}"
l2-rpc = "${INDEXER_RPC_URL_L2}" l2-rpc = "${INDEXER_RPC_URL_L2}"
[db] [db]
host = "postgres" host = "$INDEXER_DB_HOST"
port = 5432 # this port may be problematic once we depoly
user = "db_username" # the DATABASE_URL looks like this for previous services and didn't include a port
password = "db_password" # DATABASE_URL="postgresql://${INDEXER_DB_USER}:${INDEXER_DB_PASS}@localhost/${INDEXER_DB_NAME}?host=${INDEXER_DB_HOST}"
name = "db_name" # If not problematic delete these comments
port = $INDEXER_DB_PORT
user = "$INDEXER_DB_USER"
password = "$INDEXER_DB_PASS"
name = "$INDEXER_DB_NAME"
[http] [http]
host = "127.0.0.1" host = "127.0.0.1"
......
...@@ -29,35 +29,32 @@ type BridgeProcessor struct { ...@@ -29,35 +29,32 @@ type BridgeProcessor struct {
func NewBridgeProcessor(log log.Logger, db *database.DB, l1Etl *etl.L1ETL, chainConfig config.ChainConfig) (*BridgeProcessor, error) { func NewBridgeProcessor(log log.Logger, db *database.DB, l1Etl *etl.L1ETL, chainConfig config.ChainConfig) (*BridgeProcessor, error) {
log = log.New("processor", "bridge") log = log.New("processor", "bridge")
latestL1Header, err := bridge.L1LatestBridgeEventHeader(db, chainConfig) latestL1Header, err := db.BridgeTransactions.L1LatestBlockHeader()
if err != nil { if err != nil {
return nil, err return nil, err
} }
latestL2Header, err := bridge.L2LatestBridgeEventHeader(db) latestL2Header, err := db.BridgeTransactions.L2LatestBlockHeader()
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Since the bridge processor indexes events based on epochs, there's var l1Header, l2Header *types.Header
// no scenario in which we have indexed L2 data with no L1 data.
//
// NOTE: Technically there is an exception if our bridging contracts are
// used to bridges native from L2 and an op-chain happens to launch where
// only L2 native bridge events have occurred. This is a rare situation now
// and it's worth the assertion as an integrity check. We can revisit this
// as more chains launch with primarily L2-native activity.
if latestL1Header == nil && latestL2Header != nil {
log.Error("detected indexed L2 bridge activity with no indexed L1 state", "l2_block_number", latestL2Header.Number)
return nil, errors.New("detected indexed L2 bridge activity with no indexed L1 state")
}
if latestL1Header == nil && latestL2Header == nil { if latestL1Header == nil && latestL2Header == nil {
log.Info("no indexed state, starting from genesis") log.Info("no indexed state, starting from rollup genesis")
} else { } else {
log.Info("detected the latest indexed state", "l1_block_number", latestL1Header.Number, "l2_block_number", latestL2Header.Number) l1Height, l2Height := big.NewInt(0), big.NewInt(0)
if latestL1Header != nil {
l1Height = latestL1Header.Number
l1Header = latestL1Header.RLPHeader.Header()
}
if latestL2Header != nil {
l2Height = latestL2Header.Number
l2Header = latestL2Header.RLPHeader.Header()
}
log.Info("detected latest indexed state", "l1_block_number", l1Height, "l2_block_number", l2Height)
} }
return &BridgeProcessor{log, db, l1Etl, chainConfig, latestL1Header, latestL2Header}, nil return &BridgeProcessor{log, db, l1Etl, chainConfig, l1Header, l2Header}, nil
} }
func (b *BridgeProcessor) Start(ctx context.Context) error { func (b *BridgeProcessor) Start(ctx context.Context) error {
...@@ -83,29 +80,40 @@ func (b *BridgeProcessor) Start(ctx context.Context) error { ...@@ -83,29 +80,40 @@ func (b *BridgeProcessor) Start(ctx context.Context) error {
latestEpoch, err := b.db.Blocks.LatestEpoch() latestEpoch, err := b.db.Blocks.LatestEpoch()
if err != nil { if err != nil {
return err return err
} } else if latestEpoch == nil {
if latestEpoch == nil { if b.LatestL1Header != nil || b.LatestL2Header != nil {
if b.LatestL1Header != nil { // Once we have some indexed state `latestEpoch` can never return nil
// Once we have some state `latestEpoch` should never return nil. b.log.Error("bridge events indexed, but no indexed epoch returned", "latest_bridge_l1_block_number", b.LatestL1Header.Number)
b.log.Error("started with indexed bridge state, but no latest epoch returned", "latest_bridge_l1_block_number", b.LatestL1Header.Number) return errors.New("bridge events indexed, but no indexed epoch returned")
return errors.New("started with indexed bridge state, but no blocks epochs returned")
} else {
b.log.Warn("no indexed epochs. waiting...")
continue
} }
b.log.Warn("no indexed epochs available. waiting...")
continue
} }
// Integrity Checks
if b.LatestL1Header != nil && latestEpoch.L1BlockHeader.Hash == b.LatestL1Header.Hash() { if b.LatestL1Header != nil && latestEpoch.L1BlockHeader.Hash == b.LatestL1Header.Hash() {
// Marked as a warning since the bridge should always be processing at least 1 new epoch b.log.Warn("all available epochs indexed", "latest_bridge_l1_block_number", b.LatestL1Header.Number)
b.log.Warn("all available epochs indexed by the bridge", "latest_epoch_number", b.LatestL1Header.Number)
continue continue
} }
if b.LatestL1Header != nil && latestEpoch.L1BlockHeader.Number.Cmp(b.LatestL1Header.Number) <= 0 {
b.log.Error("non-increasing l1 block height observed", "latest_bridge_l1_block_number", b.LatestL1Header.Number, "latest_epoch_number", latestEpoch.L1BlockHeader.Number)
return errors.New("non-increasing l1 block heght observed")
}
if b.LatestL2Header != nil && latestEpoch.L2BlockHeader.Number.Cmp(b.LatestL2Header.Number) <= 0 {
b.log.Error("non-increasing l2 block height observed", "latest_bridge_l2_block_number", b.LatestL2Header.Number, "latest_epoch_number", latestEpoch.L2BlockHeader.Number)
return errors.New("non-increasing l2 block heght observed")
}
// Process Bridge Events
toL1Height, toL2Height := latestEpoch.L1BlockHeader.Number, latestEpoch.L2BlockHeader.Number toL1Height, toL2Height := latestEpoch.L1BlockHeader.Number, latestEpoch.L2BlockHeader.Number
fromL1Height, fromL2Height := big.NewInt(0), big.NewInt(0) fromL1Height, fromL2Height := big.NewInt(0), big.NewInt(0)
if b.LatestL1Header != nil { if b.LatestL1Header != nil {
// `NewBridgeProcessor` ensures that LatestL2Header must not be nil if LatestL1Header is set
fromL1Height = new(big.Int).Add(b.LatestL1Header.Number, big.NewInt(1)) fromL1Height = new(big.Int).Add(b.LatestL1Header.Number, big.NewInt(1))
}
if b.LatestL2Header != nil {
fromL2Height = new(big.Int).Add(b.LatestL2Header.Number, big.NewInt(1)) fromL2Height = new(big.Int).Add(b.LatestL2Header.Number, big.NewInt(1))
} }
...@@ -139,7 +147,7 @@ func (b *BridgeProcessor) Start(ctx context.Context) error { ...@@ -139,7 +147,7 @@ func (b *BridgeProcessor) Start(ctx context.Context) error {
// Try again on a subsequent interval // Try again on a subsequent interval
batchLog.Error("unable to index new bridge events", "err", err) batchLog.Error("unable to index new bridge events", "err", err)
} else { } else {
batchLog.Info("done indexing new bridge events", "latest_l1_block_number", toL1Height, "latest_l2_block_number", toL2Height) batchLog.Info("done indexing bridge events", "latest_l1_block_number", toL1Height, "latest_l2_block_number", toL2Height)
b.LatestL1Header = latestEpoch.L1BlockHeader.RLPHeader.Header() b.LatestL1Header = latestEpoch.L1BlockHeader.RLPHeader.Header()
b.LatestL2Header = latestEpoch.L2BlockHeader.RLPHeader.Header() b.LatestL2Header = latestEpoch.L2BlockHeader.RLPHeader.Header()
} }
......
...@@ -8,7 +8,6 @@ import ( ...@@ -8,7 +8,6 @@ import (
"github.com/ethereum-optimism/optimism/indexer/config" "github.com/ethereum-optimism/optimism/indexer/config"
"github.com/ethereum-optimism/optimism/indexer/database" "github.com/ethereum-optimism/optimism/indexer/database"
"github.com/ethereum-optimism/optimism/indexer/processors/contracts" "github.com/ethereum-optimism/optimism/indexer/processors/contracts"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
...@@ -65,7 +64,7 @@ func L1ProcessInitiatedBridgeEvents(log log.Logger, db *database.DB, chainConfig ...@@ -65,7 +64,7 @@ func L1ProcessInitiatedBridgeEvents(log log.Logger, db *database.DB, chainConfig
// extract the deposit hash from the previous TransactionDepositedEvent // extract the deposit hash from the previous TransactionDepositedEvent
portalDeposit, ok := portalDeposits[logKey{sentMessage.Event.BlockHash, sentMessage.Event.LogIndex - 1}] portalDeposit, ok := portalDeposits[logKey{sentMessage.Event.BlockHash, sentMessage.Event.LogIndex - 1}]
if !ok { if !ok {
return fmt.Errorf("missing expected preceding TransactionDeposit for SentMessage. tx_hash = %s", sentMessage.Event.TransactionHash) return fmt.Errorf("expected TransactionDeposit preceding SentMessage event. tx_hash = %s", sentMessage.Event.TransactionHash)
} }
l1BridgeMessages[i] = database.L1BridgeMessage{TransactionSourceHash: portalDeposit.DepositTx.SourceHash, BridgeMessage: sentMessage.BridgeMessage} l1BridgeMessages[i] = database.L1BridgeMessage{TransactionSourceHash: portalDeposit.DepositTx.SourceHash, BridgeMessage: sentMessage.BridgeMessage}
...@@ -94,11 +93,11 @@ func L1ProcessInitiatedBridgeEvents(log log.Logger, db *database.DB, chainConfig ...@@ -94,11 +93,11 @@ func L1ProcessInitiatedBridgeEvents(log log.Logger, db *database.DB, chainConfig
// extract the cross domain message hash & deposit source hash from the following events // extract the cross domain message hash & deposit source hash from the following events
portalDeposit, ok := portalDeposits[logKey{initiatedBridge.Event.BlockHash, initiatedBridge.Event.LogIndex + 1}] portalDeposit, ok := portalDeposits[logKey{initiatedBridge.Event.BlockHash, initiatedBridge.Event.LogIndex + 1}]
if !ok { if !ok {
return fmt.Errorf("missing expected following TransactionDeposit for BridgeInitiated. tx_hash = %s", initiatedBridge.Event.TransactionHash) return fmt.Errorf("expected TransactionDeposit following BridgeInitiated event. tx_hash = %s", initiatedBridge.Event.TransactionHash)
} }
sentMessage, ok := sentMessages[logKey{initiatedBridge.Event.BlockHash, initiatedBridge.Event.LogIndex + 2}] sentMessage, ok := sentMessages[logKey{initiatedBridge.Event.BlockHash, initiatedBridge.Event.LogIndex + 2}]
if !ok { if !ok {
return fmt.Errorf("missing expected following SentMessage for BridgeInitiated. tx_hash = %s", initiatedBridge.Event.TransactionHash) return fmt.Errorf("expected SentMessage following TransactionDeposit event. tx_hash = %s", initiatedBridge.Event.TransactionHash)
} }
initiatedBridge.BridgeTransfer.CrossDomainMessageHash = &sentMessage.BridgeMessage.MessageHash initiatedBridge.BridgeTransfer.CrossDomainMessageHash = &sentMessage.BridgeMessage.MessageHash
...@@ -216,7 +215,7 @@ func L1ProcessFinalizedBridgeEvents(log log.Logger, db *database.DB, chainConfig ...@@ -216,7 +215,7 @@ func L1ProcessFinalizedBridgeEvents(log log.Logger, db *database.DB, chainConfig
finalizedBridge := finalizedBridges[i] finalizedBridge := finalizedBridges[i]
relayedMessage, ok := relayedMessages[logKey{finalizedBridge.Event.BlockHash, finalizedBridge.Event.LogIndex + 1}] relayedMessage, ok := relayedMessages[logKey{finalizedBridge.Event.BlockHash, finalizedBridge.Event.LogIndex + 1}]
if !ok { if !ok {
return fmt.Errorf("missing following RelayedMessage for BridgeFinalized event. tx_hash = %s", finalizedBridge.Event.TransactionHash) return fmt.Errorf("expected RelayedMessage following BridgeFinalized event. tx_hash = %s", finalizedBridge.Event.TransactionHash)
} }
// Since the message hash is computed from the relayed message, this ensures the deposit fields must match. For good measure, // Since the message hash is computed from the relayed message, this ensures the deposit fields must match. For good measure,
...@@ -233,80 +232,3 @@ func L1ProcessFinalizedBridgeEvents(log log.Logger, db *database.DB, chainConfig ...@@ -233,80 +232,3 @@ func L1ProcessFinalizedBridgeEvents(log log.Logger, db *database.DB, chainConfig
// a-ok! // a-ok!
return nil return nil
} }
// L1LatestBridgeEventHeader returns the latest header for which and on-chain event
// has been observed on L1 -- Both initiated L1 events and finalization markers on L2.
func L1LatestBridgeEventHeader(db *database.DB, chainConfig config.ChainConfig) (*types.Header, error) {
portalAbi, err := bindings.OptimismPortalMetaData.GetAbi()
if err != nil {
return nil, err
}
depositEventID := portalAbi.Events["TransactionDeposited"].ID
provenEventID := portalAbi.Events["WithdrawalProven"].ID
finalizedEventID := portalAbi.Events["WithdrawalFinalized"].ID
// (1) Initiated L1 Events
// Since all initaited bridge events eventually reach the OptimismPortal to
// conduct the deposit, we can simply look for the last deposited transaction
// event on L2.
var latestDepositHeader *types.Header
contractEventFilter := database.ContractEvent{ContractAddress: chainConfig.L1Contracts.OptimismPortalProxy, EventSignature: depositEventID}
depositEvent, err := db.ContractEvents.L1LatestContractEventWithFilter(contractEventFilter)
if err != nil {
return nil, err
}
if depositEvent != nil {
l1BlockHeader, err := db.Blocks.L1BlockHeader(depositEvent.BlockHash)
if err != nil {
return nil, err
}
if l1BlockHeader != nil {
latestDepositHeader = l1BlockHeader.RLPHeader.Header()
}
}
// (2) Finalization markers for L2
// Like initiated L1 events, all withdrawals must flow through the OptimismPortal
// contract. We must look for both proven and finalized withdrawal events.
var latestWithdrawHeader *types.Header
contractEventFilter.EventSignature = finalizedEventID
withdrawEvent, err := db.ContractEvents.L1LatestContractEventWithFilter(contractEventFilter)
if err != nil {
return nil, err
}
if withdrawEvent != nil {
// Check if a have a later detected proven event
contractEventFilter.EventSignature = provenEventID
provenEvent, err := db.ContractEvents.L1LatestContractEventWithFilter(contractEventFilter)
if err != nil {
return nil, err
}
if provenEvent != nil && provenEvent.Timestamp > withdrawEvent.Timestamp {
withdrawEvent = provenEvent
}
l1BlockHeader, err := db.Blocks.L1BlockHeader(withdrawEvent.BlockHash)
if err != nil {
return nil, err
}
latestWithdrawHeader = l1BlockHeader.RLPHeader.Header()
}
if latestDepositHeader == nil {
// If there has been no seen deposits yet, there could have been no seen withdrawals
if latestWithdrawHeader != nil {
return nil, errors.New("detected an indexed withdrawal without any deposits")
}
return nil, nil
} else if latestWithdrawHeader == nil {
return latestDepositHeader, nil
} else {
// both deposits & withdrawals have occurred
if latestDepositHeader.Time > latestWithdrawHeader.Time {
return latestDepositHeader, nil
} else {
return latestWithdrawHeader, nil
}
}
}
...@@ -7,10 +7,8 @@ import ( ...@@ -7,10 +7,8 @@ import (
"github.com/ethereum-optimism/optimism/indexer/database" "github.com/ethereum-optimism/optimism/indexer/database"
"github.com/ethereum-optimism/optimism/indexer/processors/contracts" "github.com/ethereum-optimism/optimism/indexer/processors/contracts"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys" "github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
) )
...@@ -65,7 +63,7 @@ func L2ProcessInitiatedBridgeEvents(log log.Logger, db *database.DB, fromHeight ...@@ -65,7 +63,7 @@ func L2ProcessInitiatedBridgeEvents(log log.Logger, db *database.DB, fromHeight
// extract the withdrawal hash from the previous MessagePassed event // extract the withdrawal hash from the previous MessagePassed event
messagePassed, ok := messagesPassed[logKey{sentMessage.Event.BlockHash, sentMessage.Event.LogIndex - 1}] messagePassed, ok := messagesPassed[logKey{sentMessage.Event.BlockHash, sentMessage.Event.LogIndex - 1}]
if !ok { if !ok {
return fmt.Errorf("missing expected preceding MessagePassedEvent for SentMessage. tx_hash = %s", sentMessage.Event.TransactionHash) return fmt.Errorf("expected MessagePassedEvent preceding SentMessage. tx_hash = %s", sentMessage.Event.TransactionHash)
} }
l2BridgeMessages[i] = database.L2BridgeMessage{TransactionWithdrawalHash: messagePassed.WithdrawalHash, BridgeMessage: sentMessage.BridgeMessage} l2BridgeMessages[i] = database.L2BridgeMessage{TransactionWithdrawalHash: messagePassed.WithdrawalHash, BridgeMessage: sentMessage.BridgeMessage}
...@@ -94,11 +92,11 @@ func L2ProcessInitiatedBridgeEvents(log log.Logger, db *database.DB, fromHeight ...@@ -94,11 +92,11 @@ func L2ProcessInitiatedBridgeEvents(log log.Logger, db *database.DB, fromHeight
// extract the cross domain message hash & deposit source hash from the following events // extract the cross domain message hash & deposit source hash from the following events
messagePassed, ok := messagesPassed[logKey{initiatedBridge.Event.BlockHash, initiatedBridge.Event.LogIndex + 1}] messagePassed, ok := messagesPassed[logKey{initiatedBridge.Event.BlockHash, initiatedBridge.Event.LogIndex + 1}]
if !ok { if !ok {
return fmt.Errorf("missing expected following MessagePassed for BridgeInitiated. tx_hash = %s", initiatedBridge.Event.TransactionHash) return fmt.Errorf("expected MessagePassed following BridgeInitiated event. tx_hash = %s", initiatedBridge.Event.TransactionHash)
} }
sentMessage, ok := sentMessages[logKey{initiatedBridge.Event.BlockHash, initiatedBridge.Event.LogIndex + 2}] sentMessage, ok := sentMessages[logKey{initiatedBridge.Event.BlockHash, initiatedBridge.Event.LogIndex + 2}]
if !ok { if !ok {
return fmt.Errorf("missing expected following SentMessage for BridgeInitiated. tx_hash = %s", initiatedBridge.Event.TransactionHash) return fmt.Errorf("expected SentMessage following MessagePassed event. tx_hash = %s", initiatedBridge.Event.TransactionHash)
} }
initiatedBridge.BridgeTransfer.CrossDomainMessageHash = &sentMessage.BridgeMessage.MessageHash initiatedBridge.BridgeTransfer.CrossDomainMessageHash = &sentMessage.BridgeMessage.MessageHash
...@@ -165,7 +163,7 @@ func L2ProcessFinalizedBridgeEvents(log log.Logger, db *database.DB, fromHeight ...@@ -165,7 +163,7 @@ func L2ProcessFinalizedBridgeEvents(log log.Logger, db *database.DB, fromHeight
finalizedBridge := finalizedBridges[i] finalizedBridge := finalizedBridges[i]
relayedMessage, ok := relayedMessages[logKey{finalizedBridge.Event.BlockHash, finalizedBridge.Event.LogIndex + 1}] relayedMessage, ok := relayedMessages[logKey{finalizedBridge.Event.BlockHash, finalizedBridge.Event.LogIndex + 1}]
if !ok { if !ok {
return fmt.Errorf("missing following RelayedMessage for BridgeFinalized event. tx_hash = %s", finalizedBridge.Event.TransactionHash) return fmt.Errorf("expected RelayedMessage following BridgeFinalized event. tx_hash = %s", finalizedBridge.Event.TransactionHash)
} }
// Since the message hash is computed from the relayed message, this ensures the withdrawal fields must match. For good measure, // Since the message hash is computed from the relayed message, this ensures the withdrawal fields must match. For good measure,
...@@ -182,71 +180,3 @@ func L2ProcessFinalizedBridgeEvents(log log.Logger, db *database.DB, fromHeight ...@@ -182,71 +180,3 @@ func L2ProcessFinalizedBridgeEvents(log log.Logger, db *database.DB, fromHeight
// a-ok! // a-ok!
return nil return nil
} }
// L2LatestBridgeEventHeader returns the latest header for which and on-chain event
// has been observed on L2 -- Both initiated L2 events and finalization markers from L1.
func L2LatestBridgeEventHeader(db *database.DB) (*types.Header, error) {
l2ToL1MessagePasserAbi, err := bindings.L2ToL1MessagePasserMetaData.GetAbi()
if err != nil {
return nil, err
}
crossDomainMessengerAbi, err := bindings.CrossDomainMessengerMetaData.GetAbi()
if err != nil {
return nil, err
}
messagePassedID := l2ToL1MessagePasserAbi.Events["MessagePassed"].ID
relayedEventID := crossDomainMessengerAbi.Events["RelayedMessage"].ID
// (1) Initiated L2 Events
// Since all initiated bridge events eventually reach the L2ToL1MessagePasser to
// initiate the withdrawal, we can simply look for the last message passed from
// this cont
var latestWithdrawHeader *types.Header
contractEventFilter := database.ContractEvent{ContractAddress: predeploys.L2ToL1MessagePasserAddr, EventSignature: messagePassedID}
withdrawEvent, err := db.ContractEvents.L2LatestContractEventWithFilter(contractEventFilter)
if err != nil {
return nil, err
}
if withdrawEvent != nil {
l2BlockHeader, err := db.Blocks.L2BlockHeader(withdrawEvent.BlockHash)
if err != nil {
return nil, err
}
if l2BlockHeader != nil {
latestWithdrawHeader = l2BlockHeader.RLPHeader.Header()
}
}
// (2) Finalization markers for L1
// Since deposited transactions from L1 are apart of the block derivation process,
// there are no native finalization markers for OptimismPortal#TransactionDeposited.
// The lowest layer to check for here is the CrossDomainMessenger#RelayedMessage event.
// This also converts the StandardBridge which simply is an extension of the messenger.
var latestRelayedMessageHeader *types.Header
contractEventFilter = database.ContractEvent{ContractAddress: predeploys.L2CrossDomainMessengerAddr, EventSignature: relayedEventID}
relayedEvent, err := db.ContractEvents.L2LatestContractEventWithFilter(contractEventFilter)
if err != nil {
return nil, err
}
if relayedEvent != nil {
l2BlockHeader, err := db.Blocks.L2BlockHeader(relayedEvent.BlockHash)
if err != nil {
return nil, err
}
if l2BlockHeader != nil {
latestRelayedMessageHeader = l2BlockHeader.RLPHeader.Header()
}
}
// No causaal relationship between withdraw and relayed messages
if latestWithdrawHeader == nil || latestRelayedMessageHeader == nil {
return nil, nil
} else {
if latestWithdrawHeader.Time > latestRelayedMessageHeader.Time {
return latestWithdrawHeader, nil
} else {
return latestRelayedMessageHeader, nil
}
}
}
This diff is collapsed.
This diff is collapsed.
...@@ -13,7 +13,7 @@ const AlphabetVMStorageLayoutJSON = "{\"storage\":[{\"astId\":1000,\"contract\": ...@@ -13,7 +13,7 @@ const AlphabetVMStorageLayoutJSON = "{\"storage\":[{\"astId\":1000,\"contract\":
var AlphabetVMStorageLayout = new(solc.StorageLayout) var AlphabetVMStorageLayout = new(solc.StorageLayout)
var AlphabetVMDeployedBin = "0x608060405234801561001057600080fd5b50600436106100365760003560e01c80637dc0d1d01461003b578063f8e0cb9614610085575b600080fd5b60005461005b9073ffffffffffffffffffffffffffffffffffffffff1681565b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020015b60405180910390f35b6100986100933660046101a8565b6100a6565b60405190815260200161007c565b60008060007f000000000000000000000000000000000000000000000000000000000000000087876040516100dc929190610214565b60405180910390200361010057600091506100f986880188610224565b905061011f565b61010c8688018861023d565b90925090508161011b8161028e565b9250505b8161012b8260016102c6565b6040805160208101939093528201526060016040516020818303038152906040528051906020012092505050949350505050565b60008083601f84011261017157600080fd5b50813567ffffffffffffffff81111561018957600080fd5b6020830191508360208285010111156101a157600080fd5b9250929050565b600080600080604085870312156101be57600080fd5b843567ffffffffffffffff808211156101d657600080fd5b6101e28883890161015f565b909650945060208701359150808211156101fb57600080fd5b506102088782880161015f565b95989497509550505050565b8183823760009101908152919050565b60006020828403121561023657600080fd5b5035919050565b6000806040838503121561025057600080fd5b50508035926020909101359150565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82036102bf576102bf61025f565b5060010190565b600082198211156102d9576102d961025f565b50019056fea164736f6c634300080f000a" var AlphabetVMDeployedBin = "0x608060405234801561001057600080fd5b50600436106100365760003560e01c80637dc0d1d01461003b578063f8e0cb9614610085575b600080fd5b60005461005b9073ffffffffffffffffffffffffffffffffffffffff1681565b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020015b60405180910390f35b610098610093366004610212565b6100a6565b60405190815260200161007c565b600080600060087f0000000000000000000000000000000000000000000000000000000000000000901b600888886040516100e292919061027e565b6040518091039020901b0361010857600091506101018688018861028e565b9050610127565b610114868801886102a7565b909250905081610123816102f8565b9250505b81610133826001610330565b604080516020810193909352820152606001604080517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe081840301815291905280516020909101207effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff167f010000000000000000000000000000000000000000000000000000000000000017979650505050505050565b60008083601f8401126101db57600080fd5b50813567ffffffffffffffff8111156101f357600080fd5b60208301915083602082850101111561020b57600080fd5b9250929050565b6000806000806040858703121561022857600080fd5b843567ffffffffffffffff8082111561024057600080fd5b61024c888389016101c9565b9096509450602087013591508082111561026557600080fd5b50610272878288016101c9565b95989497509550505050565b8183823760009101908152919050565b6000602082840312156102a057600080fd5b5035919050565b600080604083850312156102ba57600080fd5b50508035926020909101359150565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8203610329576103296102c9565b5060010190565b60008219821115610343576103436102c9565b50019056fea164736f6c634300080f000a"
func init() { func init() {
if err := json.Unmarshal([]byte(AlphabetVMStorageLayoutJSON), AlphabetVMStorageLayout); err != nil { if err := json.Unmarshal([]byte(AlphabetVMStorageLayoutJSON), AlphabetVMStorageLayout); err != nil {
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
...@@ -7,6 +7,7 @@ import ( ...@@ -7,6 +7,7 @@ import (
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/solver" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/solver"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum-optimism/optimism/op-challenger/metrics" "github.com/ethereum-optimism/optimism/op-challenger/metrics"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
) )
...@@ -14,7 +15,7 @@ import ( ...@@ -14,7 +15,7 @@ import (
// Responder takes a response action & executes. // Responder takes a response action & executes.
// For full op-challenger this means executing the transaction on chain. // For full op-challenger this means executing the transaction on chain.
type Responder interface { type Responder interface {
CallResolve(ctx context.Context) (types.GameStatus, error) CallResolve(ctx context.Context) (gameTypes.GameStatus, error)
Resolve(ctx context.Context) error Resolve(ctx context.Context) error
Respond(ctx context.Context, response types.Claim) error Respond(ctx context.Context, response types.Claim) error
Step(ctx context.Context, stepData types.StepCallData) error Step(ctx context.Context, stepData types.StepCallData) error
...@@ -74,10 +75,10 @@ func (a *Agent) Act(ctx context.Context) error { ...@@ -74,10 +75,10 @@ func (a *Agent) Act(ctx context.Context) error {
// shouldResolve returns true if the agent should resolve the game. // shouldResolve returns true if the agent should resolve the game.
// This method will return false if the game is still in progress. // This method will return false if the game is still in progress.
func (a *Agent) shouldResolve(status types.GameStatus) bool { func (a *Agent) shouldResolve(status gameTypes.GameStatus) bool {
expected := types.GameStatusDefenderWon expected := gameTypes.GameStatusDefenderWon
if a.agreeWithProposedOutput { if a.agreeWithProposedOutput {
expected = types.GameStatusChallengerWon expected = gameTypes.GameStatusChallengerWon
} }
if expected != status { if expected != status {
a.log.Warn("Game will be lost", "expected", expected, "actual", status) a.log.Warn("Game will be lost", "expected", expected, "actual", status)
...@@ -89,7 +90,7 @@ func (a *Agent) shouldResolve(status types.GameStatus) bool { ...@@ -89,7 +90,7 @@ func (a *Agent) shouldResolve(status types.GameStatus) bool {
// Returns true if the game is resolvable (regardless of whether it was actually resolved) // Returns true if the game is resolvable (regardless of whether it was actually resolved)
func (a *Agent) tryResolve(ctx context.Context) bool { func (a *Agent) tryResolve(ctx context.Context) bool {
status, err := a.responder.CallResolve(ctx) status, err := a.responder.CallResolve(ctx)
if err != nil || status == types.GameStatusInProgress { if err != nil || status == gameTypes.GameStatusInProgress {
return false return false
} }
if !a.shouldResolve(status) { if !a.shouldResolve(status) {
......
...@@ -8,6 +8,7 @@ import ( ...@@ -8,6 +8,7 @@ import (
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/test" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/test"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/alphabet" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/alphabet"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum-optimism/optimism/op-challenger/metrics" "github.com/ethereum-optimism/optimism/op-challenger/metrics"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
...@@ -19,16 +20,16 @@ import ( ...@@ -19,16 +20,16 @@ import (
func TestShouldResolve(t *testing.T) { func TestShouldResolve(t *testing.T) {
t.Run("AgreeWithProposedOutput", func(t *testing.T) { t.Run("AgreeWithProposedOutput", func(t *testing.T) {
agent, _, _ := setupTestAgent(t, true) agent, _, _ := setupTestAgent(t, true)
require.False(t, agent.shouldResolve(types.GameStatusDefenderWon)) require.False(t, agent.shouldResolve(gameTypes.GameStatusDefenderWon))
require.True(t, agent.shouldResolve(types.GameStatusChallengerWon)) require.True(t, agent.shouldResolve(gameTypes.GameStatusChallengerWon))
require.False(t, agent.shouldResolve(types.GameStatusInProgress)) require.False(t, agent.shouldResolve(gameTypes.GameStatusInProgress))
}) })
t.Run("DisagreeWithProposedOutput", func(t *testing.T) { t.Run("DisagreeWithProposedOutput", func(t *testing.T) {
agent, _, _ := setupTestAgent(t, false) agent, _, _ := setupTestAgent(t, false)
require.True(t, agent.shouldResolve(types.GameStatusDefenderWon)) require.True(t, agent.shouldResolve(gameTypes.GameStatusDefenderWon))
require.False(t, agent.shouldResolve(types.GameStatusChallengerWon)) require.False(t, agent.shouldResolve(gameTypes.GameStatusChallengerWon))
require.False(t, agent.shouldResolve(types.GameStatusInProgress)) require.False(t, agent.shouldResolve(gameTypes.GameStatusInProgress))
}) })
} }
...@@ -38,31 +39,31 @@ func TestDoNotMakeMovesWhenGameIsResolvable(t *testing.T) { ...@@ -38,31 +39,31 @@ func TestDoNotMakeMovesWhenGameIsResolvable(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
agreeWithProposedOutput bool agreeWithProposedOutput bool
callResolveStatus types.GameStatus callResolveStatus gameTypes.GameStatus
shouldResolve bool shouldResolve bool
}{ }{
{ {
name: "Agree_Losing", name: "Agree_Losing",
agreeWithProposedOutput: true, agreeWithProposedOutput: true,
callResolveStatus: types.GameStatusDefenderWon, callResolveStatus: gameTypes.GameStatusDefenderWon,
shouldResolve: false, shouldResolve: false,
}, },
{ {
name: "Agree_Winning", name: "Agree_Winning",
agreeWithProposedOutput: true, agreeWithProposedOutput: true,
callResolveStatus: types.GameStatusChallengerWon, callResolveStatus: gameTypes.GameStatusChallengerWon,
shouldResolve: true, shouldResolve: true,
}, },
{ {
name: "Disagree_Losing", name: "Disagree_Losing",
agreeWithProposedOutput: false, agreeWithProposedOutput: false,
callResolveStatus: types.GameStatusChallengerWon, callResolveStatus: gameTypes.GameStatusChallengerWon,
shouldResolve: false, shouldResolve: false,
}, },
{ {
name: "Disagree_Winning", name: "Disagree_Winning",
agreeWithProposedOutput: false, agreeWithProposedOutput: false,
callResolveStatus: types.GameStatusDefenderWon, callResolveStatus: gameTypes.GameStatusDefenderWon,
shouldResolve: true, shouldResolve: true,
}, },
} }
...@@ -126,14 +127,14 @@ func (s *stubClaimLoader) FetchClaims(ctx context.Context) ([]types.Claim, error ...@@ -126,14 +127,14 @@ func (s *stubClaimLoader) FetchClaims(ctx context.Context) ([]types.Claim, error
type stubResponder struct { type stubResponder struct {
callResolveCount int callResolveCount int
callResolveStatus types.GameStatus callResolveStatus gameTypes.GameStatus
callResolveErr error callResolveErr error
resolveCount int resolveCount int
resolveErr error resolveErr error
} }
func (s *stubResponder) CallResolve(ctx context.Context) (types.GameStatus, error) { func (s *stubResponder) CallResolve(ctx context.Context) (gameTypes.GameStatus, error) {
s.callResolveCount++ s.callResolveCount++
return s.callResolveStatus, s.callResolveErr return s.callResolveStatus, s.callResolveErr
} }
......
...@@ -6,6 +6,7 @@ import ( ...@@ -6,6 +6,7 @@ import (
"github.com/ethereum-optimism/optimism/op-bindings/bindings" "github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
...@@ -49,9 +50,9 @@ func NewLoaderFromBindings(fdgAddr common.Address, client bind.ContractCaller) ( ...@@ -49,9 +50,9 @@ func NewLoaderFromBindings(fdgAddr common.Address, client bind.ContractCaller) (
} }
// GetGameStatus returns the current game status. // GetGameStatus returns the current game status.
func (l *loader) GetGameStatus(ctx context.Context) (types.GameStatus, error) { func (l *loader) GetGameStatus(ctx context.Context) (gameTypes.GameStatus, error) {
status, err := l.caller.Status(&bind.CallOpts{Context: ctx}) status, err := l.caller.Status(&bind.CallOpts{Context: ctx})
return types.GameStatus(status), err return gameTypes.GameStatus(status), err
} }
// GetClaimCount returns the number of claims in the game. // GetClaimCount returns the number of claims in the game.
...@@ -138,16 +139,15 @@ func (l *loader) FetchClaims(ctx context.Context) ([]types.Claim, error) { ...@@ -138,16 +139,15 @@ func (l *loader) FetchClaims(ctx context.Context) ([]types.Claim, error) {
} }
// FetchAbsolutePrestateHash fetches the hashed absolute prestate from the fault dispute game. // FetchAbsolutePrestateHash fetches the hashed absolute prestate from the fault dispute game.
func (l *loader) FetchAbsolutePrestateHash(ctx context.Context) ([]byte, error) { func (l *loader) FetchAbsolutePrestateHash(ctx context.Context) (common.Hash, error) {
callOpts := bind.CallOpts{ callOpts := bind.CallOpts{
Context: ctx, Context: ctx,
} }
absolutePrestate, err := l.caller.ABSOLUTEPRESTATE(&callOpts) absolutePrestate, err := l.caller.ABSOLUTEPRESTATE(&callOpts)
if err != nil { if err != nil {
return nil, err return common.Hash{}, err
} }
returnValue := absolutePrestate[:]
return returnValue, nil return absolutePrestate, nil
} }
...@@ -7,6 +7,7 @@ import ( ...@@ -7,6 +7,7 @@ import (
"testing" "testing"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
...@@ -30,15 +31,15 @@ func TestLoader_GetGameStatus(t *testing.T) { ...@@ -30,15 +31,15 @@ func TestLoader_GetGameStatus(t *testing.T) {
}{ }{
{ {
name: "challenger won status", name: "challenger won status",
status: uint8(types.GameStatusChallengerWon), status: uint8(gameTypes.GameStatusChallengerWon),
}, },
{ {
name: "defender won status", name: "defender won status",
status: uint8(types.GameStatusDefenderWon), status: uint8(gameTypes.GameStatusDefenderWon),
}, },
{ {
name: "in progress status", name: "in progress status",
status: uint8(types.GameStatusInProgress), status: uint8(gameTypes.GameStatusInProgress),
}, },
{ {
name: "error bubbled up", name: "error bubbled up",
...@@ -57,7 +58,7 @@ func TestLoader_GetGameStatus(t *testing.T) { ...@@ -57,7 +58,7 @@ func TestLoader_GetGameStatus(t *testing.T) {
require.ErrorIs(t, err, mockStatusError) require.ErrorIs(t, err, mockStatusError)
} else { } else {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, types.GameStatus(test.status), status) require.Equal(t, gameTypes.GameStatus(test.status), status)
} }
}) })
} }
......
...@@ -11,18 +11,18 @@ import ( ...@@ -11,18 +11,18 @@ import (
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/alphabet" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/alphabet"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/cannon" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/cannon"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum-optimism/optimism/op-challenger/metrics" "github.com/ethereum-optimism/optimism/op-challenger/metrics"
"github.com/ethereum-optimism/optimism/op-service/txmgr" "github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
) )
type actor func(ctx context.Context) error type actor func(ctx context.Context) error
type GameInfo interface { type GameInfo interface {
GetGameStatus(context.Context) (types.GameStatus, error) GetGameStatus(context.Context) (gameTypes.GameStatus, error)
GetClaimCount(context.Context) (uint64, error) GetClaimCount(context.Context) (uint64, error)
} }
...@@ -31,8 +31,7 @@ type GamePlayer struct { ...@@ -31,8 +31,7 @@ type GamePlayer struct {
agreeWithProposedOutput bool agreeWithProposedOutput bool
loader GameInfo loader GameInfo
logger log.Logger logger log.Logger
status gameTypes.GameStatus
completed bool
} }
func NewGamePlayer( func NewGamePlayer(
...@@ -57,14 +56,14 @@ func NewGamePlayer( ...@@ -57,14 +56,14 @@ func NewGamePlayer(
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to fetch game status: %w", err) return nil, fmt.Errorf("failed to fetch game status: %w", err)
} }
if status != types.GameStatusInProgress { if status != gameTypes.GameStatusInProgress {
logger.Info("Game already resolved", "status", status) logger.Info("Game already resolved", "status", status)
// Game is already complete so skip creating the trace provider, loading game inputs etc. // Game is already complete so skip creating the trace provider, loading game inputs etc.
return &GamePlayer{ return &GamePlayer{
logger: logger, logger: logger,
loader: loader, loader: loader,
agreeWithProposedOutput: cfg.AgreeWithProposedOutput, agreeWithProposedOutput: cfg.AgreeWithProposedOutput,
completed: true, status: status,
// Act function does nothing because the game is already complete // Act function does nothing because the game is already complete
act: func(ctx context.Context) error { act: func(ctx context.Context) error {
return nil return nil
...@@ -111,32 +110,32 @@ func NewGamePlayer( ...@@ -111,32 +110,32 @@ func NewGamePlayer(
agreeWithProposedOutput: cfg.AgreeWithProposedOutput, agreeWithProposedOutput: cfg.AgreeWithProposedOutput,
loader: loader, loader: loader,
logger: logger, logger: logger,
completed: status != types.GameStatusInProgress, status: status,
}, nil }, nil
} }
func (g *GamePlayer) ProgressGame(ctx context.Context) bool { func (g *GamePlayer) ProgressGame(ctx context.Context) gameTypes.GameStatus {
if g.completed { if g.status != gameTypes.GameStatusInProgress {
// Game is already complete so don't try to perform further actions. // Game is already complete so don't try to perform further actions.
g.logger.Trace("Skipping completed game") g.logger.Trace("Skipping completed game")
return true return g.status
} }
g.logger.Trace("Checking if actions are required") g.logger.Trace("Checking if actions are required")
if err := g.act(ctx); err != nil { if err := g.act(ctx); err != nil {
g.logger.Error("Error when acting on game", "err", err) g.logger.Error("Error when acting on game", "err", err)
} }
if status, err := g.loader.GetGameStatus(ctx); err != nil { status, err := g.loader.GetGameStatus(ctx)
if err != nil {
g.logger.Warn("Unable to retrieve game status", "err", err) g.logger.Warn("Unable to retrieve game status", "err", err)
} else { return gameTypes.GameStatusInProgress
g.logGameStatus(ctx, status)
g.completed = status != types.GameStatusInProgress
return g.completed
} }
return false g.logGameStatus(ctx, status)
g.status = status
return status
} }
func (g *GamePlayer) logGameStatus(ctx context.Context, status types.GameStatus) { func (g *GamePlayer) logGameStatus(ctx context.Context, status gameTypes.GameStatus) {
if status == types.GameStatusInProgress { if status == gameTypes.GameStatusInProgress {
claimCount, err := g.loader.GetClaimCount(ctx) claimCount, err := g.loader.GetClaimCount(ctx)
if err != nil { if err != nil {
g.logger.Error("Failed to get claim count for in progress game", "err", err) g.logger.Error("Failed to get claim count for in progress game", "err", err)
...@@ -145,11 +144,11 @@ func (g *GamePlayer) logGameStatus(ctx context.Context, status types.GameStatus) ...@@ -145,11 +144,11 @@ func (g *GamePlayer) logGameStatus(ctx context.Context, status types.GameStatus)
g.logger.Info("Game info", "claims", claimCount, "status", status) g.logger.Info("Game info", "claims", claimCount, "status", status)
return return
} }
var expectedStatus types.GameStatus var expectedStatus gameTypes.GameStatus
if g.agreeWithProposedOutput { if g.agreeWithProposedOutput {
expectedStatus = types.GameStatusChallengerWon expectedStatus = gameTypes.GameStatusChallengerWon
} else { } else {
expectedStatus = types.GameStatusDefenderWon expectedStatus = gameTypes.GameStatusDefenderWon
} }
if expectedStatus == status { if expectedStatus == status {
g.logger.Info("Game won", "status", status) g.logger.Info("Game won", "status", status)
...@@ -159,22 +158,21 @@ func (g *GamePlayer) logGameStatus(ctx context.Context, status types.GameStatus) ...@@ -159,22 +158,21 @@ func (g *GamePlayer) logGameStatus(ctx context.Context, status types.GameStatus)
} }
type PrestateLoader interface { type PrestateLoader interface {
FetchAbsolutePrestateHash(ctx context.Context) ([]byte, error) FetchAbsolutePrestateHash(ctx context.Context) (common.Hash, error)
} }
// ValidateAbsolutePrestate validates the absolute prestate of the fault game. // ValidateAbsolutePrestate validates the absolute prestate of the fault game.
func ValidateAbsolutePrestate(ctx context.Context, trace types.TraceProvider, loader PrestateLoader) error { func ValidateAbsolutePrestate(ctx context.Context, trace types.TraceProvider, loader PrestateLoader) error {
providerPrestate, err := trace.AbsolutePreState(ctx) providerPrestateHash, err := trace.AbsolutePreStateCommitment(ctx)
if err != nil { if err != nil {
return fmt.Errorf("failed to get the trace provider's absolute prestate: %w", err) return fmt.Errorf("failed to get the trace provider's absolute prestate: %w", err)
} }
providerPrestateHash := crypto.Keccak256(providerPrestate)
onchainPrestate, err := loader.FetchAbsolutePrestateHash(ctx) onchainPrestate, err := loader.FetchAbsolutePrestateHash(ctx)
if err != nil { if err != nil {
return fmt.Errorf("failed to get the onchain absolute prestate: %w", err) return fmt.Errorf("failed to get the onchain absolute prestate: %w", err)
} }
if !bytes.Equal(providerPrestateHash, onchainPrestate) { if !bytes.Equal(providerPrestateHash[:], onchainPrestate[:]) {
return fmt.Errorf("trace provider's absolute prestate does not match onchain absolute prestate") return fmt.Errorf("trace provider's absolute prestate does not match onchain absolute prestate: Provider: %s | Chain %s", providerPrestateHash.Hex(), onchainPrestate.Hex())
} }
return nil return nil
} }
...@@ -6,7 +6,9 @@ import ( ...@@ -6,7 +6,9 @@ import (
"fmt" "fmt"
"testing" "testing"
"github.com/ethereum-optimism/optimism/cannon/mipsevm"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum-optimism/optimism/op-node/testlog" "github.com/ethereum-optimism/optimism/op-node/testlog"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
...@@ -22,8 +24,8 @@ var ( ...@@ -22,8 +24,8 @@ var (
func TestProgressGame_LogErrorFromAct(t *testing.T) { func TestProgressGame_LogErrorFromAct(t *testing.T) {
handler, game, actor := setupProgressGameTest(t, true) handler, game, actor := setupProgressGameTest(t, true)
actor.actErr = errors.New("boom") actor.actErr = errors.New("boom")
done := game.ProgressGame(context.Background()) status := game.ProgressGame(context.Background())
require.False(t, done, "should not be done") require.Equal(t, gameTypes.GameStatusInProgress, status)
require.Equal(t, 1, actor.callCount, "should perform next actions") require.Equal(t, 1, actor.callCount, "should perform next actions")
errLog := handler.FindLog(log.LvlError, "Error when acting on game") errLog := handler.FindLog(log.LvlError, "Error when acting on game")
require.NotNil(t, errLog, "should log error") require.NotNil(t, errLog, "should log error")
...@@ -38,42 +40,42 @@ func TestProgressGame_LogErrorFromAct(t *testing.T) { ...@@ -38,42 +40,42 @@ func TestProgressGame_LogErrorFromAct(t *testing.T) {
func TestProgressGame_LogGameStatus(t *testing.T) { func TestProgressGame_LogGameStatus(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
status types.GameStatus status gameTypes.GameStatus
agreeWithOutput bool agreeWithOutput bool
logLevel log.Lvl logLevel log.Lvl
logMsg string logMsg string
}{ }{
{ {
name: "GameLostAsDefender", name: "GameLostAsDefender",
status: types.GameStatusChallengerWon, status: gameTypes.GameStatusChallengerWon,
agreeWithOutput: false, agreeWithOutput: false,
logLevel: log.LvlError, logLevel: log.LvlError,
logMsg: "Game lost", logMsg: "Game lost",
}, },
{ {
name: "GameLostAsChallenger", name: "GameLostAsChallenger",
status: types.GameStatusDefenderWon, status: gameTypes.GameStatusDefenderWon,
agreeWithOutput: true, agreeWithOutput: true,
logLevel: log.LvlError, logLevel: log.LvlError,
logMsg: "Game lost", logMsg: "Game lost",
}, },
{ {
name: "GameWonAsDefender", name: "GameWonAsDefender",
status: types.GameStatusDefenderWon, status: gameTypes.GameStatusDefenderWon,
agreeWithOutput: false, agreeWithOutput: false,
logLevel: log.LvlInfo, logLevel: log.LvlInfo,
logMsg: "Game won", logMsg: "Game won",
}, },
{ {
name: "GameWonAsChallenger", name: "GameWonAsChallenger",
status: types.GameStatusChallengerWon, status: gameTypes.GameStatusChallengerWon,
agreeWithOutput: true, agreeWithOutput: true,
logLevel: log.LvlInfo, logLevel: log.LvlInfo,
logMsg: "Game won", logMsg: "Game won",
}, },
{ {
name: "GameInProgress", name: "GameInProgress",
status: types.GameStatusInProgress, status: gameTypes.GameStatusInProgress,
agreeWithOutput: true, agreeWithOutput: true,
logLevel: log.LvlInfo, logLevel: log.LvlInfo,
logMsg: "Game info", logMsg: "Game info",
...@@ -85,9 +87,9 @@ func TestProgressGame_LogGameStatus(t *testing.T) { ...@@ -85,9 +87,9 @@ func TestProgressGame_LogGameStatus(t *testing.T) {
handler, game, gameState := setupProgressGameTest(t, test.agreeWithOutput) handler, game, gameState := setupProgressGameTest(t, test.agreeWithOutput)
gameState.status = test.status gameState.status = test.status
done := game.ProgressGame(context.Background()) status := game.ProgressGame(context.Background())
require.Equal(t, 1, gameState.callCount, "should perform next actions") require.Equal(t, 1, gameState.callCount, "should perform next actions")
require.Equal(t, test.status != types.GameStatusInProgress, done, "should be done when not in progress") require.Equal(t, test.status, status)
errLog := handler.FindLog(test.logLevel, test.logMsg) errLog := handler.FindLog(test.logLevel, test.logMsg)
require.NotNil(t, errLog, "should log game result") require.NotNil(t, errLog, "should log game result")
require.Equal(t, test.status, errLog.GetContextValue("status")) require.Equal(t, test.status, errLog.GetContextValue("status"))
...@@ -96,19 +98,19 @@ func TestProgressGame_LogGameStatus(t *testing.T) { ...@@ -96,19 +98,19 @@ func TestProgressGame_LogGameStatus(t *testing.T) {
} }
func TestDoNotActOnCompleteGame(t *testing.T) { func TestDoNotActOnCompleteGame(t *testing.T) {
for _, status := range []types.GameStatus{types.GameStatusChallengerWon, types.GameStatusDefenderWon} { for _, status := range []gameTypes.GameStatus{gameTypes.GameStatusChallengerWon, gameTypes.GameStatusDefenderWon} {
t.Run(status.String(), func(t *testing.T) { t.Run(status.String(), func(t *testing.T) {
_, game, gameState := setupProgressGameTest(t, true) _, game, gameState := setupProgressGameTest(t, true)
gameState.status = status gameState.status = status
done := game.ProgressGame(context.Background()) fetched := game.ProgressGame(context.Background())
require.Equal(t, 1, gameState.callCount, "acts the first time") require.Equal(t, 1, gameState.callCount, "acts the first time")
require.True(t, done, "should be done") require.Equal(t, status, fetched)
// Should not act when it knows the game is already complete // Should not act when it knows the game is already complete
done = game.ProgressGame(context.Background()) fetched = game.ProgressGame(context.Background())
require.Equal(t, 1, gameState.callCount, "does not act after game is complete") require.Equal(t, 1, gameState.callCount, "does not act after game is complete")
require.True(t, done, "should still be done") require.Equal(t, status, fetched)
}) })
} }
} }
...@@ -119,8 +121,9 @@ func TestValidateAbsolutePrestate(t *testing.T) { ...@@ -119,8 +121,9 @@ func TestValidateAbsolutePrestate(t *testing.T) {
t.Run("ValidPrestates", func(t *testing.T) { t.Run("ValidPrestates", func(t *testing.T) {
prestate := []byte{0x00, 0x01, 0x02, 0x03} prestate := []byte{0x00, 0x01, 0x02, 0x03}
prestateHash := crypto.Keccak256(prestate) prestateHash := crypto.Keccak256(prestate)
prestateHash[0] = mipsevm.VMStatusUnfinished
mockTraceProvider := newMockTraceProvider(false, prestate) mockTraceProvider := newMockTraceProvider(false, prestate)
mockLoader := newMockPrestateLoader(false, prestateHash) mockLoader := newMockPrestateLoader(false, common.BytesToHash(prestateHash))
err := ValidateAbsolutePrestate(context.Background(), mockTraceProvider, mockLoader) err := ValidateAbsolutePrestate(context.Background(), mockTraceProvider, mockLoader)
require.NoError(t, err) require.NoError(t, err)
}) })
...@@ -128,7 +131,7 @@ func TestValidateAbsolutePrestate(t *testing.T) { ...@@ -128,7 +131,7 @@ func TestValidateAbsolutePrestate(t *testing.T) {
t.Run("TraceProviderErrors", func(t *testing.T) { t.Run("TraceProviderErrors", func(t *testing.T) {
prestate := []byte{0x00, 0x01, 0x02, 0x03} prestate := []byte{0x00, 0x01, 0x02, 0x03}
mockTraceProvider := newMockTraceProvider(true, prestate) mockTraceProvider := newMockTraceProvider(true, prestate)
mockLoader := newMockPrestateLoader(false, prestate) mockLoader := newMockPrestateLoader(false, common.BytesToHash(prestate))
err := ValidateAbsolutePrestate(context.Background(), mockTraceProvider, mockLoader) err := ValidateAbsolutePrestate(context.Background(), mockTraceProvider, mockLoader)
require.ErrorIs(t, err, mockTraceProviderError) require.ErrorIs(t, err, mockTraceProviderError)
}) })
...@@ -136,14 +139,14 @@ func TestValidateAbsolutePrestate(t *testing.T) { ...@@ -136,14 +139,14 @@ func TestValidateAbsolutePrestate(t *testing.T) {
t.Run("LoaderErrors", func(t *testing.T) { t.Run("LoaderErrors", func(t *testing.T) {
prestate := []byte{0x00, 0x01, 0x02, 0x03} prestate := []byte{0x00, 0x01, 0x02, 0x03}
mockTraceProvider := newMockTraceProvider(false, prestate) mockTraceProvider := newMockTraceProvider(false, prestate)
mockLoader := newMockPrestateLoader(true, prestate) mockLoader := newMockPrestateLoader(true, common.BytesToHash(prestate))
err := ValidateAbsolutePrestate(context.Background(), mockTraceProvider, mockLoader) err := ValidateAbsolutePrestate(context.Background(), mockTraceProvider, mockLoader)
require.ErrorIs(t, err, mockLoaderError) require.ErrorIs(t, err, mockLoaderError)
}) })
t.Run("PrestateMismatch", func(t *testing.T) { t.Run("PrestateMismatch", func(t *testing.T) {
mockTraceProvider := newMockTraceProvider(false, []byte{0x00, 0x01, 0x02, 0x03}) mockTraceProvider := newMockTraceProvider(false, []byte{0x00, 0x01, 0x02, 0x03})
mockLoader := newMockPrestateLoader(false, []byte{0x00}) mockLoader := newMockPrestateLoader(false, common.BytesToHash([]byte{0x00}))
err := ValidateAbsolutePrestate(context.Background(), mockTraceProvider, mockLoader) err := ValidateAbsolutePrestate(context.Background(), mockTraceProvider, mockLoader)
require.Error(t, err) require.Error(t, err)
}) })
...@@ -166,7 +169,7 @@ func setupProgressGameTest(t *testing.T, agreeWithProposedRoot bool) (*testlog.C ...@@ -166,7 +169,7 @@ func setupProgressGameTest(t *testing.T, agreeWithProposedRoot bool) (*testlog.C
} }
type stubGameState struct { type stubGameState struct {
status types.GameStatus status gameTypes.GameStatus
claimCount uint64 claimCount uint64
callCount int callCount int
actErr error actErr error
...@@ -178,7 +181,7 @@ func (s *stubGameState) Act(ctx context.Context) error { ...@@ -178,7 +181,7 @@ func (s *stubGameState) Act(ctx context.Context) error {
return s.actErr return s.actErr
} }
func (s *stubGameState) GetGameStatus(ctx context.Context) (types.GameStatus, error) { func (s *stubGameState) GetGameStatus(ctx context.Context) (gameTypes.GameStatus, error) {
return s.status, nil return s.status, nil
} }
...@@ -209,21 +212,31 @@ func (m *mockTraceProvider) AbsolutePreState(ctx context.Context) ([]byte, error ...@@ -209,21 +212,31 @@ func (m *mockTraceProvider) AbsolutePreState(ctx context.Context) ([]byte, error
} }
return m.prestate, nil return m.prestate, nil
} }
func (m *mockTraceProvider) AbsolutePreStateCommitment(ctx context.Context) (common.Hash, error) {
prestate, err := m.AbsolutePreState(ctx)
if err != nil {
return common.Hash{}, err
}
hash := common.BytesToHash(crypto.Keccak256(prestate))
hash[0] = mipsevm.VMStatusUnfinished
return hash, nil
}
type mockLoader struct { type mockLoader struct {
prestateError bool prestateError bool
prestate []byte prestate common.Hash
} }
func newMockPrestateLoader(prestateError bool, prestate []byte) *mockLoader { func newMockPrestateLoader(prestateError bool, prestate common.Hash) *mockLoader {
return &mockLoader{ return &mockLoader{
prestateError: prestateError, prestateError: prestateError,
prestate: prestate, prestate: prestate,
} }
} }
func (m *mockLoader) FetchAbsolutePrestateHash(ctx context.Context) ([]byte, error) { func (m *mockLoader) FetchAbsolutePrestateHash(ctx context.Context) (common.Hash, error) {
if m.prestateError { if m.prestateError {
return nil, mockLoaderError return common.Hash{}, mockLoaderError
} }
return m.prestate, nil return m.prestate, nil
} }
...@@ -6,6 +6,7 @@ import ( ...@@ -6,6 +6,7 @@ import (
"github.com/ethereum-optimism/optimism/op-bindings/bindings" "github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum-optimism/optimism/op-service/txmgr" "github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum"
...@@ -81,23 +82,23 @@ func (r *faultResponder) BuildTx(ctx context.Context, response types.Claim) ([]b ...@@ -81,23 +82,23 @@ func (r *faultResponder) BuildTx(ctx context.Context, response types.Claim) ([]b
// CallResolve determines if the resolve function on the fault dispute game contract // CallResolve determines if the resolve function on the fault dispute game contract
// would succeed. Returns the game status if the call would succeed, errors otherwise. // would succeed. Returns the game status if the call would succeed, errors otherwise.
func (r *faultResponder) CallResolve(ctx context.Context) (types.GameStatus, error) { func (r *faultResponder) CallResolve(ctx context.Context) (gameTypes.GameStatus, error) {
txData, err := r.buildResolveData() txData, err := r.buildResolveData()
if err != nil { if err != nil {
return types.GameStatusInProgress, err return gameTypes.GameStatusInProgress, err
} }
res, err := r.txMgr.Call(ctx, ethereum.CallMsg{ res, err := r.txMgr.Call(ctx, ethereum.CallMsg{
To: &r.fdgAddr, To: &r.fdgAddr,
Data: txData, Data: txData,
}, nil) }, nil)
if err != nil { if err != nil {
return types.GameStatusInProgress, err return gameTypes.GameStatusInProgress, err
} }
var status uint8 var status uint8
if err = r.fdgAbi.UnpackIntoInterface(&status, "resolve", res); err != nil { if err = r.fdgAbi.UnpackIntoInterface(&status, "resolve", res); err != nil {
return types.GameStatusInProgress, err return gameTypes.GameStatusInProgress, err
} }
return types.GameStatusFromUint8(status) return gameTypes.GameStatusFromUint8(status)
} }
// Resolve executes a resolve transaction to resolve a fault dispute game. // Resolve executes a resolve transaction to resolve a fault dispute game.
......
...@@ -8,6 +8,7 @@ import ( ...@@ -8,6 +8,7 @@ import (
"github.com/ethereum-optimism/optimism/op-bindings/bindings" "github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum-optimism/optimism/op-node/testlog" "github.com/ethereum-optimism/optimism/op-node/testlog"
"github.com/ethereum-optimism/optimism/op-service/txmgr" "github.com/ethereum-optimism/optimism/op-service/txmgr"
...@@ -32,7 +33,7 @@ func TestCallResolve(t *testing.T) { ...@@ -32,7 +33,7 @@ func TestCallResolve(t *testing.T) {
mockTxMgr.callFails = true mockTxMgr.callFails = true
status, err := responder.CallResolve(context.Background()) status, err := responder.CallResolve(context.Background())
require.ErrorIs(t, err, mockCallError) require.ErrorIs(t, err, mockCallError)
require.Equal(t, types.GameStatusInProgress, status) require.Equal(t, gameTypes.GameStatusInProgress, status)
require.Equal(t, 0, mockTxMgr.calls) require.Equal(t, 0, mockTxMgr.calls)
}) })
...@@ -41,7 +42,7 @@ func TestCallResolve(t *testing.T) { ...@@ -41,7 +42,7 @@ func TestCallResolve(t *testing.T) {
mockTxMgr.callBytes = []byte{0x00, 0x01} mockTxMgr.callBytes = []byte{0x00, 0x01}
status, err := responder.CallResolve(context.Background()) status, err := responder.CallResolve(context.Background())
require.Error(t, err) require.Error(t, err)
require.Equal(t, types.GameStatusInProgress, status) require.Equal(t, gameTypes.GameStatusInProgress, status)
require.Equal(t, 1, mockTxMgr.calls) require.Equal(t, 1, mockTxMgr.calls)
}) })
...@@ -49,7 +50,7 @@ func TestCallResolve(t *testing.T) { ...@@ -49,7 +50,7 @@ func TestCallResolve(t *testing.T) {
responder, mockTxMgr := newTestFaultResponder(t) responder, mockTxMgr := newTestFaultResponder(t)
status, err := responder.CallResolve(context.Background()) status, err := responder.CallResolve(context.Background())
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, types.GameStatusInProgress, status) require.Equal(t, gameTypes.GameStatusInProgress, status)
require.Equal(t, 1, mockTxMgr.calls) require.Equal(t, 1, mockTxMgr.calls)
}) })
} }
......
package solver package solver
import ( import (
"bytes"
"context" "context"
"errors" "errors"
"fmt" "fmt"
...@@ -132,7 +133,7 @@ func (s *Solver) defend(ctx context.Context, claim types.Claim) (*types.Claim, e ...@@ -132,7 +133,7 @@ func (s *Solver) defend(ctx context.Context, claim types.Claim) (*types.Claim, e
// agreeWithClaim returns true if the claim is correct according to the internal [TraceProvider]. // agreeWithClaim returns true if the claim is correct according to the internal [TraceProvider].
func (s *Solver) agreeWithClaim(ctx context.Context, claim types.ClaimData) (bool, error) { func (s *Solver) agreeWithClaim(ctx context.Context, claim types.ClaimData) (bool, error) {
ourValue, err := s.traceAtPosition(ctx, claim.Position) ourValue, err := s.traceAtPosition(ctx, claim.Position)
return ourValue == claim.Value, err return bytes.Equal(ourValue[:], claim.Value[:]), err
} }
// traceAtPosition returns the [common.Hash] from internal [TraceProvider] at the given [Position]. // traceAtPosition returns the [common.Hash] from internal [TraceProvider] at the given [Position].
......
...@@ -6,6 +6,7 @@ import ( ...@@ -6,6 +6,7 @@ import (
"math/big" "math/big"
"strings" "strings"
"github.com/ethereum-optimism/optimism/cannon/mipsevm"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
...@@ -58,7 +59,7 @@ func (ap *AlphabetTraceProvider) Get(ctx context.Context, i uint64) (common.Hash ...@@ -58,7 +59,7 @@ func (ap *AlphabetTraceProvider) Get(ctx context.Context, i uint64) (common.Hash
if err != nil { if err != nil {
return common.Hash{}, err return common.Hash{}, err
} }
return crypto.Keccak256Hash(claimBytes), nil return alphabetStateHash(claimBytes), nil
} }
// AbsolutePreState returns the absolute pre-state for the alphabet trace. // AbsolutePreState returns the absolute pre-state for the alphabet trace.
...@@ -66,11 +67,27 @@ func (ap *AlphabetTraceProvider) AbsolutePreState(ctx context.Context) ([]byte, ...@@ -66,11 +67,27 @@ func (ap *AlphabetTraceProvider) AbsolutePreState(ctx context.Context) ([]byte,
return common.Hex2Bytes("0000000000000000000000000000000000000000000000000000000000000060"), nil return common.Hex2Bytes("0000000000000000000000000000000000000000000000000000000000000060"), nil
} }
func (ap *AlphabetTraceProvider) AbsolutePreStateCommitment(ctx context.Context) (common.Hash, error) {
prestate, err := ap.AbsolutePreState(ctx)
if err != nil {
return common.Hash{}, err
}
hash := common.BytesToHash(crypto.Keccak256(prestate))
hash[0] = mipsevm.VMStatusUnfinished
return hash, nil
}
// BuildAlphabetPreimage constructs the claim bytes for the index and state item. // BuildAlphabetPreimage constructs the claim bytes for the index and state item.
func BuildAlphabetPreimage(i uint64, letter string) []byte { func BuildAlphabetPreimage(i uint64, letter string) []byte {
return append(IndexToBytes(i), LetterToBytes(letter)...) return append(IndexToBytes(i), LetterToBytes(letter)...)
} }
func alphabetStateHash(state []byte) common.Hash {
h := crypto.Keccak256Hash(state)
h[0] = mipsevm.VMStatusInvalid
return h
}
// IndexToBytes converts an index to a byte slice big endian // IndexToBytes converts an index to a byte slice big endian
func IndexToBytes(i uint64) []byte { func IndexToBytes(i uint64) []byte {
big := new(big.Int) big := new(big.Int)
......
...@@ -6,12 +6,11 @@ import ( ...@@ -6,12 +6,11 @@ import (
"testing" "testing"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func alphabetClaim(index uint64, letter string) common.Hash { func alphabetClaim(index uint64, letter string) common.Hash {
return crypto.Keccak256Hash(BuildAlphabetPreimage(index, letter)) return alphabetStateHash(BuildAlphabetPreimage(index, letter))
} }
// TestAlphabetProvider_Get_ClaimsByTraceIndex tests the [fault.AlphabetProvider] Get function. // TestAlphabetProvider_Get_ClaimsByTraceIndex tests the [fault.AlphabetProvider] Get function.
...@@ -60,7 +59,7 @@ func FuzzIndexToBytes(f *testing.F) { ...@@ -60,7 +59,7 @@ func FuzzIndexToBytes(f *testing.F) {
// returns the correct pre-image for a index. // returns the correct pre-image for a index.
func TestGetStepData_Succeeds(t *testing.T) { func TestGetStepData_Succeeds(t *testing.T) {
ap := NewTraceProvider("abc", 2) ap := NewTraceProvider("abc", 2)
expected := BuildAlphabetPreimage(0, "a'") expected := BuildAlphabetPreimage(0, "a")
retrieved, proof, data, err := ap.GetStepData(context.Background(), uint64(1)) retrieved, proof, data, err := ap.GetStepData(context.Background(), uint64(1))
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, expected, retrieved) require.Equal(t, expected, retrieved)
......
...@@ -15,9 +15,10 @@ import ( ...@@ -15,9 +15,10 @@ import (
"github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum-optimism/optimism/cannon/mipsevm"
) )
const ( const (
...@@ -25,7 +26,7 @@ const ( ...@@ -25,7 +26,7 @@ const (
) )
type proofData struct { type proofData struct {
ClaimValue hexutil.Bytes `json:"post"` ClaimValue common.Hash `json:"post"`
StateData hexutil.Bytes `json:"state-data"` StateData hexutil.Bytes `json:"state-data"`
ProofData hexutil.Bytes `json:"proof-data"` ProofData hexutil.Bytes `json:"proof-data"`
OracleKey hexutil.Bytes `json:"oracle-key,omitempty"` OracleKey hexutil.Bytes `json:"oracle-key,omitempty"`
...@@ -86,7 +87,7 @@ func (p *CannonTraceProvider) Get(ctx context.Context, i uint64) (common.Hash, e ...@@ -86,7 +87,7 @@ func (p *CannonTraceProvider) Get(ctx context.Context, i uint64) (common.Hash, e
if err != nil { if err != nil {
return common.Hash{}, err return common.Hash{}, err
} }
value := common.BytesToHash(proof.ClaimValue) value := proof.ClaimValue
if value == (common.Hash{}) { if value == (common.Hash{}) {
return common.Hash{}, errors.New("proof missing post hash") return common.Hash{}, errors.New("proof missing post hash")
...@@ -122,6 +123,18 @@ func (p *CannonTraceProvider) AbsolutePreState(ctx context.Context) ([]byte, err ...@@ -122,6 +123,18 @@ func (p *CannonTraceProvider) AbsolutePreState(ctx context.Context) ([]byte, err
return state.EncodeWitness(), nil return state.EncodeWitness(), nil
} }
func (p *CannonTraceProvider) AbsolutePreStateCommitment(ctx context.Context) (common.Hash, error) {
state, err := p.AbsolutePreState(ctx)
if err != nil {
return common.Hash{}, fmt.Errorf("cannot load absolute pre-state: %w", err)
}
hash, err := mipsevm.StateWitness(state).StateHash()
if err != nil {
return common.Hash{}, fmt.Errorf("cannot hash absolute pre-state: %w", err)
}
return hash, nil
}
// loadProof will attempt to load or generate the proof data at the specified index // loadProof will attempt to load or generate the proof data at the specified index
// If the requested index is beyond the end of the actual trace it is extended with no-op instructions. // If the requested index is beyond the end of the actual trace it is extended with no-op instructions.
func (p *CannonTraceProvider) loadProof(ctx context.Context, i uint64) (*proofData, error) { func (p *CannonTraceProvider) loadProof(ctx context.Context, i uint64) (*proofData, error) {
...@@ -151,9 +164,13 @@ func (p *CannonTraceProvider) loadProof(ctx context.Context, i uint64) (*proofDa ...@@ -151,9 +164,13 @@ func (p *CannonTraceProvider) loadProof(ctx context.Context, i uint64) (*proofDa
// Extend the trace out to the full length using a no-op instruction that doesn't change any state // Extend the trace out to the full length using a no-op instruction that doesn't change any state
// No execution is done, so no proof-data or oracle values are required. // No execution is done, so no proof-data or oracle values are required.
witness := state.EncodeWitness() witness := state.EncodeWitness()
witnessHash, err := mipsevm.StateWitness(witness).StateHash()
if err != nil {
return nil, fmt.Errorf("cannot hash witness: %w", err)
}
proof := &proofData{ proof := &proofData{
ClaimValue: crypto.Keccak256(witness), ClaimValue: witnessHash,
StateData: witness, StateData: hexutil.Bytes(witness),
ProofData: []byte{}, ProofData: []byte{},
OracleKey: nil, OracleKey: nil,
OracleValue: nil, OracleValue: nil,
......
...@@ -15,7 +15,6 @@ import ( ...@@ -15,7 +15,6 @@ import (
"github.com/ethereum-optimism/optimism/op-node/testlog" "github.com/ethereum-optimism/optimism/op-node/testlog"
"github.com/ethereum-optimism/optimism/op-service/ioutil" "github.com/ethereum-optimism/optimism/op-service/ioutil"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
...@@ -43,7 +42,9 @@ func TestGet(t *testing.T) { ...@@ -43,7 +42,9 @@ func TestGet(t *testing.T) {
value, err := provider.Get(context.Background(), 7000) value, err := provider.Get(context.Background(), 7000)
require.NoError(t, err) require.NoError(t, err)
require.Contains(t, generator.generated, 7000, "should have tried to generate the proof") require.Contains(t, generator.generated, 7000, "should have tried to generate the proof")
require.Equal(t, crypto.Keccak256Hash(generator.finalState.EncodeWitness()), value) stateHash, err := generator.finalState.EncodeWitness().StateHash()
require.NoError(t, err)
require.Equal(t, stateHash, value)
}) })
t.Run("MissingPostHash", func(t *testing.T) { t.Run("MissingPostHash", func(t *testing.T) {
...@@ -86,7 +87,7 @@ func TestGetStepData(t *testing.T) { ...@@ -86,7 +87,7 @@ func TestGetStepData(t *testing.T) {
Exited: true, Exited: true,
} }
generator.proof = &proofData{ generator.proof = &proofData{
ClaimValue: common.Hash{0xaa}.Bytes(), ClaimValue: common.Hash{0xaa},
StateData: []byte{0xbb}, StateData: []byte{0xbb},
ProofData: []byte{0xcc}, ProofData: []byte{0xcc},
OracleKey: common.Hash{0xdd}.Bytes(), OracleKey: common.Hash{0xdd}.Bytes(),
...@@ -111,7 +112,7 @@ func TestGetStepData(t *testing.T) { ...@@ -111,7 +112,7 @@ func TestGetStepData(t *testing.T) {
Exited: true, Exited: true,
} }
generator.proof = &proofData{ generator.proof = &proofData{
ClaimValue: common.Hash{0xaa}.Bytes(), ClaimValue: common.Hash{0xaa},
StateData: []byte{0xbb}, StateData: []byte{0xbb},
ProofData: []byte{0xcc}, ProofData: []byte{0xcc},
OracleKey: common.Hash{0xdd}.Bytes(), OracleKey: common.Hash{0xdd}.Bytes(),
...@@ -185,7 +186,7 @@ func TestAbsolutePreState(t *testing.T) { ...@@ -185,7 +186,7 @@ func TestAbsolutePreState(t *testing.T) {
Step: 0, Step: 0,
Registers: [32]uint32{}, Registers: [32]uint32{},
} }
require.Equal(t, state.EncodeWitness(), preState) require.Equal(t, []byte(state.EncodeWitness()), preState)
}) })
} }
......
...@@ -3,7 +3,6 @@ package types ...@@ -3,7 +3,6 @@ package types
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"math/big" "math/big"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
...@@ -13,36 +12,6 @@ var ( ...@@ -13,36 +12,6 @@ var (
ErrGameDepthReached = errors.New("game depth reached") ErrGameDepthReached = errors.New("game depth reached")
) )
type GameStatus uint8
const (
GameStatusInProgress GameStatus = iota
GameStatusChallengerWon
GameStatusDefenderWon
)
// String returns the string representation of the game status.
func (s GameStatus) String() string {
switch s {
case GameStatusInProgress:
return "In Progress"
case GameStatusChallengerWon:
return "Challenger Won"
case GameStatusDefenderWon:
return "Defender Won"
default:
return "Unknown"
}
}
// GameStatusFromUint8 returns a game status from the uint8 representation.
func GameStatusFromUint8(i uint8) (GameStatus, error) {
if i > 2 {
return GameStatus(i), fmt.Errorf("invalid game status: %d", i)
}
return GameStatus(i), nil
}
// PreimageOracleData encapsulates the preimage oracle data // PreimageOracleData encapsulates the preimage oracle data
// to load into the onchain oracle. // to load into the onchain oracle.
type PreimageOracleData struct { type PreimageOracleData struct {
...@@ -105,6 +74,9 @@ type TraceProvider interface { ...@@ -105,6 +74,9 @@ type TraceProvider interface {
// AbsolutePreState is the pre-image value of the trace that transitions to the trace value at index 0 // AbsolutePreState is the pre-image value of the trace that transitions to the trace value at index 0
AbsolutePreState(ctx context.Context) (preimage []byte, err error) AbsolutePreState(ctx context.Context) (preimage []byte, err error)
// AbsolutePreStateCommitment is the commitment of the pre-image value of the trace that transitions to the trace value at index 0
AbsolutePreStateCommitment(ctx context.Context) (hash common.Hash, err error)
} }
// ClaimData is the core of a claim. It must be unique inside a specific game. // ClaimData is the core of a claim. It must be unique inside a specific game.
......
package types package types
import ( import (
"fmt"
"testing" "testing"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
var validGameStatuses = []GameStatus{
GameStatusInProgress,
GameStatusChallengerWon,
GameStatusDefenderWon,
}
func TestGameStatusFromUint8(t *testing.T) {
for _, status := range validGameStatuses {
t.Run(fmt.Sprintf("Valid Game Status %v", status), func(t *testing.T) {
parsed, err := GameStatusFromUint8(uint8(status))
require.NoError(t, err)
require.Equal(t, status, parsed)
})
}
t.Run("Invalid", func(t *testing.T) {
status, err := GameStatusFromUint8(3)
require.Error(t, err)
require.Equal(t, GameStatus(3), status)
})
}
func TestNewPreimageOracleData(t *testing.T) { func TestNewPreimageOracleData(t *testing.T) {
t.Run("LocalData", func(t *testing.T) { t.Run("LocalData", func(t *testing.T) {
data := NewPreimageOracleData([]byte{1, 2, 3}, []byte{4, 5, 6}, 7) data := NewPreimageOracleData([]byte{1, 2, 3}, []byte{4, 5, 6}, 7)
......
...@@ -8,7 +8,6 @@ import ( ...@@ -8,7 +8,6 @@ import (
"time" "time"
"github.com/ethereum-optimism/optimism/op-challenger/game/scheduler" "github.com/ethereum-optimism/optimism/op-challenger/game/scheduler"
"github.com/ethereum-optimism/optimism/op-challenger/metrics"
"github.com/ethereum-optimism/optimism/op-service/clock" "github.com/ethereum-optimism/optimism/op-service/clock"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
...@@ -27,7 +26,6 @@ type gameScheduler interface { ...@@ -27,7 +26,6 @@ type gameScheduler interface {
type gameMonitor struct { type gameMonitor struct {
logger log.Logger logger log.Logger
metrics metrics.Metricer
clock clock.Clock clock clock.Clock
source gameSource source gameSource
scheduler gameScheduler scheduler gameScheduler
...@@ -38,7 +36,6 @@ type gameMonitor struct { ...@@ -38,7 +36,6 @@ type gameMonitor struct {
func newGameMonitor( func newGameMonitor(
logger log.Logger, logger log.Logger,
m metrics.Metricer,
cl clock.Clock, cl clock.Clock,
source gameSource, source gameSource,
scheduler gameScheduler, scheduler gameScheduler,
...@@ -48,7 +45,6 @@ func newGameMonitor( ...@@ -48,7 +45,6 @@ func newGameMonitor(
) *gameMonitor { ) *gameMonitor {
return &gameMonitor{ return &gameMonitor{
logger: logger, logger: logger,
metrics: m,
clock: cl, clock: cl,
scheduler: scheduler, scheduler: scheduler,
source: source, source: source,
......
...@@ -6,7 +6,6 @@ import ( ...@@ -6,7 +6,6 @@ import (
"testing" "testing"
"time" "time"
"github.com/ethereum-optimism/optimism/op-challenger/metrics"
"github.com/ethereum-optimism/optimism/op-node/testlog" "github.com/ethereum-optimism/optimism/op-node/testlog"
"github.com/ethereum-optimism/optimism/op-service/clock" "github.com/ethereum-optimism/optimism/op-service/clock"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
...@@ -101,7 +100,7 @@ func setupMonitorTest(t *testing.T, allowedGames []common.Address) (*gameMonitor ...@@ -101,7 +100,7 @@ func setupMonitorTest(t *testing.T, allowedGames []common.Address) (*gameMonitor
return i, nil return i, nil
} }
sched := &stubScheduler{} sched := &stubScheduler{}
monitor := newGameMonitor(logger, metrics.NoopMetrics, clock.SystemClock, source, sched, time.Duration(0), fetchBlockNum, allowedGames) monitor := newGameMonitor(logger, clock.SystemClock, source, sched, time.Duration(0), fetchBlockNum, allowedGames)
return monitor, source, sched return monitor, source, sched
} }
......
...@@ -5,6 +5,8 @@ import ( ...@@ -5,6 +5,8 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
...@@ -17,7 +19,7 @@ type PlayerCreator func(address common.Address, dir string) (GamePlayer, error) ...@@ -17,7 +19,7 @@ type PlayerCreator func(address common.Address, dir string) (GamePlayer, error)
type gameState struct { type gameState struct {
player GamePlayer player GamePlayer
inflight bool inflight bool
resolved bool status types.GameStatus
} }
// coordinator manages the set of current games, queues games to be played (on separate worker threads) and // coordinator manages the set of current games, queues games to be played (on separate worker threads) and
...@@ -31,6 +33,7 @@ type coordinator struct { ...@@ -31,6 +33,7 @@ type coordinator struct {
resultQueue <-chan job resultQueue <-chan job
logger log.Logger logger log.Logger
m SchedulerMetricer
createPlayer PlayerCreator createPlayer PlayerCreator
states map[common.Address]*gameState states map[common.Address]*gameState
disk DiskManager disk DiskManager
...@@ -49,18 +52,36 @@ func (c *coordinator) schedule(ctx context.Context, games []common.Address) erro ...@@ -49,18 +52,36 @@ func (c *coordinator) schedule(ctx context.Context, games []common.Address) erro
} }
} }
var gamesInProgress int
var gamesChallengerWon int
var gamesDefenderWon int
var errs []error var errs []error
var jobs []job
// Next collect all the jobs to schedule and ensure all games are recorded in the states map. // Next collect all the jobs to schedule and ensure all games are recorded in the states map.
// Otherwise, results may start being processed before all games are recorded, resulting in existing // Otherwise, results may start being processed before all games are recorded, resulting in existing
// data directories potentially being deleted for games that are required. // data directories potentially being deleted for games that are required.
var jobs []job
for _, addr := range games { for _, addr := range games {
if j, err := c.createJob(addr); err != nil { if j, err := c.createJob(addr); err != nil {
errs = append(errs, err) errs = append(errs, err)
} else if j != nil { } else if j != nil {
jobs = append(jobs, *j) jobs = append(jobs, *j)
c.m.RecordGameUpdateScheduled()
}
state, ok := c.states[addr]
if ok {
switch state.status {
case types.GameStatusInProgress:
gamesInProgress++
case types.GameStatusDefenderWon:
gamesDefenderWon++
case types.GameStatusChallengerWon:
gamesChallengerWon++
}
} else {
c.logger.Warn("Game not found in states map", "game", addr)
} }
} }
c.m.RecordGamesStatus(gamesInProgress, gamesChallengerWon, gamesDefenderWon)
// Finally, enqueue the jobs // Finally, enqueue the jobs
for _, j := range jobs { for _, j := range jobs {
...@@ -114,15 +135,16 @@ func (c *coordinator) processResult(j job) error { ...@@ -114,15 +135,16 @@ func (c *coordinator) processResult(j job) error {
return fmt.Errorf("game %v received unexpected result: %w", j.addr, errUnknownGame) return fmt.Errorf("game %v received unexpected result: %w", j.addr, errUnknownGame)
} }
state.inflight = false state.inflight = false
state.resolved = j.resolved state.status = j.status
c.deleteResolvedGameFiles() c.deleteResolvedGameFiles()
c.m.RecordGameUpdateCompleted()
return nil return nil
} }
func (c *coordinator) deleteResolvedGameFiles() { func (c *coordinator) deleteResolvedGameFiles() {
var keepGames []common.Address var keepGames []common.Address
for addr, state := range c.states { for addr, state := range c.states {
if !state.resolved || state.inflight { if state.status == types.GameStatusInProgress || state.inflight {
keepGames = append(keepGames, addr) keepGames = append(keepGames, addr)
} }
} }
...@@ -131,9 +153,10 @@ func (c *coordinator) deleteResolvedGameFiles() { ...@@ -131,9 +153,10 @@ func (c *coordinator) deleteResolvedGameFiles() {
} }
} }
func newCoordinator(logger log.Logger, jobQueue chan<- job, resultQueue <-chan job, createPlayer PlayerCreator, disk DiskManager) *coordinator { func newCoordinator(logger log.Logger, m SchedulerMetricer, jobQueue chan<- job, resultQueue <-chan job, createPlayer PlayerCreator, disk DiskManager) *coordinator {
return &coordinator{ return &coordinator{
logger: logger, logger: logger,
m: m,
jobQueue: jobQueue, jobQueue: jobQueue,
resultQueue: resultQueue, resultQueue: resultQueue,
createPlayer: createPlayer, createPlayer: createPlayer,
......
...@@ -5,6 +5,8 @@ import ( ...@@ -5,6 +5,8 @@ import (
"fmt" "fmt"
"testing" "testing"
"github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum-optimism/optimism/op-challenger/metrics"
"github.com/ethereum-optimism/optimism/op-node/testlog" "github.com/ethereum-optimism/optimism/op-node/testlog"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
...@@ -140,7 +142,7 @@ func TestDeleteDataForResolvedGames(t *testing.T) { ...@@ -140,7 +142,7 @@ func TestDeleteDataForResolvedGames(t *testing.T) {
require.NoError(t, c.schedule(ctx, []common.Address{gameAddr3})) require.NoError(t, c.schedule(ctx, []common.Address{gameAddr3}))
require.Len(t, workQueue, 1) require.Len(t, workQueue, 1)
j := <-workQueue j := <-workQueue
j.resolved = true j.status = types.GameStatusDefenderWon
require.NoError(t, c.processResult(j)) require.NoError(t, c.processResult(j))
// But ensure its data directory is marked as existing // But ensure its data directory is marked as existing
disk.DirForGame(gameAddr3) disk.DirForGame(gameAddr3)
...@@ -155,7 +157,9 @@ func TestDeleteDataForResolvedGames(t *testing.T) { ...@@ -155,7 +157,9 @@ func TestDeleteDataForResolvedGames(t *testing.T) {
// Game 3 hasn't yet progressed (update is still in flight) // Game 3 hasn't yet progressed (update is still in flight)
for i := 0; i < len(gameAddrs)-1; i++ { for i := 0; i < len(gameAddrs)-1; i++ {
j := <-workQueue j := <-workQueue
j.resolved = j.addr == gameAddr2 if j.addr == gameAddr2 {
j.status = types.GameStatusDefenderWon
}
require.NoError(t, c.processResult(j)) require.NoError(t, c.processResult(j))
} }
...@@ -229,20 +233,20 @@ func setupCoordinatorTest(t *testing.T, bufferSize int) (*coordinator, <-chan jo ...@@ -229,20 +233,20 @@ func setupCoordinatorTest(t *testing.T, bufferSize int) (*coordinator, <-chan jo
created: make(map[common.Address]*stubGame), created: make(map[common.Address]*stubGame),
} }
disk := &stubDiskManager{gameDirExists: make(map[common.Address]bool)} disk := &stubDiskManager{gameDirExists: make(map[common.Address]bool)}
c := newCoordinator(logger, workQueue, resultQueue, games.CreateGame, disk) c := newCoordinator(logger, metrics.NoopMetrics, workQueue, resultQueue, games.CreateGame, disk)
return c, workQueue, resultQueue, games, disk return c, workQueue, resultQueue, games, disk
} }
type stubGame struct { type stubGame struct {
addr common.Address addr common.Address
progressCount int progressCount int
done bool status types.GameStatus
dir string dir string
} }
func (g *stubGame) ProgressGame(_ context.Context) bool { func (g *stubGame) ProgressGame(_ context.Context) types.GameStatus {
g.progressCount++ g.progressCount++
return g.done return g.status
} }
type createdGames struct { type createdGames struct {
...@@ -259,10 +263,14 @@ func (c *createdGames) CreateGame(addr common.Address, dir string) (GamePlayer, ...@@ -259,10 +263,14 @@ func (c *createdGames) CreateGame(addr common.Address, dir string) (GamePlayer,
if _, exists := c.created[addr]; exists { if _, exists := c.created[addr]; exists {
c.t.Fatalf("game %v already exists", addr) c.t.Fatalf("game %v already exists", addr)
} }
status := types.GameStatusInProgress
if addr == c.createCompleted {
status = types.GameStatusDefenderWon
}
game := &stubGame{ game := &stubGame{
addr: addr, addr: addr,
done: addr == c.createCompleted, status: status,
dir: dir, dir: dir,
} }
c.created[addr] = game c.created[addr] = game
return game, nil return game, nil
......
...@@ -11,6 +11,12 @@ import ( ...@@ -11,6 +11,12 @@ import (
var ErrBusy = errors.New("busy scheduling previous update") var ErrBusy = errors.New("busy scheduling previous update")
type SchedulerMetricer interface {
RecordGamesStatus(inProgress, defenderWon, challengerWon int)
RecordGameUpdateScheduled()
RecordGameUpdateCompleted()
}
type Scheduler struct { type Scheduler struct {
logger log.Logger logger log.Logger
coordinator *coordinator coordinator *coordinator
...@@ -22,7 +28,7 @@ type Scheduler struct { ...@@ -22,7 +28,7 @@ type Scheduler struct {
cancel func() cancel func()
} }
func NewScheduler(logger log.Logger, disk DiskManager, maxConcurrency uint, createPlayer PlayerCreator) *Scheduler { func NewScheduler(logger log.Logger, m SchedulerMetricer, disk DiskManager, maxConcurrency uint, createPlayer PlayerCreator) *Scheduler {
// Size job and results queues to be fairly small so backpressure is applied early // Size job and results queues to be fairly small so backpressure is applied early
// but with enough capacity to keep the workers busy // but with enough capacity to keep the workers busy
jobQueue := make(chan job, maxConcurrency*2) jobQueue := make(chan job, maxConcurrency*2)
...@@ -34,7 +40,7 @@ func NewScheduler(logger log.Logger, disk DiskManager, maxConcurrency uint, crea ...@@ -34,7 +40,7 @@ func NewScheduler(logger log.Logger, disk DiskManager, maxConcurrency uint, crea
return &Scheduler{ return &Scheduler{
logger: logger, logger: logger,
coordinator: newCoordinator(logger, jobQueue, resultQueue, createPlayer, disk), coordinator: newCoordinator(logger, m, jobQueue, resultQueue, createPlayer, disk),
maxConcurrency: maxConcurrency, maxConcurrency: maxConcurrency,
scheduleQueue: scheduleQueue, scheduleQueue: scheduleQueue,
jobQueue: jobQueue, jobQueue: jobQueue,
......
...@@ -4,6 +4,7 @@ import ( ...@@ -4,6 +4,7 @@ import (
"context" "context"
"testing" "testing"
"github.com/ethereum-optimism/optimism/op-challenger/metrics"
"github.com/ethereum-optimism/optimism/op-node/testlog" "github.com/ethereum-optimism/optimism/op-node/testlog"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
...@@ -18,7 +19,7 @@ func TestSchedulerProcessesGames(t *testing.T) { ...@@ -18,7 +19,7 @@ func TestSchedulerProcessesGames(t *testing.T) {
} }
removeExceptCalls := make(chan []common.Address) removeExceptCalls := make(chan []common.Address)
disk := &trackingDiskManager{removeExceptCalls: removeExceptCalls} disk := &trackingDiskManager{removeExceptCalls: removeExceptCalls}
s := NewScheduler(logger, disk, 2, createPlayer) s := NewScheduler(logger, metrics.NoopMetrics, disk, 2, createPlayer)
s.Start(ctx) s.Start(ctx)
gameAddr1 := common.Address{0xaa} gameAddr1 := common.Address{0xaa}
...@@ -46,7 +47,7 @@ func TestReturnBusyWhenScheduleQueueFull(t *testing.T) { ...@@ -46,7 +47,7 @@ func TestReturnBusyWhenScheduleQueueFull(t *testing.T) {
} }
removeExceptCalls := make(chan []common.Address) removeExceptCalls := make(chan []common.Address)
disk := &trackingDiskManager{removeExceptCalls: removeExceptCalls} disk := &trackingDiskManager{removeExceptCalls: removeExceptCalls}
s := NewScheduler(logger, disk, 2, createPlayer) s := NewScheduler(logger, metrics.NoopMetrics, disk, 2, createPlayer)
// Scheduler not started - first call fills the queue // Scheduler not started - first call fills the queue
require.NoError(t, s.Schedule([]common.Address{{0xaa}})) require.NoError(t, s.Schedule([]common.Address{{0xaa}}))
......
...@@ -4,10 +4,12 @@ import ( ...@@ -4,10 +4,12 @@ import (
"context" "context"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum-optimism/optimism/op-challenger/game/types"
) )
type GamePlayer interface { type GamePlayer interface {
ProgressGame(ctx context.Context) bool ProgressGame(ctx context.Context) types.GameStatus
} }
type DiskManager interface { type DiskManager interface {
...@@ -16,7 +18,7 @@ type DiskManager interface { ...@@ -16,7 +18,7 @@ type DiskManager interface {
} }
type job struct { type job struct {
addr common.Address addr common.Address
player GamePlayer player GamePlayer
resolved bool status types.GameStatus
} }
...@@ -15,7 +15,7 @@ func progressGames(ctx context.Context, in <-chan job, out chan<- job, wg *sync. ...@@ -15,7 +15,7 @@ func progressGames(ctx context.Context, in <-chan job, out chan<- job, wg *sync.
case <-ctx.Done(): case <-ctx.Done():
return return
case j := <-in: case j := <-in:
j.resolved = j.player.ProgressGame(ctx) j.status = j.player.ProgressGame(ctx)
out <- j out <- j
} }
} }
......
...@@ -6,6 +6,8 @@ import ( ...@@ -6,6 +6,8 @@ import (
"testing" "testing"
"time" "time"
"github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
...@@ -20,17 +22,17 @@ func TestWorkerShouldProcessJobsUntilContextDone(t *testing.T) { ...@@ -20,17 +22,17 @@ func TestWorkerShouldProcessJobsUntilContextDone(t *testing.T) {
go progressGames(ctx, in, out, &wg) go progressGames(ctx, in, out, &wg)
in <- job{ in <- job{
player: &stubPlayer{done: false}, player: &stubPlayer{status: types.GameStatusInProgress},
} }
in <- job{ in <- job{
player: &stubPlayer{done: true}, player: &stubPlayer{status: types.GameStatusDefenderWon},
} }
result1 := readWithTimeout(t, out) result1 := readWithTimeout(t, out)
result2 := readWithTimeout(t, out) result2 := readWithTimeout(t, out)
require.Equal(t, result1.resolved, false) require.Equal(t, result1.status, types.GameStatusInProgress)
require.Equal(t, result2.resolved, true) require.Equal(t, result2.status, types.GameStatusDefenderWon)
// Cancel the context which should exit the worker // Cancel the context which should exit the worker
cancel() cancel()
...@@ -38,11 +40,11 @@ func TestWorkerShouldProcessJobsUntilContextDone(t *testing.T) { ...@@ -38,11 +40,11 @@ func TestWorkerShouldProcessJobsUntilContextDone(t *testing.T) {
} }
type stubPlayer struct { type stubPlayer struct {
done bool status types.GameStatus
} }
func (s *stubPlayer) ProgressGame(ctx context.Context) bool { func (s *stubPlayer) ProgressGame(ctx context.Context) types.GameStatus {
return s.done return s.status
} }
func readWithTimeout[T any](t *testing.T, ch <-chan T) T { func readWithTimeout[T any](t *testing.T, ch <-chan T) T {
......
...@@ -69,13 +69,14 @@ func NewService(ctx context.Context, logger log.Logger, cfg *config.Config) (*Se ...@@ -69,13 +69,14 @@ func NewService(ctx context.Context, logger log.Logger, cfg *config.Config) (*Se
disk := newDiskManager(cfg.Datadir) disk := newDiskManager(cfg.Datadir)
sched := scheduler.NewScheduler( sched := scheduler.NewScheduler(
logger, logger,
m,
disk, disk,
cfg.MaxConcurrency, cfg.MaxConcurrency,
func(addr common.Address, dir string) (scheduler.GamePlayer, error) { func(addr common.Address, dir string) (scheduler.GamePlayer, error) {
return fault.NewGamePlayer(ctx, logger, m, cfg, dir, addr, txMgr, client) return fault.NewGamePlayer(ctx, logger, m, cfg, dir, addr, txMgr, client)
}) })
monitor := newGameMonitor(logger, m, cl, loader, sched, cfg.GameWindow, client.BlockNumber, cfg.GameAllowlist) monitor := newGameMonitor(logger, cl, loader, sched, cfg.GameWindow, client.BlockNumber, cfg.GameAllowlist)
m.RecordInfo(version.SimpleWithMeta) m.RecordInfo(version.SimpleWithMeta)
m.RecordUp() m.RecordUp()
......
package types
import (
"fmt"
)
type GameStatus uint8
const (
GameStatusInProgress GameStatus = iota
GameStatusChallengerWon
GameStatusDefenderWon
)
// String returns the string representation of the game status.
func (s GameStatus) String() string {
switch s {
case GameStatusInProgress:
return "In Progress"
case GameStatusChallengerWon:
return "Challenger Won"
case GameStatusDefenderWon:
return "Defender Won"
default:
return "Unknown"
}
}
// GameStatusFromUint8 returns a game status from the uint8 representation.
func GameStatusFromUint8(i uint8) (GameStatus, error) {
if i > 2 {
return GameStatus(i), fmt.Errorf("invalid game status: %d", i)
}
return GameStatus(i), nil
}
package types
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
)
var validGameStatuses = []GameStatus{
GameStatusInProgress,
GameStatusChallengerWon,
GameStatusDefenderWon,
}
func TestGameStatusFromUint8(t *testing.T) {
for _, status := range validGameStatuses {
t.Run(fmt.Sprintf("Valid Game Status %v", status), func(t *testing.T) {
parsed, err := GameStatusFromUint8(uint8(status))
require.NoError(t, err)
require.Equal(t, status, parsed)
})
}
t.Run("Invalid", func(t *testing.T) {
status, err := GameStatusFromUint8(3)
require.Error(t, err)
require.Equal(t, GameStatus(3), status)
})
}
...@@ -24,6 +24,11 @@ type Metricer interface { ...@@ -24,6 +24,11 @@ type Metricer interface {
RecordGameStep() RecordGameStep()
RecordGameMove() RecordGameMove()
RecordCannonExecutionTime(t float64) RecordCannonExecutionTime(t float64)
RecordGamesStatus(inProgress, defenderWon, challengerWon int)
RecordGameUpdateScheduled()
RecordGameUpdateCompleted()
} }
type Metrics struct { type Metrics struct {
...@@ -36,9 +41,13 @@ type Metrics struct { ...@@ -36,9 +41,13 @@ type Metrics struct {
info prometheus.GaugeVec info prometheus.GaugeVec
up prometheus.Gauge up prometheus.Gauge
moves prometheus.Counter moves prometheus.Counter
steps prometheus.Counter steps prometheus.Counter
cannonExecutionTime prometheus.Histogram cannonExecutionTime prometheus.Histogram
trackedGames prometheus.GaugeVec
inflightGames prometheus.Gauge
} }
var _ Metricer = (*Metrics)(nil) var _ Metricer = (*Metrics)(nil)
...@@ -80,7 +89,21 @@ func NewMetrics() *Metrics { ...@@ -80,7 +89,21 @@ func NewMetrics() *Metrics {
Namespace: Namespace, Namespace: Namespace,
Name: "cannon_execution_time", Name: "cannon_execution_time",
Help: "Time (in seconds) to execute cannon", Help: "Time (in seconds) to execute cannon",
Buckets: append([]float64{1.0, 10.0}, prometheus.ExponentialBuckets(30.0, 2.0, 14)...), Buckets: append(
[]float64{1.0, 10.0},
prometheus.ExponentialBuckets(30.0, 2.0, 14)...),
}),
trackedGames: *factory.NewGaugeVec(prometheus.GaugeOpts{
Namespace: Namespace,
Name: "tracked_games",
Help: "Number of games being tracked by the challenger",
}, []string{
"status",
}),
inflightGames: factory.NewGauge(prometheus.GaugeOpts{
Namespace: Namespace,
Name: "inflight_games",
Help: "Number of games being tracked by the challenger",
}), }),
} }
} }
...@@ -89,7 +112,12 @@ func (m *Metrics) Serve(ctx context.Context, host string, port int) error { ...@@ -89,7 +112,12 @@ func (m *Metrics) Serve(ctx context.Context, host string, port int) error {
return opmetrics.ListenAndServe(ctx, m.registry, host, port) return opmetrics.ListenAndServe(ctx, m.registry, host, port)
} }
func (m *Metrics) StartBalanceMetrics(ctx context.Context, l log.Logger, client *ethclient.Client, account common.Address) { func (m *Metrics) StartBalanceMetrics(
ctx context.Context,
l log.Logger,
client *ethclient.Client,
account common.Address,
) {
opmetrics.LaunchBalanceMetrics(ctx, l, m.registry, m.ns, client, account) opmetrics.LaunchBalanceMetrics(ctx, l, m.registry, m.ns, client, account)
} }
...@@ -120,3 +148,17 @@ func (m *Metrics) RecordGameStep() { ...@@ -120,3 +148,17 @@ func (m *Metrics) RecordGameStep() {
func (m *Metrics) RecordCannonExecutionTime(t float64) { func (m *Metrics) RecordCannonExecutionTime(t float64) {
m.cannonExecutionTime.Observe(t) m.cannonExecutionTime.Observe(t)
} }
func (m *Metrics) RecordGamesStatus(inProgress, defenderWon, challengerWon int) {
m.trackedGames.WithLabelValues("in_progress").Set(float64(inProgress))
m.trackedGames.WithLabelValues("defender_won").Set(float64(defenderWon))
m.trackedGames.WithLabelValues("challenger_won").Set(float64(challengerWon))
}
func (m *Metrics) RecordGameUpdateScheduled() {
m.inflightGames.Add(1)
}
func (m *Metrics) RecordGameUpdateCompleted() {
m.inflightGames.Sub(1)
}
...@@ -10,8 +10,15 @@ type noopMetrics struct { ...@@ -10,8 +10,15 @@ type noopMetrics struct {
var NoopMetrics Metricer = new(noopMetrics) var NoopMetrics Metricer = new(noopMetrics)
func (*noopMetrics) RecordInfo(version string) {} func (*noopMetrics) RecordInfo(version string) {}
func (*noopMetrics) RecordUp() {} func (*noopMetrics) RecordUp() {}
func (*noopMetrics) RecordGameMove() {}
func (*noopMetrics) RecordGameStep() {} func (*noopMetrics) RecordGameMove() {}
func (*noopMetrics) RecordGameStep() {}
func (*noopMetrics) RecordCannonExecutionTime(t float64) {} func (*noopMetrics) RecordCannonExecutionTime(t float64) {}
func (*noopMetrics) RecordGamesStatus(inProgress, defenderWon, challengerWon int) {}
func (*noopMetrics) RecordGameUpdateScheduled() {}
func (*noopMetrics) RecordGameUpdateCompleted() {}
...@@ -8,6 +8,7 @@ import ( ...@@ -8,6 +8,7 @@ import (
"github.com/ethereum-optimism/optimism/op-bindings/bindings" "github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys" "github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/geth"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive" "github.com/ethereum-optimism/optimism/op-node/rollup/derive"
"github.com/ethereum-optimism/optimism/op-node/testlog" "github.com/ethereum-optimism/optimism/op-node/testlog"
"github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/abi/bind"
...@@ -40,14 +41,14 @@ func TestERC20BridgeDeposits(t *testing.T) { ...@@ -40,14 +41,14 @@ func TestERC20BridgeDeposits(t *testing.T) {
// Deploy WETH9 // Deploy WETH9
weth9Address, tx, WETH9, err := bindings.DeployWETH9(opts, l1Client) weth9Address, tx, WETH9, err := bindings.DeployWETH9(opts, l1Client)
require.NoError(t, err) require.NoError(t, err)
_, err = waitForTransaction(tx.Hash(), l1Client, 3*time.Duration(cfg.DeployConfig.L1BlockTime)*time.Second) _, err = geth.WaitForTransaction(tx.Hash(), l1Client, 3*time.Duration(cfg.DeployConfig.L1BlockTime)*time.Second)
require.NoError(t, err, "Waiting for deposit tx on L1") require.NoError(t, err, "Waiting for deposit tx on L1")
// Get some WETH // Get some WETH
opts.Value = big.NewInt(params.Ether) opts.Value = big.NewInt(params.Ether)
tx, err = WETH9.Fallback(opts, []byte{}) tx, err = WETH9.Fallback(opts, []byte{})
require.NoError(t, err) require.NoError(t, err)
_, err = waitForTransaction(tx.Hash(), l1Client, 3*time.Duration(cfg.DeployConfig.L1BlockTime)*time.Second) _, err = geth.WaitForTransaction(tx.Hash(), l1Client, 3*time.Duration(cfg.DeployConfig.L1BlockTime)*time.Second)
require.NoError(t, err) require.NoError(t, err)
opts.Value = nil opts.Value = nil
wethBalance, err := WETH9.BalanceOf(&bind.CallOpts{}, opts.From) wethBalance, err := WETH9.BalanceOf(&bind.CallOpts{}, opts.From)
...@@ -61,7 +62,7 @@ func TestERC20BridgeDeposits(t *testing.T) { ...@@ -61,7 +62,7 @@ func TestERC20BridgeDeposits(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
tx, err = optimismMintableTokenFactory.CreateOptimismMintableERC20(l2Opts, weth9Address, "L2-WETH", "L2-WETH") tx, err = optimismMintableTokenFactory.CreateOptimismMintableERC20(l2Opts, weth9Address, "L2-WETH", "L2-WETH")
require.NoError(t, err) require.NoError(t, err)
_, err = waitForTransaction(tx.Hash(), l2Client, 3*time.Duration(cfg.DeployConfig.L2BlockTime)*time.Second) _, err = geth.WaitForTransaction(tx.Hash(), l2Client, 3*time.Duration(cfg.DeployConfig.L2BlockTime)*time.Second)
require.NoError(t, err) require.NoError(t, err)
// Get the deployment event to have access to the L2 WETH9 address // Get the deployment event to have access to the L2 WETH9 address
...@@ -76,7 +77,7 @@ func TestERC20BridgeDeposits(t *testing.T) { ...@@ -76,7 +77,7 @@ func TestERC20BridgeDeposits(t *testing.T) {
// Approve WETH9 with the bridge // Approve WETH9 with the bridge
tx, err = WETH9.Approve(opts, cfg.L1Deployments.L1StandardBridgeProxy, new(big.Int).SetUint64(math.MaxUint64)) tx, err = WETH9.Approve(opts, cfg.L1Deployments.L1StandardBridgeProxy, new(big.Int).SetUint64(math.MaxUint64))
require.NoError(t, err) require.NoError(t, err)
_, err = waitForTransaction(tx.Hash(), l1Client, 6*time.Duration(cfg.DeployConfig.L1BlockTime)*time.Second) _, err = geth.WaitForTransaction(tx.Hash(), l1Client, 6*time.Duration(cfg.DeployConfig.L1BlockTime)*time.Second)
require.NoError(t, err) require.NoError(t, err)
// Bridge the WETH9 // Bridge the WETH9
...@@ -84,7 +85,7 @@ func TestERC20BridgeDeposits(t *testing.T) { ...@@ -84,7 +85,7 @@ func TestERC20BridgeDeposits(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
tx, err = l1StandardBridge.BridgeERC20(opts, weth9Address, event.LocalToken, big.NewInt(100), 100000, []byte{}) tx, err = l1StandardBridge.BridgeERC20(opts, weth9Address, event.LocalToken, big.NewInt(100), 100000, []byte{})
require.NoError(t, err) require.NoError(t, err)
depositReceipt, err := waitForTransaction(tx.Hash(), l1Client, 3*time.Duration(cfg.DeployConfig.L1BlockTime)*time.Second) depositReceipt, err := geth.WaitForTransaction(tx.Hash(), l1Client, 3*time.Duration(cfg.DeployConfig.L1BlockTime)*time.Second)
require.NoError(t, err) require.NoError(t, err)
t.Log("Deposit through L1StandardBridge", "gas used", depositReceipt.GasUsed) t.Log("Deposit through L1StandardBridge", "gas used", depositReceipt.GasUsed)
...@@ -103,7 +104,7 @@ func TestERC20BridgeDeposits(t *testing.T) { ...@@ -103,7 +104,7 @@ func TestERC20BridgeDeposits(t *testing.T) {
depositTx, err := derive.UnmarshalDepositLogEvent(&depositEvent.Raw) depositTx, err := derive.UnmarshalDepositLogEvent(&depositEvent.Raw)
require.NoError(t, err) require.NoError(t, err)
_, err = waitForTransaction(types.NewTx(depositTx).Hash(), l2Client, 3*time.Duration(cfg.DeployConfig.L2BlockTime)*time.Second) _, err = geth.WaitForTransaction(types.NewTx(depositTx).Hash(), l2Client, 3*time.Duration(cfg.DeployConfig.L2BlockTime)*time.Second)
require.NoError(t, err) require.NoError(t, err)
// Ensure that the deposit went through // Ensure that the deposit went through
......
...@@ -16,6 +16,7 @@ import ( ...@@ -16,6 +16,7 @@ import (
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/cannon" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/cannon"
"github.com/ethereum-optimism/optimism/op-challenger/metrics" "github.com/ethereum-optimism/optimism/op-challenger/metrics"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/challenger" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/challenger"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/l2oo"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/transactions" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/transactions"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait"
"github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-node/rollup"
...@@ -65,7 +66,7 @@ type FactoryHelper struct { ...@@ -65,7 +66,7 @@ type FactoryHelper struct {
factoryAddr common.Address factoryAddr common.Address
factory *bindings.DisputeGameFactory factory *bindings.DisputeGameFactory
blockOracle *bindings.BlockOracle blockOracle *bindings.BlockOracle
l2oo *bindings.L2OutputOracleCaller l2ooHelper *l2oo.L2OOHelper
} }
func NewFactoryHelper(t *testing.T, ctx context.Context, deployments *genesis.L1Deployments, client *ethclient.Client) *FactoryHelper { func NewFactoryHelper(t *testing.T, ctx context.Context, deployments *genesis.L1Deployments, client *ethclient.Client) *FactoryHelper {
...@@ -81,8 +82,6 @@ func NewFactoryHelper(t *testing.T, ctx context.Context, deployments *genesis.L1 ...@@ -81,8 +82,6 @@ func NewFactoryHelper(t *testing.T, ctx context.Context, deployments *genesis.L1
require.NoError(err) require.NoError(err)
blockOracle, err := bindings.NewBlockOracle(deployments.BlockOracle, client) blockOracle, err := bindings.NewBlockOracle(deployments.BlockOracle, client)
require.NoError(err) require.NoError(err)
l2oo, err := bindings.NewL2OutputOracleCaller(deployments.L2OutputOracleProxy, client)
require.NoError(err, "Error creating l2oo caller")
return &FactoryHelper{ return &FactoryHelper{
t: t, t: t,
...@@ -92,7 +91,7 @@ func NewFactoryHelper(t *testing.T, ctx context.Context, deployments *genesis.L1 ...@@ -92,7 +91,7 @@ func NewFactoryHelper(t *testing.T, ctx context.Context, deployments *genesis.L1
factory: factory, factory: factory,
factoryAddr: factoryAddr, factoryAddr: factoryAddr,
blockOracle: blockOracle, blockOracle: blockOracle,
l2oo: l2oo, l2ooHelper: l2oo.NewL2OOHelperReadOnly(t, deployments, client),
} }
} }
...@@ -150,12 +149,8 @@ func (h *FactoryHelper) StartCannonGameWithCorrectRoot(ctx context.Context, roll ...@@ -150,12 +149,8 @@ func (h *FactoryHelper) StartCannonGameWithCorrectRoot(ctx context.Context, roll
challengerOpts = append(challengerOpts, options...) challengerOpts = append(challengerOpts, options...)
cfg := challenger.NewChallengerConfig(h.t, l1Endpoint, challengerOpts...) cfg := challenger.NewChallengerConfig(h.t, l1Endpoint, challengerOpts...)
opts := &bind.CallOpts{Context: ctx} opts := &bind.CallOpts{Context: ctx}
outputIdx, err := h.l2oo.GetL2OutputIndexAfter(opts, new(big.Int).SetUint64(l2BlockNumber)) challengedOutput := h.l2ooHelper.GetL2OutputAfter(ctx, l2BlockNumber)
h.require.NoError(err, "Fetch challenged output index") agreedOutput := h.l2ooHelper.GetL2OutputBefore(ctx, l2BlockNumber)
challengedOutput, err := h.l2oo.GetL2Output(opts, outputIdx)
h.require.NoError(err, "Fetch challenged output")
agreedOutput, err := h.l2oo.GetL2Output(opts, new(big.Int).Sub(outputIdx, common.Big1))
h.require.NoError(err, "Fetch agreed output")
l1BlockInfo, err := h.blockOracle.Load(opts, l1Head) l1BlockInfo, err := h.blockOracle.Load(opts, l1Head)
h.require.NoError(err, "Fetch L1 block info") h.require.NoError(err, "Fetch L1 block info")
...@@ -246,26 +241,8 @@ func (h *FactoryHelper) prepareCannonGame(ctx context.Context) (uint64, *big.Int ...@@ -246,26 +241,8 @@ func (h *FactoryHelper) prepareCannonGame(ctx context.Context) (uint64, *big.Int
func (h *FactoryHelper) waitForProposals(ctx context.Context) uint64 { func (h *FactoryHelper) waitForProposals(ctx context.Context) uint64 {
ctx, cancel := context.WithTimeout(ctx, 2*time.Minute) ctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
defer cancel() defer cancel()
opts := &bind.CallOpts{Context: ctx} latestOutputIdx := h.l2ooHelper.WaitForProposals(ctx, 2)
latestOutputIndex, err := wait.AndGet( return h.l2ooHelper.GetL2Output(ctx, latestOutputIdx).L2BlockNumber.Uint64()
ctx,
time.Second,
func() (*big.Int, error) {
index, err := h.l2oo.LatestOutputIndex(opts)
if err != nil {
h.t.Logf("Could not get latest output index: %v", err.Error())
return nil, nil
}
h.t.Logf("Latest output index: %v", index)
return index, nil
},
func(index *big.Int) bool {
return index != nil && index.Cmp(big.NewInt(1)) >= 0
})
h.require.NoError(err, "Did not get two output roots")
output, err := h.l2oo.GetL2Output(opts, latestOutputIndex)
h.require.NoErrorf(err, "Could not get latst output root index: %v", latestOutputIndex)
return output.L2BlockNumber.Uint64()
} }
// checkpointL1Block stores the current L1 block in the oracle // checkpointL1Block stores the current L1 block in the oracle
......
package op_e2e package geth
import ( import (
"time" "time"
......
package op_e2e package geth
import ( import (
"context"
"crypto/ecdsa"
"errors"
"fmt" "fmt"
"math/big" "math/big"
"time"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
"github.com/ethereum-optimism/optimism/op-service/clock" "github.com/ethereum-optimism/optimism/op-service/clock"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/cmd/utils"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/eth" "github.com/ethereum/go-ethereum/eth"
"github.com/ethereum/go-ethereum/eth/catalyst" "github.com/ethereum/go-ethereum/eth/catalyst"
"github.com/ethereum/go-ethereum/eth/ethconfig" "github.com/ethereum/go-ethereum/eth/ethconfig"
"github.com/ethereum/go-ethereum/eth/tracers" "github.com/ethereum/go-ethereum/eth/tracers"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/miner" "github.com/ethereum/go-ethereum/miner"
"github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/node"
...@@ -30,101 +21,9 @@ import ( ...@@ -30,101 +21,9 @@ import (
_ "github.com/ethereum/go-ethereum/eth/tracers/native" _ "github.com/ethereum/go-ethereum/eth/tracers/native"
) )
var ( func InitL1(chainID uint64, blockTime uint64, genesis *core.Genesis, c clock.Clock, opts ...GethOption) (*node.Node, *eth.Ethereum, error) {
// errTimeout represents a timeout
errTimeout = errors.New("timeout")
)
func waitForL1OriginOnL2(l1BlockNum uint64, client *ethclient.Client, timeout time.Duration) (*types.Block, error) {
timeoutCh := time.After(timeout)
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
headChan := make(chan *types.Header, 100)
headSub, err := client.SubscribeNewHead(ctx, headChan)
if err != nil {
return nil, err
}
defer headSub.Unsubscribe()
for {
select {
case head := <-headChan:
block, err := client.BlockByNumber(ctx, head.Number)
if err != nil {
return nil, err
}
l1Info, err := derive.L1InfoDepositTxData(block.Transactions()[0].Data())
if err != nil {
return nil, err
}
if l1Info.Number >= l1BlockNum {
return block, nil
}
case err := <-headSub.Err():
return nil, fmt.Errorf("error in head subscription: %w", err)
case <-timeoutCh:
return nil, errTimeout
}
}
}
func waitForTransaction(hash common.Hash, client *ethclient.Client, timeout time.Duration) (*types.Receipt, error) {
timeoutCh := time.After(timeout)
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
for {
receipt, err := client.TransactionReceipt(ctx, hash)
if receipt != nil && err == nil {
return receipt, nil
} else if err != nil && !errors.Is(err, ethereum.NotFound) {
return nil, err
}
select {
case <-timeoutCh:
tip, err := client.BlockByNumber(context.Background(), nil)
if err != nil {
return nil, err
}
return nil, fmt.Errorf("receipt for transaction %s not found. tip block number is %d: %w", hash.Hex(), tip.NumberU64(), errTimeout)
case <-ticker.C:
}
}
}
func waitForBlock(number *big.Int, client *ethclient.Client, timeout time.Duration) (*types.Block, error) {
timeoutCh := time.After(timeout)
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
headChan := make(chan *types.Header, 100)
headSub, err := client.SubscribeNewHead(ctx, headChan)
if err != nil {
return nil, err
}
defer headSub.Unsubscribe()
for {
select {
case head := <-headChan:
if head.Number.Cmp(number) >= 0 {
return client.BlockByNumber(ctx, number)
}
case err := <-headSub.Err():
return nil, fmt.Errorf("error in head subscription: %w", err)
case <-timeoutCh:
return nil, errTimeout
}
}
}
func initL1Geth(cfg *SystemConfig, genesis *core.Genesis, c clock.Clock, opts ...GethOption) (*node.Node, *eth.Ethereum, error) {
ethConfig := &ethconfig.Config{ ethConfig := &ethconfig.Config{
NetworkId: cfg.DeployConfig.L1ChainID, NetworkId: chainID,
Genesis: genesis, Genesis: genesis,
} }
nodeConfig := &node.Config{ nodeConfig := &node.Config{
...@@ -137,7 +36,7 @@ func initL1Geth(cfg *SystemConfig, genesis *core.Genesis, c clock.Clock, opts .. ...@@ -137,7 +36,7 @@ func initL1Geth(cfg *SystemConfig, genesis *core.Genesis, c clock.Clock, opts ..
HTTPModules: []string{"debug", "admin", "eth", "txpool", "net", "rpc", "web3", "personal", "engine"}, HTTPModules: []string{"debug", "admin", "eth", "txpool", "net", "rpc", "web3", "personal", "engine"},
} }
l1Node, l1Eth, err := createGethNode(false, nodeConfig, ethConfig, []*ecdsa.PrivateKey{cfg.Secrets.CliqueSigner}, opts...) l1Node, l1Eth, err := createGethNode(false, nodeConfig, ethConfig, opts...)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
...@@ -149,7 +48,7 @@ func initL1Geth(cfg *SystemConfig, genesis *core.Genesis, c clock.Clock, opts .. ...@@ -149,7 +48,7 @@ func initL1Geth(cfg *SystemConfig, genesis *core.Genesis, c clock.Clock, opts ..
clock: c, clock: c,
eth: l1Eth, eth: l1Eth,
log: log.Root(), // geth logger is global anyway. Would be nice to replace with a local logger though. log: log.Root(), // geth logger is global anyway. Would be nice to replace with a local logger though.
blockTime: cfg.DeployConfig.L1BlockTime, blockTime: blockTime,
// for testing purposes we make it really fast, otherwise we don't see it finalize in short tests // for testing purposes we make it really fast, otherwise we don't see it finalize in short tests
finalizedDistance: 8, finalizedDistance: 8,
safeDistance: 4, safeDistance: 4,
...@@ -176,8 +75,8 @@ func defaultNodeConfig(name string, jwtPath string) *node.Config { ...@@ -176,8 +75,8 @@ func defaultNodeConfig(name string, jwtPath string) *node.Config {
type GethOption func(ethCfg *ethconfig.Config, nodeCfg *node.Config) error type GethOption func(ethCfg *ethconfig.Config, nodeCfg *node.Config) error
// init a geth node. // InitL2 inits a L2 geth node.
func initL2Geth(name string, l2ChainID *big.Int, genesis *core.Genesis, jwtPath string, opts ...GethOption) (*node.Node, *eth.Ethereum, error) { func InitL2(name string, l2ChainID *big.Int, genesis *core.Genesis, jwtPath string, opts ...GethOption) (*node.Node, *eth.Ethereum, error) {
ethConfig := &ethconfig.Config{ ethConfig := &ethconfig.Config{
NetworkId: l2ChainID.Uint64(), NetworkId: l2ChainID.Uint64(),
Genesis: genesis, Genesis: genesis,
...@@ -192,14 +91,14 @@ func initL2Geth(name string, l2ChainID *big.Int, genesis *core.Genesis, jwtPath ...@@ -192,14 +91,14 @@ func initL2Geth(name string, l2ChainID *big.Int, genesis *core.Genesis, jwtPath
}, },
} }
nodeConfig := defaultNodeConfig(fmt.Sprintf("l2-geth-%v", name), jwtPath) nodeConfig := defaultNodeConfig(fmt.Sprintf("l2-geth-%v", name), jwtPath)
return createGethNode(true, nodeConfig, ethConfig, nil, opts...) return createGethNode(true, nodeConfig, ethConfig, opts...)
} }
// createGethNode creates an in-memory geth node based on the configuration. // createGethNode creates an in-memory geth node based on the configuration.
// The private keys are added to the keystore and are unlocked. // The private keys are added to the keystore and are unlocked.
// If the node is l2, catalyst is enabled. // If the node is l2, catalyst is enabled.
// The node should be started and then closed when done. // The node should be started and then closed when done.
func createGethNode(l2 bool, nodeCfg *node.Config, ethCfg *ethconfig.Config, privateKeys []*ecdsa.PrivateKey, opts ...GethOption) (*node.Node, *eth.Ethereum, error) { func createGethNode(l2 bool, nodeCfg *node.Config, ethCfg *ethconfig.Config, opts ...GethOption) (*node.Node, *eth.Ethereum, error) {
for i, opt := range opts { for i, opt := range opts {
if err := opt(ethCfg, nodeCfg); err != nil { if err := opt(ethCfg, nodeCfg); err != nil {
return nil, nil, fmt.Errorf("failed to apply geth option %d: %w", i, err) return nil, nil, fmt.Errorf("failed to apply geth option %d: %w", i, err)
...@@ -212,28 +111,6 @@ func createGethNode(l2 bool, nodeCfg *node.Config, ethCfg *ethconfig.Config, pri ...@@ -212,28 +111,6 @@ func createGethNode(l2 bool, nodeCfg *node.Config, ethCfg *ethconfig.Config, pri
return nil, nil, err return nil, nil, err
} }
if !l2 {
keydir := n.KeyStoreDir()
scryptN := 2
scryptP := 1
n.AccountManager().AddBackend(keystore.NewKeyStore(keydir, scryptN, scryptP))
ks := n.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
password := "foobar"
for _, pk := range privateKeys {
act, err := ks.ImportECDSA(pk, password)
if err != nil {
n.Close()
return nil, nil, err
}
err = ks.Unlock(act, password)
if err != nil {
n.Close()
return nil, nil, err
}
}
}
backend, err := eth.New(n, ethCfg) backend, err := eth.New(n, ethCfg)
if err != nil { if err != nil {
n.Close() n.Close()
......
package geth
import (
"context"
"errors"
"fmt"
"math/big"
"time"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
)
var (
// errTimeout represents a timeout
errTimeout = errors.New("timeout")
)
func WaitForL1OriginOnL2(l1BlockNum uint64, client *ethclient.Client, timeout time.Duration) (*types.Block, error) {
timeoutCh := time.After(timeout)
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
headChan := make(chan *types.Header, 100)
headSub, err := client.SubscribeNewHead(ctx, headChan)
if err != nil {
return nil, err
}
defer headSub.Unsubscribe()
for {
select {
case head := <-headChan:
block, err := client.BlockByNumber(ctx, head.Number)
if err != nil {
return nil, err
}
l1Info, err := derive.L1InfoDepositTxData(block.Transactions()[0].Data())
if err != nil {
return nil, err
}
if l1Info.Number >= l1BlockNum {
return block, nil
}
case err := <-headSub.Err():
return nil, fmt.Errorf("error in head subscription: %w", err)
case <-timeoutCh:
return nil, errTimeout
}
}
}
func WaitForTransaction(hash common.Hash, client *ethclient.Client, timeout time.Duration) (*types.Receipt, error) {
timeoutCh := time.After(timeout)
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
for {
receipt, err := client.TransactionReceipt(ctx, hash)
if receipt != nil && err == nil {
return receipt, nil
} else if err != nil && !errors.Is(err, ethereum.NotFound) {
return nil, err
}
select {
case <-timeoutCh:
tip, err := client.BlockByNumber(context.Background(), nil)
if err != nil {
return nil, err
}
return nil, fmt.Errorf("receipt for transaction %s not found. tip block number is %d: %w", hash.Hex(), tip.NumberU64(), errTimeout)
case <-ticker.C:
}
}
}
func WaitForBlock(number *big.Int, client *ethclient.Client, timeout time.Duration) (*types.Block, error) {
timeoutCh := time.After(timeout)
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
headChan := make(chan *types.Header, 100)
headSub, err := client.SubscribeNewHead(ctx, headChan)
if err != nil {
return nil, err
}
defer headSub.Unsubscribe()
for {
select {
case head := <-headChan:
if head.Number.Cmp(number) >= 0 {
return client.BlockByNumber(ctx, number)
}
case err := <-headSub.Err():
return nil, fmt.Errorf("error in head subscription: %w", err)
case <-timeoutCh:
return nil, errTimeout
}
}
}
package l2oo
import (
"context"
"crypto/ecdsa"
"math/big"
"testing"
"time"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-chain-ops/genesis"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait"
"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/stretchr/testify/require"
)
type L2OOHelper struct {
t *testing.T
require *require.Assertions
client *ethclient.Client
l2oo *bindings.L2OutputOracle
// Nil when read-only
transactOpts *bind.TransactOpts
rollupCfg *rollup.Config
}
func NewL2OOHelperReadOnly(t *testing.T, deployments *genesis.L1Deployments, client *ethclient.Client) *L2OOHelper {
require := require.New(t)
l2oo, err := bindings.NewL2OutputOracle(deployments.L2OutputOracleProxy, client)
require.NoError(err, "Error creating l2oo bindings")
return &L2OOHelper{
t: t,
require: require,
client: client,
l2oo: l2oo,
}
}
func NewL2OOHelper(t *testing.T, deployments *genesis.L1Deployments, client *ethclient.Client, proposerKey *ecdsa.PrivateKey, rollupCfg *rollup.Config) *L2OOHelper {
h := NewL2OOHelperReadOnly(t, deployments, client)
chainID, err := client.ChainID(context.Background())
h.require.NoError(err, "Failed to get chain ID")
transactOpts, err := bind.NewKeyedTransactorWithChainID(proposerKey, chainID)
h.require.NoError(err)
h.transactOpts = transactOpts
h.rollupCfg = rollupCfg
return h
}
// WaitForProposals waits until there are at least the specified number of proposals in the output oracle
// Returns the index of the latest output proposal
func (h *L2OOHelper) WaitForProposals(ctx context.Context, req int64) uint64 {
ctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
defer cancel()
opts := &bind.CallOpts{Context: ctx}
latestOutputIndex, err := wait.AndGet(
ctx,
time.Second,
func() (*big.Int, error) {
index, err := h.l2oo.LatestOutputIndex(opts)
if err != nil {
h.t.Logf("Could not get latest output index: %v", err.Error())
return nil, nil
}
h.t.Logf("Latest output index: %v", index)
return index, nil
},
func(index *big.Int) bool {
return index != nil && index.Cmp(big.NewInt(req-1)) >= 0
})
h.require.NoErrorf(err, "Did not get %v output roots", req)
return latestOutputIndex.Uint64()
}
func (h *L2OOHelper) GetL2Output(ctx context.Context, idx uint64) bindings.TypesOutputProposal {
output, err := h.l2oo.GetL2Output(&bind.CallOpts{Context: ctx}, new(big.Int).SetUint64(idx))
h.require.NoErrorf(err, "Failed to get output root at index: %v", idx)
return output
}
func (h *L2OOHelper) GetL2OutputAfter(ctx context.Context, l2BlockNum uint64) bindings.TypesOutputProposal {
opts := &bind.CallOpts{Context: ctx}
outputIdx, err := h.l2oo.GetL2OutputIndexAfter(opts, new(big.Int).SetUint64(l2BlockNum))
h.require.NoError(err, "Fetch challenged output index")
output, err := h.l2oo.GetL2Output(opts, outputIdx)
h.require.NoError(err, "Fetch challenged output")
return output
}
func (h *L2OOHelper) GetL2OutputBefore(ctx context.Context, l2BlockNum uint64) bindings.TypesOutputProposal {
opts := &bind.CallOpts{Context: ctx}
latestBlockNum, err := h.l2oo.LatestBlockNumber(opts)
h.require.NoError(err, "Failed to get latest output root block number")
var outputIdx *big.Int
if latestBlockNum.Uint64() < l2BlockNum {
outputIdx, err = h.l2oo.LatestOutputIndex(opts)
h.require.NoError(err, "Failed to get latest output index")
} else {
outputIdx, err = h.l2oo.GetL2OutputIndexAfter(opts, new(big.Int).SetUint64(l2BlockNum))
h.require.NoErrorf(err, "Failed to get output index after block %v", l2BlockNum)
h.require.NotZerof(outputIdx.Uint64(), "No l2 output before block %v", l2BlockNum)
outputIdx = new(big.Int).Sub(outputIdx, common.Big1)
}
return h.GetL2Output(ctx, outputIdx.Uint64())
}
func (h *L2OOHelper) PublishNextOutput(ctx context.Context, outputRoot common.Hash) {
h.require.NotNil(h.transactOpts, "Can't publish outputs from a read only L2OOHelper")
nextBlockNum, err := h.l2oo.NextBlockNumber(&bind.CallOpts{Context: ctx})
h.require.NoError(err, "Should get next block number")
genesis := h.rollupCfg.Genesis
targetTimestamp := genesis.L2Time + ((nextBlockNum.Uint64() - genesis.L2.Number) * h.rollupCfg.BlockTime)
timedCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
h.require.NoErrorf(
wait.ForBlockWithTimestamp(timedCtx, h.client, targetTimestamp),
"Wait for L1 block with timestamp >= %v", targetTimestamp)
tx, err := h.l2oo.ProposeL2Output(h.transactOpts, outputRoot, nextBlockNum, [32]byte{}, common.Big0)
h.require.NoErrorf(err, "Failed to propose output root for l2 block number %v", nextBlockNum)
_, err = wait.ForReceiptOK(ctx, h.client, tx.Hash())
h.require.NoErrorf(err, "Proposal for l2 block %v failed", nextBlockNum)
}
...@@ -85,6 +85,19 @@ func ForBlock(ctx context.Context, client *ethclient.Client, n uint64) error { ...@@ -85,6 +85,19 @@ func ForBlock(ctx context.Context, client *ethclient.Client, n uint64) error {
return nil return nil
} }
func ForBlockWithTimestamp(ctx context.Context, client *ethclient.Client, target uint64) error {
_, err := AndGet(ctx, time.Second, func() (uint64, error) {
head, err := client.BlockByNumber(ctx, nil)
if err != nil {
return 0, err
}
return head.Time(), nil
}, func(actual uint64) bool {
return actual >= target
})
return err
}
func ForNextBlock(ctx context.Context, client *ethclient.Client) error { func ForNextBlock(ctx context.Context, client *ethclient.Client) error {
current, err := client.BlockNumber(ctx) current, err := client.BlockNumber(ctx)
if err != nil { if err != nil {
......
...@@ -6,6 +6,7 @@ import ( ...@@ -6,6 +6,7 @@ import (
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/challenger" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/challenger"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/disputegame" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/disputegame"
l2oo2 "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/l2oo"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/ethclient"
...@@ -64,8 +65,8 @@ func TestMultipleCannonGames(t *testing.T) { ...@@ -64,8 +65,8 @@ func TestMultipleCannonGames(t *testing.T) {
challenger.WithAgreeProposedOutput(true), challenger.WithAgreeProposedOutput(true),
) )
game1 := gameFactory.StartCannonGame(ctx, common.Hash{0xaa}) game1 := gameFactory.StartCannonGame(ctx, common.Hash{0x01, 0xaa})
game2 := gameFactory.StartCannonGame(ctx, common.Hash{0xbb}) game2 := gameFactory.StartCannonGame(ctx, common.Hash{0x01, 0xbb})
game1.WaitForClaimCount(ctx, 2) game1.WaitForClaimCount(ctx, 2)
game2.WaitForClaimCount(ctx, 2) game2.WaitForClaimCount(ctx, 2)
...@@ -260,7 +261,7 @@ func TestCannonDisputeGame(t *testing.T) { ...@@ -260,7 +261,7 @@ func TestCannonDisputeGame(t *testing.T) {
t.Cleanup(sys.Close) t.Cleanup(sys.Close)
disputeGameFactory := disputegame.NewFactoryHelper(t, ctx, sys.cfg.L1Deployments, l1Client) disputeGameFactory := disputegame.NewFactoryHelper(t, ctx, sys.cfg.L1Deployments, l1Client)
game := disputeGameFactory.StartCannonGame(ctx, common.Hash{0xaa}) game := disputeGameFactory.StartCannonGame(ctx, common.Hash{0x01, 0xaa})
require.NotNil(t, game) require.NotNil(t, game)
game.LogGameData(ctx) game.LogGameData(ctx)
...@@ -309,7 +310,7 @@ func TestCannonDefendStep(t *testing.T) { ...@@ -309,7 +310,7 @@ func TestCannonDefendStep(t *testing.T) {
t.Cleanup(sys.Close) t.Cleanup(sys.Close)
disputeGameFactory := disputegame.NewFactoryHelper(t, ctx, sys.cfg.L1Deployments, l1Client) disputeGameFactory := disputegame.NewFactoryHelper(t, ctx, sys.cfg.L1Deployments, l1Client)
game := disputeGameFactory.StartCannonGame(ctx, common.Hash{0xaa}) game := disputeGameFactory.StartCannonGame(ctx, common.Hash{0x01, 0xaa})
require.NotNil(t, game) require.NotNil(t, game)
game.LogGameData(ctx) game.LogGameData(ctx)
...@@ -355,6 +356,83 @@ func TestCannonDefendStep(t *testing.T) { ...@@ -355,6 +356,83 @@ func TestCannonDefendStep(t *testing.T) {
game.LogGameData(ctx) game.LogGameData(ctx)
} }
func TestCannonProposedOutputRootInvalid(t *testing.T) {
InitParallel(t)
ctx := context.Background()
sys, l1Client, game, correctTrace := setupDisputeGameForInvalidOutputRoot(t, common.Hash{0xab})
t.Cleanup(sys.Close)
maxDepth := game.MaxDepth(ctx)
// Now maliciously play the game and it should be impossible to win
for claimCount := int64(1); claimCount < maxDepth; {
// Attack everything but oddly using the correct hash.
correctTrace.Attack(ctx, claimCount-1)
claimCount++
game.LogGameData(ctx)
game.WaitForClaimCount(ctx, claimCount)
game.LogGameData(ctx)
// Wait for the challenger to counter
claimCount++
game.WaitForClaimCount(ctx, claimCount)
}
game.LogGameData(ctx)
// Wait for the challenger to call step and counter our invalid claim
game.WaitForClaimAtMaxDepth(ctx, false)
// It's on us to call step if we want to win but shouldn't be possible
// Need to add support for this to the helper
// Time travel past when the game will be resolvable.
sys.TimeTravelClock.AdvanceTime(game.GameDuration(ctx))
require.NoError(t, wait.ForNextBlock(ctx, l1Client))
game.WaitForGameStatus(ctx, disputegame.StatusDefenderWins)
game.LogGameData(ctx)
}
// setupDisputeGameForInvalidOutputRoot sets up an L2 chain with at least one valid output root followed by an invalid output root.
// A cannon dispute game is started to dispute the invalid output root with the correct root claim provided.
// An honest challenger is run to defend the root claim (ie disagree with the invalid output root).
func setupDisputeGameForInvalidOutputRoot(t *testing.T, outputRoot common.Hash) (*System, *ethclient.Client, *disputegame.CannonGameHelper, *disputegame.HonestHelper) {
ctx := context.Background()
sys, l1Client := startFaultDisputeSystem(t)
l2oo := l2oo2.NewL2OOHelper(t, sys.cfg.L1Deployments, l1Client, sys.cfg.Secrets.Proposer, sys.RollupConfig)
// Wait for one valid output root to be submitted
l2oo.WaitForProposals(ctx, 1)
// Stop the honest output submitter so we can publish invalid outputs
sys.L2OutputSubmitter.Stop()
sys.L2OutputSubmitter = nil
// Submit an invalid output rooot
l2oo.PublishNextOutput(ctx, outputRoot)
l1Endpoint := sys.NodeEndpoint("l1")
l2Endpoint := sys.NodeEndpoint("sequencer")
// Dispute the new output root by creating a new game with the correct cannon trace.
disputeGameFactory := disputegame.NewFactoryHelper(t, ctx, sys.cfg.L1Deployments, l1Client)
game, correctTrace := disputeGameFactory.StartCannonGameWithCorrectRoot(ctx, sys.RollupConfig, sys.L2GenesisCfg, l1Endpoint, l2Endpoint,
challenger.WithPrivKey(sys.cfg.Secrets.Mallory),
)
require.NotNil(t, game)
// Start the honest challenger
game.StartChallenger(ctx, sys.RollupConfig, sys.L2GenesisCfg, l1Endpoint, l2Endpoint, "Defender",
// Disagree with the proposed output, so agree with the (correct) root claim
challenger.WithAgreeProposedOutput(false),
challenger.WithPrivKey(sys.cfg.Secrets.Mallory),
)
return sys, l1Client, game, correctTrace
}
func TestCannonChallengeWithCorrectRoot(t *testing.T) { func TestCannonChallengeWithCorrectRoot(t *testing.T) {
t.Skip("Not currently handling this case as the correct approach will change when output root bisection is added") t.Skip("Not currently handling this case as the correct approach will change when output root bisection is added")
InitParallel(t) InitParallel(t)
......
...@@ -11,7 +11,7 @@ import ( ...@@ -11,7 +11,7 @@ import (
func TestTxGossip(t *testing.T) { func TestTxGossip(t *testing.T) {
InitParallel(t) InitParallel(t)
cfg := DefaultSystemConfig(t) cfg := DefaultSystemConfig(t)
gethOpts := []GethOption{ gethOpts := []geth.GethOption{
geth.WithP2P(), geth.WithP2P(),
} }
cfg.GethOptions["sequencer"] = gethOpts cfg.GethOptions["sequencer"] = gethOpts
......
...@@ -11,6 +11,7 @@ import ( ...@@ -11,6 +11,7 @@ import (
"github.com/ethereum-optimism/optimism/op-chain-ops/genesis" "github.com/ethereum-optimism/optimism/op-chain-ops/genesis"
"github.com/ethereum-optimism/optimism/op-e2e/config" "github.com/ethereum-optimism/optimism/op-e2e/config"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/geth"
"github.com/ethereum-optimism/optimism/op-node/client" "github.com/ethereum-optimism/optimism/op-node/client"
"github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive" "github.com/ethereum-optimism/optimism/op-node/rollup/derive"
...@@ -72,7 +73,7 @@ func NewOpGeth(t *testing.T, ctx context.Context, cfg *SystemConfig) (*OpGeth, e ...@@ -72,7 +73,7 @@ func NewOpGeth(t *testing.T, ctx context.Context, cfg *SystemConfig) (*OpGeth, e
SystemConfig: e2eutils.SystemConfigFromDeployConfig(cfg.DeployConfig), SystemConfig: e2eutils.SystemConfigFromDeployConfig(cfg.DeployConfig),
} }
node, _, err := initL2Geth("l2", big.NewInt(int64(cfg.DeployConfig.L2ChainID)), l2Genesis, cfg.JWTFilePath) node, _, err := geth.InitL2("l2", big.NewInt(int64(cfg.DeployConfig.L2ChainID)), l2Genesis, cfg.JWTFilePath)
require.Nil(t, err) require.Nil(t, err)
require.Nil(t, node.Start()) require.Nil(t, node.Start())
......
...@@ -14,6 +14,7 @@ import ( ...@@ -14,6 +14,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/geth"
"github.com/ethereum-optimism/optimism/op-node/p2p/store" "github.com/ethereum-optimism/optimism/op-node/p2p/store"
"github.com/ethereum-optimism/optimism/op-service/clock" "github.com/ethereum-optimism/optimism/op-service/clock"
ds "github.com/ipfs/go-datastore" ds "github.com/ipfs/go-datastore"
...@@ -140,7 +141,7 @@ func DefaultSystemConfig(t *testing.T) SystemConfig { ...@@ -140,7 +141,7 @@ func DefaultSystemConfig(t *testing.T) SystemConfig {
"batcher": testlog.Logger(t, log.LvlInfo).New("role", "batcher"), "batcher": testlog.Logger(t, log.LvlInfo).New("role", "batcher"),
"proposer": testlog.Logger(t, log.LvlCrit).New("role", "proposer"), "proposer": testlog.Logger(t, log.LvlCrit).New("role", "proposer"),
}, },
GethOptions: map[string][]GethOption{}, GethOptions: map[string][]geth.GethOption{},
P2PTopology: nil, // no P2P connectivity by default P2PTopology: nil, // no P2P connectivity by default
NonFinalizedProposals: false, NonFinalizedProposals: false,
ExternalL2Shim: config.ExternalL2Shim, ExternalL2Shim: config.ExternalL2Shim,
...@@ -175,7 +176,7 @@ type SystemConfig struct { ...@@ -175,7 +176,7 @@ type SystemConfig struct {
Premine map[common.Address]*big.Int Premine map[common.Address]*big.Int
Nodes map[string]*rollupNode.Config // Per node config. Don't use populate rollup.Config Nodes map[string]*rollupNode.Config // Per node config. Don't use populate rollup.Config
Loggers map[string]log.Logger Loggers map[string]log.Logger
GethOptions map[string][]GethOption GethOptions map[string][]geth.GethOption
ProposerLogger log.Logger ProposerLogger log.Logger
BatcherLogger log.Logger BatcherLogger log.Logger
...@@ -426,7 +427,7 @@ func (cfg SystemConfig) Start(t *testing.T, _opts ...SystemConfigOption) (*Syste ...@@ -426,7 +427,7 @@ func (cfg SystemConfig) Start(t *testing.T, _opts ...SystemConfigOption) (*Syste
sys.RollupConfig = &defaultConfig sys.RollupConfig = &defaultConfig
// Initialize nodes // Initialize nodes
l1Node, l1Backend, err := initL1Geth(&cfg, l1Genesis, c, cfg.GethOptions["l1"]...) l1Node, l1Backend, err := geth.InitL1(cfg.DeployConfig.L1ChainID, cfg.DeployConfig.L1BlockTime, l1Genesis, c, cfg.GethOptions["l1"]...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -443,7 +444,7 @@ func (cfg SystemConfig) Start(t *testing.T, _opts ...SystemConfigOption) (*Syste ...@@ -443,7 +444,7 @@ func (cfg SystemConfig) Start(t *testing.T, _opts ...SystemConfigOption) (*Syste
for name := range cfg.Nodes { for name := range cfg.Nodes {
var ethClient EthInstance var ethClient EthInstance
if cfg.ExternalL2Shim == "" { if cfg.ExternalL2Shim == "" {
node, backend, err := initL2Geth(name, big.NewInt(int64(cfg.DeployConfig.L2ChainID)), l2Genesis, cfg.JWTFilePath, cfg.GethOptions[name]...) node, backend, err := geth.InitL2(name, big.NewInt(int64(cfg.DeployConfig.L2ChainID)), l2Genesis, cfg.JWTFilePath, cfg.GethOptions[name]...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -507,7 +508,7 @@ func (cfg SystemConfig) Start(t *testing.T, _opts ...SystemConfigOption) (*Syste ...@@ -507,7 +508,7 @@ func (cfg SystemConfig) Start(t *testing.T, _opts ...SystemConfigOption) (*Syste
sys.Clients[name] = client sys.Clients[name] = client
} }
_, err = waitForBlock(big.NewInt(2), l1Client, 6*time.Second*time.Duration(cfg.DeployConfig.L1BlockTime)) _, err = geth.WaitForBlock(big.NewInt(2), l1Client, 6*time.Second*time.Duration(cfg.DeployConfig.L1BlockTime))
if err != nil { if err != nil {
return nil, fmt.Errorf("waiting for blocks: %w", err) return nil, fmt.Errorf("waiting for blocks: %w", err)
} }
......
...@@ -6,6 +6,7 @@ import ( ...@@ -6,6 +6,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/geth"
"github.com/ethereum-optimism/optimism/op-node/client" "github.com/ethereum-optimism/optimism/op-node/client"
"github.com/ethereum-optimism/optimism/op-node/sources" "github.com/ethereum-optimism/optimism/op-node/sources"
"github.com/ethereum-optimism/optimism/op-node/testlog" "github.com/ethereum-optimism/optimism/op-node/testlog"
...@@ -99,7 +100,7 @@ func testVerifyL2OutputRootEmptyBlock(t *testing.T, detached bool) { ...@@ -99,7 +100,7 @@ func testVerifyL2OutputRootEmptyBlock(t *testing.T, detached bool) {
t.Log("Wait for sequencer to catch up with last submitted batch") t.Log("Wait for sequencer to catch up with last submitted batch")
l1HeadNum, err := l1Client.BlockNumber(ctx) l1HeadNum, err := l1Client.BlockNumber(ctx)
require.NoError(t, err) require.NoError(t, err)
_, err = waitForL1OriginOnL2(l1HeadNum, l2Seq, 30*time.Second) _, err = geth.WaitForL1OriginOnL2(l1HeadNum, l2Seq, 30*time.Second)
require.NoError(t, err) require.NoError(t, err)
// Get the current safe head now that the batcher is stopped // Get the current safe head now that the batcher is stopped
......
...@@ -9,6 +9,7 @@ import ( ...@@ -9,6 +9,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/geth"
"github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
...@@ -88,7 +89,7 @@ func TestL2OutputSubmitter(t *testing.T) { ...@@ -88,7 +89,7 @@ func TestL2OutputSubmitter(t *testing.T) {
// for that block and subsequently reorgs to match what the verifier derives when running the // for that block and subsequently reorgs to match what the verifier derives when running the
// reconcillation process. // reconcillation process.
l2Verif := sys.Clients["verifier"] l2Verif := sys.Clients["verifier"]
_, err = waitForBlock(big.NewInt(6), l2Verif, 10*time.Duration(cfg.DeployConfig.L2BlockTime)*time.Second) _, err = geth.WaitForBlock(big.NewInt(6), l2Verif, 10*time.Duration(cfg.DeployConfig.L2BlockTime)*time.Second)
require.Nil(t, err) require.Nil(t, err)
// Wait for batch submitter to update L2 output oracle. // Wait for batch submitter to update L2 output oracle.
...@@ -260,14 +261,14 @@ func TestPendingGasLimit(t *testing.T) { ...@@ -260,14 +261,14 @@ func TestPendingGasLimit(t *testing.T) {
// configure the L2 gas limit to be high, and the pending gas limits to be lower for resource saving. // configure the L2 gas limit to be high, and the pending gas limits to be lower for resource saving.
cfg.DeployConfig.L2GenesisBlockGasLimit = 30_000_000 cfg.DeployConfig.L2GenesisBlockGasLimit = 30_000_000
cfg.GethOptions["sequencer"] = []GethOption{ cfg.GethOptions["sequencer"] = []geth.GethOption{
func(ethCfg *ethconfig.Config, nodeCfg *node.Config) error { func(ethCfg *ethconfig.Config, nodeCfg *node.Config) error {
ethCfg.Miner.GasCeil = 10_000_000 ethCfg.Miner.GasCeil = 10_000_000
ethCfg.Miner.RollupComputePendingBlock = true ethCfg.Miner.RollupComputePendingBlock = true
return nil return nil
}, },
} }
cfg.GethOptions["verifier"] = []GethOption{ cfg.GethOptions["verifier"] = []geth.GethOption{
func(ethCfg *ethconfig.Config, nodeCfg *node.Config) error { func(ethCfg *ethconfig.Config, nodeCfg *node.Config) error {
ethCfg.Miner.GasCeil = 9_000_000 ethCfg.Miner.GasCeil = 9_000_000
ethCfg.Miner.RollupComputePendingBlock = true ethCfg.Miner.RollupComputePendingBlock = true
...@@ -370,7 +371,7 @@ func TestMissingBatchE2E(t *testing.T) { ...@@ -370,7 +371,7 @@ func TestMissingBatchE2E(t *testing.T) {
}) })
// Wait until the block it was first included in shows up in the safe chain on the verifier // Wait until the block it was first included in shows up in the safe chain on the verifier
_, err = waitForBlock(receipt.BlockNumber, l2Verif, time.Duration((sys.RollupConfig.SeqWindowSize+4)*cfg.DeployConfig.L1BlockTime)*time.Second) _, err = geth.WaitForBlock(receipt.BlockNumber, l2Verif, time.Duration((sys.RollupConfig.SeqWindowSize+4)*cfg.DeployConfig.L1BlockTime)*time.Second)
require.Nil(t, err, "Waiting for block on verifier") require.Nil(t, err, "Waiting for block on verifier")
// Assert that the transaction is not found on the verifier // Assert that the transaction is not found on the verifier
...@@ -701,7 +702,7 @@ func TestSystemP2PAltSync(t *testing.T) { ...@@ -701,7 +702,7 @@ func TestSystemP2PAltSync(t *testing.T) {
}, },
} }
configureL1(syncNodeCfg, sys.EthInstances["l1"]) configureL1(syncNodeCfg, sys.EthInstances["l1"])
syncerL2Engine, _, err := initL2Geth("syncer", big.NewInt(int64(cfg.DeployConfig.L2ChainID)), sys.L2GenesisCfg, cfg.JWTFilePath) syncerL2Engine, _, err := geth.InitL2("syncer", big.NewInt(int64(cfg.DeployConfig.L2ChainID)), sys.L2GenesisCfg, cfg.JWTFilePath)
require.NoError(t, err) require.NoError(t, err)
require.NoError(t, syncerL2Engine.Start()) require.NoError(t, syncerL2Engine.Start())
...@@ -723,7 +724,7 @@ func TestSystemP2PAltSync(t *testing.T) { ...@@ -723,7 +724,7 @@ func TestSystemP2PAltSync(t *testing.T) {
l2Verif := ethclient.NewClient(rpc) l2Verif := ethclient.NewClient(rpc)
// It may take a while to sync, but eventually we should see the sequenced data show up // It may take a while to sync, but eventually we should see the sequenced data show up
receiptVerif, err := waitForTransaction(receiptSeq.TxHash, l2Verif, 100*time.Duration(sys.RollupConfig.BlockTime)*time.Second) receiptVerif, err := geth.WaitForTransaction(receiptSeq.TxHash, l2Verif, 100*time.Duration(sys.RollupConfig.BlockTime)*time.Second)
require.Nil(t, err, "Waiting for L2 tx on verifier") require.Nil(t, err, "Waiting for L2 tx on verifier")
require.Equal(t, receiptSeq, receiptVerif) require.Equal(t, receiptSeq, receiptVerif)
...@@ -853,9 +854,9 @@ func TestL1InfoContract(t *testing.T) { ...@@ -853,9 +854,9 @@ func TestL1InfoContract(t *testing.T) {
endVerifBlockNumber := big.NewInt(4) endVerifBlockNumber := big.NewInt(4)
endSeqBlockNumber := big.NewInt(6) endSeqBlockNumber := big.NewInt(6)
endVerifBlock, err := waitForBlock(endVerifBlockNumber, l2Verif, time.Minute) endVerifBlock, err := geth.WaitForBlock(endVerifBlockNumber, l2Verif, time.Minute)
require.Nil(t, err) require.Nil(t, err)
endSeqBlock, err := waitForBlock(endSeqBlockNumber, l2Seq, time.Minute) endSeqBlock, err := geth.WaitForBlock(endSeqBlockNumber, l2Seq, time.Minute)
require.Nil(t, err) require.Nil(t, err)
seqL1Info, err := bindings.NewL1Block(cfg.L1InfoPredeployAddress, l2Seq) seqL1Info, err := bindings.NewL1Block(cfg.L1InfoPredeployAddress, l2Seq)
...@@ -1252,7 +1253,7 @@ func TestStopStartBatcher(t *testing.T) { ...@@ -1252,7 +1253,7 @@ func TestStopStartBatcher(t *testing.T) {
// wait until the block the tx was first included in shows up in the safe chain on the verifier // wait until the block the tx was first included in shows up in the safe chain on the verifier
safeBlockInclusionDuration := time.Duration(6*cfg.DeployConfig.L1BlockTime) * time.Second safeBlockInclusionDuration := time.Duration(6*cfg.DeployConfig.L1BlockTime) * time.Second
_, err = waitForBlock(receipt.BlockNumber, l2Verif, safeBlockInclusionDuration) _, err = geth.WaitForBlock(receipt.BlockNumber, l2Verif, safeBlockInclusionDuration)
require.Nil(t, err, "Waiting for block on verifier") require.Nil(t, err, "Waiting for block on verifier")
// ensure the safe chain advances // ensure the safe chain advances
...@@ -1289,7 +1290,7 @@ func TestStopStartBatcher(t *testing.T) { ...@@ -1289,7 +1290,7 @@ func TestStopStartBatcher(t *testing.T) {
receipt = sendTx() receipt = sendTx()
// wait until the block the tx was first included in shows up in the safe chain on the verifier // wait until the block the tx was first included in shows up in the safe chain on the verifier
_, err = waitForBlock(receipt.BlockNumber, l2Verif, safeBlockInclusionDuration) _, err = geth.WaitForBlock(receipt.BlockNumber, l2Verif, safeBlockInclusionDuration)
require.Nil(t, err, "Waiting for block on verifier") require.Nil(t, err, "Waiting for block on verifier")
// ensure that the safe chain advances after restarting the batcher // ensure that the safe chain advances after restarting the batcher
...@@ -1311,7 +1312,7 @@ func TestBatcherMultiTx(t *testing.T) { ...@@ -1311,7 +1312,7 @@ func TestBatcherMultiTx(t *testing.T) {
l1Client := sys.Clients["l1"] l1Client := sys.Clients["l1"]
l2Seq := sys.Clients["sequencer"] l2Seq := sys.Clients["sequencer"]
_, err = waitForBlock(big.NewInt(10), l2Seq, time.Duration(cfg.DeployConfig.L2BlockTime*15)*time.Second) _, err = geth.WaitForBlock(big.NewInt(10), l2Seq, time.Duration(cfg.DeployConfig.L2BlockTime*15)*time.Second)
require.Nil(t, err, "Waiting for L2 blocks") require.Nil(t, err, "Waiting for L2 blocks")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
...@@ -1328,7 +1329,7 @@ func TestBatcherMultiTx(t *testing.T) { ...@@ -1328,7 +1329,7 @@ func TestBatcherMultiTx(t *testing.T) {
// possible additional L1 blocks will be created before the batcher starts, // possible additional L1 blocks will be created before the batcher starts,
// so we wait additional blocks. // so we wait additional blocks.
for i := int64(0); i < 10; i++ { for i := int64(0); i < 10; i++ {
block, err := waitForBlock(big.NewInt(int64(l1Number)+i), l1Client, time.Duration(cfg.DeployConfig.L1BlockTime*5)*time.Second) block, err := geth.WaitForBlock(big.NewInt(int64(l1Number)+i), l1Client, time.Duration(cfg.DeployConfig.L1BlockTime*5)*time.Second)
require.Nil(t, err, "Waiting for l1 blocks") require.Nil(t, err, "Waiting for l1 blocks")
totalTxCount += len(block.Transactions()) totalTxCount += len(block.Transactions())
......
...@@ -13,6 +13,7 @@ import ( ...@@ -13,6 +13,7 @@ import (
"github.com/ethereum-optimism/optimism/op-bindings/bindings" "github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys" "github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/geth"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait"
"github.com/ethereum-optimism/optimism/op-node/testutils/fuzzerutils" "github.com/ethereum-optimism/optimism/op-node/testutils/fuzzerutils"
"github.com/ethereum-optimism/optimism/op-node/withdrawals" "github.com/ethereum-optimism/optimism/op-node/withdrawals"
...@@ -70,11 +71,11 @@ func TestGasPriceOracleFeeUpdates(t *testing.T) { ...@@ -70,11 +71,11 @@ func TestGasPriceOracleFeeUpdates(t *testing.T) {
cancel() cancel()
require.Nil(t, err, "sending overhead update tx") require.Nil(t, err, "sending overhead update tx")
receipt, err := waitForTransaction(tx.Hash(), l1Client, txTimeoutDuration) receipt, err := geth.WaitForTransaction(tx.Hash(), l1Client, txTimeoutDuration)
require.Nil(t, err, "waiting for sysconfig set gas config update tx") require.Nil(t, err, "waiting for sysconfig set gas config update tx")
require.Equal(t, receipt.Status, types.ReceiptStatusSuccessful, "transaction failed") require.Equal(t, receipt.Status, types.ReceiptStatusSuccessful, "transaction failed")
_, err = waitForL1OriginOnL2(receipt.BlockNumber.Uint64(), l2Seq, txTimeoutDuration) _, err = geth.WaitForL1OriginOnL2(receipt.BlockNumber.Uint64(), l2Seq, txTimeoutDuration)
require.NoError(t, err, "waiting for L2 block to include the sysconfig update") require.NoError(t, err, "waiting for L2 block to include the sysconfig update")
gpoOverhead, err := gpoContract.Overhead(&bind.CallOpts{}) gpoOverhead, err := gpoContract.Overhead(&bind.CallOpts{})
...@@ -97,11 +98,11 @@ func TestGasPriceOracleFeeUpdates(t *testing.T) { ...@@ -97,11 +98,11 @@ func TestGasPriceOracleFeeUpdates(t *testing.T) {
cancel() cancel()
require.Nil(t, err, "sending overhead update tx") require.Nil(t, err, "sending overhead update tx")
receipt, err = waitForTransaction(tx.Hash(), l1Client, txTimeoutDuration) receipt, err = geth.WaitForTransaction(tx.Hash(), l1Client, txTimeoutDuration)
require.Nil(t, err, "waiting for sysconfig set gas config update tx") require.Nil(t, err, "waiting for sysconfig set gas config update tx")
require.Equal(t, receipt.Status, types.ReceiptStatusSuccessful, "transaction failed") require.Equal(t, receipt.Status, types.ReceiptStatusSuccessful, "transaction failed")
_, err = waitForL1OriginOnL2(receipt.BlockNumber.Uint64(), l2Seq, txTimeoutDuration) _, err = geth.WaitForL1OriginOnL2(receipt.BlockNumber.Uint64(), l2Seq, txTimeoutDuration)
require.NoError(t, err, "waiting for L2 block to include the sysconfig update") require.NoError(t, err, "waiting for L2 block to include the sysconfig update")
gpoOverhead, err = gpoContract.Overhead(&bind.CallOpts{}) gpoOverhead, err = gpoContract.Overhead(&bind.CallOpts{})
...@@ -504,7 +505,7 @@ func TestMixedWithdrawalValidity(t *testing.T) { ...@@ -504,7 +505,7 @@ func TestMixedWithdrawalValidity(t *testing.T) {
require.Nil(t, err, "sending initiate withdraw tx") require.Nil(t, err, "sending initiate withdraw tx")
t.Logf("Waiting for tx %s to be in sequencer", tx.Hash().Hex()) t.Logf("Waiting for tx %s to be in sequencer", tx.Hash().Hex())
receiptSeq, err := waitForTransaction(tx.Hash(), l2Seq, txTimeoutDuration) receiptSeq, err := geth.WaitForTransaction(tx.Hash(), l2Seq, txTimeoutDuration)
require.Nil(t, err, "withdrawal initiated on L2 sequencer") require.Nil(t, err, "withdrawal initiated on L2 sequencer")
require.Equal(t, receiptSeq.Status, types.ReceiptStatusSuccessful, "transaction failed") require.Equal(t, receiptSeq.Status, types.ReceiptStatusSuccessful, "transaction failed")
...@@ -513,7 +514,7 @@ func TestMixedWithdrawalValidity(t *testing.T) { ...@@ -513,7 +514,7 @@ func TestMixedWithdrawalValidity(t *testing.T) {
t.Logf("Waiting for tx %s to be in verifier. Verifier tip is %s:%d. Included in sequencer in block %s:%d", tx.Hash().Hex(), verifierTip.Hash().Hex(), verifierTip.NumberU64(), receiptSeq.BlockHash.Hex(), receiptSeq.BlockNumber) t.Logf("Waiting for tx %s to be in verifier. Verifier tip is %s:%d. Included in sequencer in block %s:%d", tx.Hash().Hex(), verifierTip.Hash().Hex(), verifierTip.NumberU64(), receiptSeq.BlockHash.Hex(), receiptSeq.BlockNumber)
// Wait for the transaction to appear in L2 verifier // Wait for the transaction to appear in L2 verifier
receipt, err := waitForTransaction(tx.Hash(), l2Verif, txTimeoutDuration) receipt, err := geth.WaitForTransaction(tx.Hash(), l2Verif, txTimeoutDuration)
require.Nilf(t, err, "withdrawal tx %s not found in verifier. included in block %s:%d", tx.Hash().Hex(), receiptSeq.BlockHash.Hex(), receiptSeq.BlockNumber) require.Nilf(t, err, "withdrawal tx %s not found in verifier. included in block %s:%d", tx.Hash().Hex(), receiptSeq.BlockHash.Hex(), receiptSeq.BlockNumber)
require.Equal(t, receipt.Status, types.ReceiptStatusSuccessful, "transaction failed") require.Equal(t, receipt.Status, types.ReceiptStatusSuccessful, "transaction failed")
...@@ -638,7 +639,7 @@ func TestMixedWithdrawalValidity(t *testing.T) { ...@@ -638,7 +639,7 @@ func TestMixedWithdrawalValidity(t *testing.T) {
} else { } else {
require.NoError(t, err) require.NoError(t, err)
receipt, err = waitForTransaction(tx.Hash(), l1Client, txTimeoutDuration) receipt, err = geth.WaitForTransaction(tx.Hash(), l1Client, txTimeoutDuration)
require.Nil(t, err, "finalize withdrawal") require.Nil(t, err, "finalize withdrawal")
require.Equal(t, types.ReceiptStatusSuccessful, receipt.Status) require.Equal(t, types.ReceiptStatusSuccessful, receipt.Status)
...@@ -656,7 +657,7 @@ func TestMixedWithdrawalValidity(t *testing.T) { ...@@ -656,7 +657,7 @@ func TestMixedWithdrawalValidity(t *testing.T) {
transactor.ExpectedL1Nonce++ transactor.ExpectedL1Nonce++
// Ensure that our withdrawal was proved successfully // Ensure that our withdrawal was proved successfully
proveReceipt, err := waitForTransaction(tx.Hash(), l1Client, 3*time.Duration(cfg.DeployConfig.L1BlockTime)*time.Second) proveReceipt, err := geth.WaitForTransaction(tx.Hash(), l1Client, 3*time.Duration(cfg.DeployConfig.L1BlockTime)*time.Second)
require.Nil(t, err, "prove withdrawal") require.Nil(t, err, "prove withdrawal")
require.Equal(t, types.ReceiptStatusSuccessful, proveReceipt.Status) require.Equal(t, types.ReceiptStatusSuccessful, proveReceipt.Status)
......
...@@ -8,6 +8,7 @@ import ( ...@@ -8,6 +8,7 @@ import (
"time" "time"
"github.com/ethereum-optimism/optimism/op-bindings/bindings" "github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/geth"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/transactions" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/transactions"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive" "github.com/ethereum-optimism/optimism/op-node/rollup/derive"
"github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/abi/bind"
...@@ -39,14 +40,14 @@ func SendDepositTx(t *testing.T, cfg SystemConfig, l1Client *ethclient.Client, l ...@@ -39,14 +40,14 @@ func SendDepositTx(t *testing.T, cfg SystemConfig, l1Client *ethclient.Client, l
require.Nil(t, err, "with deposit tx") require.Nil(t, err, "with deposit tx")
// Wait for transaction on L1 // Wait for transaction on L1
l1Receipt, err := waitForTransaction(tx.Hash(), l1Client, 10*time.Duration(cfg.DeployConfig.L1BlockTime)*time.Second) l1Receipt, err := geth.WaitForTransaction(tx.Hash(), l1Client, 10*time.Duration(cfg.DeployConfig.L1BlockTime)*time.Second)
require.Nil(t, err, "Waiting for deposit tx on L1") require.Nil(t, err, "Waiting for deposit tx on L1")
// Wait for transaction to be included on L2 // Wait for transaction to be included on L2
reconstructedDep, err := derive.UnmarshalDepositLogEvent(l1Receipt.Logs[0]) reconstructedDep, err := derive.UnmarshalDepositLogEvent(l1Receipt.Logs[0])
require.NoError(t, err, "Could not reconstruct L2 Deposit") require.NoError(t, err, "Could not reconstruct L2 Deposit")
tx = types.NewTx(reconstructedDep) tx = types.NewTx(reconstructedDep)
l2Receipt, err := waitForTransaction(tx.Hash(), l2Client, 10*time.Duration(cfg.DeployConfig.L2BlockTime)*time.Second) l2Receipt, err := geth.WaitForTransaction(tx.Hash(), l2Client, 10*time.Duration(cfg.DeployConfig.L2BlockTime)*time.Second)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, l2Opts.ExpectedStatus, l2Receipt.Status, "l2 transaction status") require.Equal(t, l2Opts.ExpectedStatus, l2Receipt.Status, "l2 transaction status")
return l2Receipt return l2Receipt
...@@ -95,13 +96,13 @@ func SendL2Tx(t *testing.T, cfg SystemConfig, l2Client *ethclient.Client, privKe ...@@ -95,13 +96,13 @@ func SendL2Tx(t *testing.T, cfg SystemConfig, l2Client *ethclient.Client, privKe
err := l2Client.SendTransaction(ctx, tx) err := l2Client.SendTransaction(ctx, tx)
require.NoError(t, err, "Sending L2 tx") require.NoError(t, err, "Sending L2 tx")
receipt, err := waitForTransaction(tx.Hash(), l2Client, 10*time.Duration(cfg.DeployConfig.L2BlockTime)*time.Second) receipt, err := geth.WaitForTransaction(tx.Hash(), l2Client, 10*time.Duration(cfg.DeployConfig.L2BlockTime)*time.Second)
require.NoError(t, err, "Waiting for L2 tx") require.NoError(t, err, "Waiting for L2 tx")
require.Equal(t, opts.ExpectedStatus, receipt.Status, "TX should have expected status") require.Equal(t, opts.ExpectedStatus, receipt.Status, "TX should have expected status")
for i, client := range opts.VerifyClients { for i, client := range opts.VerifyClients {
t.Logf("Waiting for tx %v on verification client %d", tx.Hash(), i) t.Logf("Waiting for tx %v on verification client %d", tx.Hash(), i)
receiptVerif, err := waitForTransaction(tx.Hash(), client, 10*time.Duration(cfg.DeployConfig.L2BlockTime)*time.Second) receiptVerif, err := geth.WaitForTransaction(tx.Hash(), client, 10*time.Duration(cfg.DeployConfig.L2BlockTime)*time.Second)
require.NoErrorf(t, err, "Waiting for L2 tx on verification client %d", i) require.NoErrorf(t, err, "Waiting for L2 tx on verification client %d", i)
require.Equalf(t, receipt, receiptVerif, "Receipts should be the same on sequencer and verification client %d", i) require.Equalf(t, receipt, receiptVerif, "Receipts should be the same on sequencer and verification client %d", i)
} }
......
...@@ -10,6 +10,7 @@ import ( ...@@ -10,6 +10,7 @@ import (
"github.com/ethereum-optimism/optimism/op-bindings/bindings" "github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys" "github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"github.com/ethereum-optimism/optimism/op-e2e/config" "github.com/ethereum-optimism/optimism/op-e2e/config"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/geth"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait"
"github.com/ethereum-optimism/optimism/op-node/withdrawals" "github.com/ethereum-optimism/optimism/op-node/withdrawals"
"github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/abi/bind"
...@@ -36,13 +37,13 @@ func SendWithdrawal(t *testing.T, cfg SystemConfig, l2Client *ethclient.Client, ...@@ -36,13 +37,13 @@ func SendWithdrawal(t *testing.T, cfg SystemConfig, l2Client *ethclient.Client,
tx, err := l2withdrawer.InitiateWithdrawal(l2opts, l2opts.From, big.NewInt(int64(opts.Gas)), opts.Data) tx, err := l2withdrawer.InitiateWithdrawal(l2opts, l2opts.From, big.NewInt(int64(opts.Gas)), opts.Data)
require.Nil(t, err, "sending initiate withdraw tx") require.Nil(t, err, "sending initiate withdraw tx")
receipt, err := waitForTransaction(tx.Hash(), l2Client, 10*time.Duration(cfg.DeployConfig.L1BlockTime)*time.Second) receipt, err := geth.WaitForTransaction(tx.Hash(), l2Client, 10*time.Duration(cfg.DeployConfig.L1BlockTime)*time.Second)
require.Nil(t, err, "withdrawal initiated on L2 sequencer") require.Nil(t, err, "withdrawal initiated on L2 sequencer")
require.Equal(t, opts.ExpectedStatus, receipt.Status, "transaction had incorrect status") require.Equal(t, opts.ExpectedStatus, receipt.Status, "transaction had incorrect status")
for i, client := range opts.VerifyClients { for i, client := range opts.VerifyClients {
t.Logf("Waiting for tx %v on verification client %d", tx.Hash(), i) t.Logf("Waiting for tx %v on verification client %d", tx.Hash(), i)
receiptVerif, err := waitForTransaction(tx.Hash(), client, 10*time.Duration(cfg.DeployConfig.L2BlockTime)*time.Second) receiptVerif, err := geth.WaitForTransaction(tx.Hash(), client, 10*time.Duration(cfg.DeployConfig.L2BlockTime)*time.Second)
require.Nilf(t, err, "Waiting for L2 tx on verification client %d", i) require.Nilf(t, err, "Waiting for L2 tx on verification client %d", i)
require.Equalf(t, receipt, receiptVerif, "Receipts should be the same on sequencer and verification client %d", i) require.Equalf(t, receipt, receiptVerif, "Receipts should be the same on sequencer and verification client %d", i)
} }
...@@ -134,7 +135,7 @@ func ProveWithdrawal(t *testing.T, cfg SystemConfig, l1Client *ethclient.Client, ...@@ -134,7 +135,7 @@ func ProveWithdrawal(t *testing.T, cfg SystemConfig, l1Client *ethclient.Client,
require.Nil(t, err) require.Nil(t, err)
// Ensure that our withdrawal was proved successfully // Ensure that our withdrawal was proved successfully
proveReceipt, err := waitForTransaction(tx.Hash(), l1Client, 3*time.Duration(cfg.DeployConfig.L1BlockTime)*time.Second) proveReceipt, err := geth.WaitForTransaction(tx.Hash(), l1Client, 3*time.Duration(cfg.DeployConfig.L1BlockTime)*time.Second)
require.Nil(t, err, "prove withdrawal") require.Nil(t, err, "prove withdrawal")
require.Equal(t, types.ReceiptStatusSuccessful, proveReceipt.Status) require.Equal(t, types.ReceiptStatusSuccessful, proveReceipt.Status)
return params, proveReceipt return params, proveReceipt
...@@ -167,7 +168,7 @@ func FinalizeWithdrawal(t *testing.T, cfg SystemConfig, l1Client *ethclient.Clie ...@@ -167,7 +168,7 @@ func FinalizeWithdrawal(t *testing.T, cfg SystemConfig, l1Client *ethclient.Clie
require.Nil(t, err) require.Nil(t, err)
// Ensure that our withdrawal was finalized successfully // Ensure that our withdrawal was finalized successfully
finalizeReceipt, err := waitForTransaction(tx.Hash(), l1Client, 3*time.Duration(cfg.DeployConfig.L1BlockTime)*time.Second) finalizeReceipt, err := geth.WaitForTransaction(tx.Hash(), l1Client, 3*time.Duration(cfg.DeployConfig.L1BlockTime)*time.Second)
require.Nil(t, err, "finalize withdrawal") require.Nil(t, err, "finalize withdrawal")
require.Equal(t, types.ReceiptStatusSuccessful, finalizeReceipt.Status) require.Equal(t, types.ReceiptStatusSuccessful, finalizeReceipt.Status)
return finalizeReceipt return finalizeReceipt
......
...@@ -3,8 +3,8 @@ module github.com/ethereum-optimism/optimism/op-exporter ...@@ -3,8 +3,8 @@ module github.com/ethereum-optimism/optimism/op-exporter
go 1.20 go 1.20
require ( require (
github.com/ethereum/go-ethereum v1.10.17 github.com/ethereum/go-ethereum v1.12.1
github.com/prometheus/client_golang v1.11.1 github.com/prometheus/client_golang v1.14.0
github.com/sirupsen/logrus v1.7.0 github.com/sirupsen/logrus v1.7.0
github.com/ybbus/jsonrpc v2.1.2+incompatible github.com/ybbus/jsonrpc v2.1.2+incompatible
gopkg.in/alecthomas/kingpin.v2 v2.2.6 gopkg.in/alecthomas/kingpin.v2 v2.2.6
...@@ -16,30 +16,30 @@ require ( ...@@ -16,30 +16,30 @@ require (
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-logr/logr v0.4.0 // indirect github.com/go-logr/logr v0.4.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.8 // indirect github.com/google/go-cmp v0.5.9 // indirect
github.com/google/gofuzz v1.1.1-0.20200604201612-c04b05f3adfa // indirect github.com/google/gofuzz v1.1.1-0.20200604201612-c04b05f3adfa // indirect
github.com/googleapis/gnostic v0.4.1 // indirect github.com/googleapis/gnostic v0.4.1 // indirect
github.com/json-iterator/go v1.1.11 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/onsi/gomega v1.16.0 // indirect github.com/onsi/gomega v1.16.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.30.0 // indirect github.com/prometheus/common v0.39.0 // indirect
github.com/prometheus/procfs v0.7.3 // indirect github.com/prometheus/procfs v0.9.0 // indirect
golang.org/x/net v0.7.0 // indirect golang.org/x/net v0.10.0 // indirect
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c // indirect golang.org/x/oauth2 v0.3.0 // indirect
golang.org/x/sys v0.5.0 // indirect golang.org/x/sys v0.9.0 // indirect
golang.org/x/term v0.5.0 // indirect golang.org/x/term v0.8.0 // indirect
golang.org/x/text v0.7.0 // indirect golang.org/x/text v0.9.0 // indirect
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 // indirect golang.org/x/time v0.3.0 // indirect
google.golang.org/appengine v1.6.6 // indirect google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.27.1 // indirect google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/api v0.21.2 // indirect k8s.io/api v0.21.2 // indirect
......
This diff is collapsed.
This diff is collapsed.
...@@ -43,7 +43,7 @@ ...@@ -43,7 +43,7 @@
"eip1559Elasticity": 6, "eip1559Elasticity": 6,
"l1GenesisBlockTimestamp": "0x64c811bf", "l1GenesisBlockTimestamp": "0x64c811bf",
"l2GenesisRegolithTimeOffset": "0x0", "l2GenesisRegolithTimeOffset": "0x0",
"faultGameAbsolutePrestate": "0x41c7ae758795765c6664a5d39bf63841c71ff191e9189522bad8ebff5d4eca98", "faultGameAbsolutePrestate": "0x03c7ae758795765c6664a5d39bf63841c71ff191e9189522bad8ebff5d4eca98",
"faultGameMaxDepth": 30, "faultGameMaxDepth": 30,
"faultGameMaxDuration": 1200, "faultGameMaxDuration": 1200,
"systemConfigStartBlock": 0 "systemConfigStartBlock": 0
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
"src/L2/L2StandardBridge.sol": "0xe025dcccbf21d48828ecf588941c9ba04c91b87bdd177a653d3f1b265b0b02a8", "src/L2/L2StandardBridge.sol": "0xe025dcccbf21d48828ecf588941c9ba04c91b87bdd177a653d3f1b265b0b02a8",
"src/L2/L2ToL1MessagePasser.sol": "0xda56ba2e5b2c28fa8ca2df24077d49e96155a00ecc99cd0778d681be6ed166fe", "src/L2/L2ToL1MessagePasser.sol": "0xda56ba2e5b2c28fa8ca2df24077d49e96155a00ecc99cd0778d681be6ed166fe",
"src/L2/SequencerFeeVault.sol": "0x37816035c992d38cf7e3d5a1846b02d017dd7bdca46abe6e5c5171b9ee6225ab", "src/L2/SequencerFeeVault.sol": "0x37816035c992d38cf7e3d5a1846b02d017dd7bdca46abe6e5c5171b9ee6225ab",
"src/dispute/FaultDisputeGame.sol": "0x72c917e8513d17f274753a391bdbddc1f4daeca1a392f79492df29a1107c3525", "src/dispute/FaultDisputeGame.sol": "0x7b8462c29d003e96a73491c644001e1a9034bcc45c5be2a7bac3caf80d521635",
"src/legacy/DeployerWhitelist.sol": "0xf2129ec3da75307ba8e21bc943c332bb04704642e6e263149b5c8ee92dbcb7a8", "src/legacy/DeployerWhitelist.sol": "0xf2129ec3da75307ba8e21bc943c332bb04704642e6e263149b5c8ee92dbcb7a8",
"src/legacy/L1BlockNumber.sol": "0x30aae1fc85103476af0226b6e98c71c01feebbdc35d93401390b1ad438a37be6", "src/legacy/L1BlockNumber.sol": "0x30aae1fc85103476af0226b6e98c71c01feebbdc35d93401390b1ad438a37be6",
"src/legacy/LegacyMessagePasser.sol": "0x5c08b0a663cc49d30e4e38540f6aefab19ef287c3ecd31c8d8c3decd5f5bd497", "src/legacy/LegacyMessagePasser.sol": "0x5c08b0a663cc49d30e4e38540f6aefab19ef287c3ecd31c8d8c3decd5f5bd497",
......
...@@ -103,7 +103,9 @@ contract MIPS { ...@@ -103,7 +103,9 @@ contract MIPS {
from, to := copyMem(from, to, 4) // lo from, to := copyMem(from, to, 4) // lo
from, to := copyMem(from, to, 4) // hi from, to := copyMem(from, to, 4) // hi
from, to := copyMem(from, to, 4) // heap from, to := copyMem(from, to, 4) // heap
let exitCode := mload(from)
from, to := copyMem(from, to, 1) // exitCode from, to := copyMem(from, to, 1) // exitCode
let exited := mload(from)
from, to := copyMem(from, to, 1) // exited from, to := copyMem(from, to, 1) // exited
from, to := copyMem(from, to, 8) // step from, to := copyMem(from, to, 8) // step
from := add(from, 32) // offset to registers from := add(from, 32) // offset to registers
...@@ -117,8 +119,24 @@ contract MIPS { ...@@ -117,8 +119,24 @@ contract MIPS {
// Log the resulting MIPS state, for debugging // Log the resulting MIPS state, for debugging
log0(start, sub(to, start)) log0(start, sub(to, start))
// Compute the hash of the resulting MIPS state // Determine the VM status
let status := 0
switch exited
case 1 {
switch exitCode
// VMStatusValid
case 0 { status := 0 }
// VMStatusInvalid
case 1 { status := 1 }
// VMStatusPanic
default { status := 2 }
}
// VMStatusUnfinished
default { status := 3 }
// Compute the hash of the resulting MIPS state and set the status byte
out_ := keccak256(start, sub(to, start)) out_ := keccak256(start, sub(to, start))
out_ := or(and(not(shl(248, 0xFF)), out_), shl(248, status))
} }
} }
......
...@@ -85,7 +85,7 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver { ...@@ -85,7 +85,7 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver {
/// @param _blockOracle The block oracle, used for loading block hashes further back /// @param _blockOracle The block oracle, used for loading block hashes further back
/// than the `BLOCKHASH` opcode allows as well as their estimated /// than the `BLOCKHASH` opcode allows as well as their estimated
/// timestamps. /// timestamps.
/// @custom:semver 0.0.7 /// @custom:semver 0.0.9
constructor( constructor(
GameType _gameType, GameType _gameType,
Claim _absolutePrestate, Claim _absolutePrestate,
...@@ -95,7 +95,7 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver { ...@@ -95,7 +95,7 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver {
L2OutputOracle _l2oo, L2OutputOracle _l2oo,
BlockOracle _blockOracle BlockOracle _blockOracle
) )
Semver(0, 0, 8) Semver(0, 0, 9)
{ {
GAME_TYPE = _gameType; GAME_TYPE = _gameType;
ABSOLUTE_PRESTATE = _absolutePrestate; ABSOLUTE_PRESTATE = _absolutePrestate;
...@@ -149,7 +149,11 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver { ...@@ -149,7 +149,11 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver {
// INVARIANT: The prestate is always invalid if the passed `_stateData` is not the // INVARIANT: The prestate is always invalid if the passed `_stateData` is not the
// preimage of the prestate claim hash. // preimage of the prestate claim hash.
if (keccak256(_stateData) != Claim.unwrap(preStateClaim)) revert InvalidPrestate(); // We ignore the highest order byte of the digest because it is used to
// indicate the VM Status and is added after the digest is computed.
if (keccak256(_stateData) << 8 != Claim.unwrap(preStateClaim) << 8) {
revert InvalidPrestate();
}
// INVARIANT: If a step is an attack, the poststate is valid if the step produces // INVARIANT: If a step is an attack, the poststate is valid if the step produces
// the same poststate hash as the parent claim's value. // the same poststate hash as the parent claim's value.
...@@ -434,9 +438,18 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver { ...@@ -434,9 +438,18 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver {
function initialize() external { function initialize() external {
// SAFETY: Any revert in this function will bubble up to the DisputeGameFactory and // SAFETY: Any revert in this function will bubble up to the DisputeGameFactory and
// prevent the game from being created. // prevent the game from being created.
//
// Implicit assumptions: // Implicit assumptions:
// - The `gameStatus` state variable defaults to 0, which is `GameStatus.IN_PROGRESS` // - The `gameStatus` state variable defaults to 0, which is `GameStatus.IN_PROGRESS`
// The VMStatus must indicate (1) 'invalid', to argue that disputed thing is invalid.
// Games that agree with the existing outcome are not allowed.
// NOTE(clabby): This assumption will change in Alpha Chad.
uint8 vmStatus = uint8(Claim.unwrap(rootClaim())[0]);
if (!(vmStatus == VMStatus.unwrap(VMStatuses.INVALID) || vmStatus == VMStatus.unwrap(VMStatuses.PANIC))) {
revert UnexpectedRootClaim(rootClaim());
}
// Set the game's starting timestamp // Set the game's starting timestamp
createdAt = Timestamp.wrap(uint64(block.timestamp)); createdAt = Timestamp.wrap(uint64(block.timestamp));
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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