Commit 2ab81cef authored by Adrian Sutton's avatar Adrian Sutton

Merge branch 'develop' into cleanup-devnet-modified-file

parents 208731a6 8d8597a2
...@@ -1142,7 +1142,7 @@ workflows: ...@@ -1142,7 +1142,7 @@ workflows:
working_directory: proxyd working_directory: proxyd
- indexer-tests - indexer-tests
- go-lint-test-build: - go-lint-test-build:
name: op-heartbeat tests name: op-heartbeat-tests
binary_name: op-heartbeat binary_name: op-heartbeat
working_directory: op-heartbeat working_directory: op-heartbeat
- semgrep-scan - semgrep-scan
...@@ -1224,7 +1224,11 @@ workflows: ...@@ -1224,7 +1224,11 @@ workflows:
target: test-external-geth target: test-external-geth
- bedrock-go-tests: - bedrock-go-tests:
requires: requires:
- go-mod-tidy
- cannon-build-test-vectors
- cannon-go-lint-and-test - cannon-go-lint-and-test
- check-generated-mocks-op-node
- check-generated-mocks-op-service
- op-batcher-lint - op-batcher-lint
- op-bootnode-lint - op-bootnode-lint
- op-bindings-lint - op-bindings-lint
...@@ -1238,6 +1242,7 @@ workflows: ...@@ -1238,6 +1242,7 @@ workflows:
- op-batcher-tests - op-batcher-tests
- op-bindings-tests - op-bindings-tests
- op-chain-ops-tests - op-chain-ops-tests
- op-heartbeat-tests
- op-node-tests - op-node-tests
- op-proposer-tests - op-proposer-tests
- op-challenger-tests - op-challenger-tests
...@@ -1245,6 +1250,7 @@ workflows: ...@@ -1245,6 +1250,7 @@ workflows:
- op-service-tests - op-service-tests
- op-e2e-WS-tests - op-e2e-WS-tests
- op-e2e-HTTP-tests - op-e2e-HTTP-tests
- op-e2e-ext-geth-tests
- docker-build: - docker-build:
name: op-node-docker-build name: op-node-docker-build
docker_file: op-node/Dockerfile docker_file: op-node/Dockerfile
......
...@@ -57,6 +57,7 @@ Refer to the Directory Structure section below to understand which packages are ...@@ -57,6 +57,7 @@ Refer to the Directory Structure section below to understand which packages are
├── <a href="./op-exporter">op-exporter</a>: Prometheus exporter client ├── <a href="./op-exporter">op-exporter</a>: Prometheus exporter client
├── <a href="./op-heartbeat">op-heartbeat</a>: Heartbeat monitor service ├── <a href="./op-heartbeat">op-heartbeat</a>: Heartbeat monitor service
├── <a href="./op-node">op-node</a>: rollup consensus-layer client ├── <a href="./op-node">op-node</a>: rollup consensus-layer client
├── <a href="./op-preimage">op-preimage</a>: Go bindings for Preimage Oracle
├── <a href="./op-program">op-program</a>: Fault proof program ├── <a href="./op-program">op-program</a>: Fault proof program
├── <a href="./op-proposer">op-proposer</a>: L2-Output Submitter, submits proposals to L1 ├── <a href="./op-proposer">op-proposer</a>: L2-Output Submitter, submits proposals to L1
├── <a href="./op-service">op-service</a>: Common codebase utilities ├── <a href="./op-service">op-service</a>: Common codebase utilities
......
...@@ -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")
......
comment: comment: false
layout: "reach, diff, flags, files"
behavior: default
require_changes: true # only post the comment if coverage changes
ignore: ignore:
- "op-e2e" - "op-e2e"
- "**/*.t.sol" - "**/*.t.sol"
......
...@@ -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>...
```
...@@ -2,18 +2,21 @@ FROM golang:1.20.7-alpine3.18 as builder ...@@ -2,18 +2,21 @@ FROM golang:1.20.7-alpine3.18 as builder
RUN apk --no-cache add make jq bash git alpine-sdk RUN apk --no-cache add make jq bash git alpine-sdk
COPY ./go.mod /app/go.mod
COPY ./go.sum /app/go.sum
WORKDIR /app
RUN go mod download
COPY ./endpoint-monitor /app/endpoint-monitor COPY ./endpoint-monitor /app/endpoint-monitor
COPY ./op-service /app/op-service COPY ./op-service /app/op-service
COPY ./op-node /app/op-node COPY ./op-node /app/op-node
COPY ./go.mod /app/go.mod
COPY ./go.sum /app/go.sum
COPY ./.git /app/.git COPY ./.git /app/.git
WORKDIR /app/endpoint-monitor WORKDIR /app/endpoint-monitor
RUN go mod download
RUN make build RUN make build
FROM alpine:3.18 FROM alpine:3.18
......
...@@ -2,20 +2,21 @@ FROM --platform=$BUILDPLATFORM golang:1.20.7-alpine3.18 as builder ...@@ -2,20 +2,21 @@ FROM --platform=$BUILDPLATFORM golang:1.20.7-alpine3.18 as builder
RUN apk add --no-cache make gcc musl-dev linux-headers git jq bash RUN apk add --no-cache make gcc musl-dev linux-headers git jq bash
COPY ./go.mod /app/go.mod
COPY ./go.sum /app/go.sum
WORKDIR /app
RUN go mod download
# build indexer with the shared go.mod & go.sum files # build indexer with the shared go.mod & go.sum files
COPY ./indexer /app/indexer COPY ./indexer /app/indexer
COPY ./op-bindings /app/op-bindings COPY ./op-bindings /app/op-bindings
COPY ./op-service /app/op-service COPY ./op-service /app/op-service
COPY ./op-node /app/op-node COPY ./op-node /app/op-node
COPY ./go.mod /app/go.mod
COPY ./go.sum /app/go.sum
COPY ./.git /app/.git
WORKDIR /app/indexer WORKDIR /app/indexer
RUN go mod download
RUN make indexer RUN make indexer
FROM alpine:3.18 FROM alpine:3.18
......
...@@ -33,3 +33,28 @@ TODO add indexer to the optimism devnet compose file (previously removed for bre ...@@ -33,3 +33,28 @@ TODO add indexer to the optimism devnet compose file (previously removed for bre
`docker-compose.dev.yml` is git ignored. Fill in your own docker-compose file here. `docker-compose.dev.yml` is git ignored. Fill in your own docker-compose file here.
## Architecture
![Architectural Diagram](./assets/architecture.png)
The indexer application supports two separate services for collective operation:
**Indexer API** - Provides a lightweight API service that supports paginated lookups for bridge events.
**Indexer Service** - A polling based service that constantly reads and persists OP Stack chain data (i.e, block meta, system contract events, synchronized bridge events) from a L1 and L2 chain.
### Indexer API
TBD
### Indexer Service
![Service Component Diagram](./assets/indexer-service.png)
The indexer service is responsible for polling and processing real-time batches of L1 and L2 chain data. The indexer service is currently composed of the following key components:
- **Poller Routines** - Individually polls the L1/L2 chain for new blocks and OP Stack system contract events.
- **Insertion Routines** - Awaits new batches from the poller routines and inserts them into the database upon retrieval.
- **Bridge Routine** - Polls the database directly for new L1 blocks and bridge events. Upon retrieval, the bridge routine will:
* Process and persist new bridge events
* Synchronize L1 proven/finalized withdrawals with their L2 initialization counterparts
### Database
The indexer service currently supports a Postgres database for storing L1/L2 OP Stack chain data. The most up-to-date database schemas can be found in the `./migrations` directory.
**NOTE:** The indexer service implementation currently does not natively support database migration. Because of this a database must be manually updated to ensure forward compatibility with the latest indexer service implementation.
\ No newline at end of file
...@@ -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)
......
...@@ -38,7 +38,7 @@ func runIndexer(ctx *cli.Context) error { ...@@ -38,7 +38,7 @@ func runIndexer(ctx *cli.Context) error {
} }
defer db.Close() defer db.Close()
indexer, err := indexer.NewIndexer(log, db, cfg.Chain, cfg.RPCs, cfg.Metrics) indexer, err := indexer.NewIndexer(log, db, cfg.Chain, cfg.RPCs, cfg.HTTPServer, cfg.MetricsServer)
if err != nil { if err != nil {
log.Error("failed to create indexer", "err", err) log.Error("failed to create indexer", "err", err)
return err return 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.API.Port) return api.Start(ctx.Context)
} }
func runAll(ctx *cli.Context) error { func runAll(ctx *cli.Context) error {
......
...@@ -2,7 +2,6 @@ package config ...@@ -2,7 +2,6 @@ package config
import ( import (
"fmt" "fmt"
"math/big"
"os" "os"
"reflect" "reflect"
...@@ -21,11 +20,11 @@ const ( ...@@ -21,11 +20,11 @@ const (
// Config represents the `indexer.toml` file used to configure the indexer // Config represents the `indexer.toml` file used to configure the indexer
type Config struct { type Config struct {
Chain ChainConfig `toml:"chain"` Chain ChainConfig `toml:"chain"`
RPCs RPCsConfig `toml:"rpcs"` RPCs RPCsConfig `toml:"rpcs"`
DB DBConfig `toml:"db"` DB DBConfig `toml:"db"`
API APIConfig `toml:"api"` HTTPServer ServerConfig `toml:"http"`
Metrics MetricsConfig `toml:"metrics"` MetricsServer ServerConfig `toml:"metrics"`
} }
// fetch this via onchain config from RPCsConfig and remove from config in future // fetch this via onchain config from RPCsConfig and remove from config in future
...@@ -65,10 +64,15 @@ func (c *L1Contracts) AsSlice() ([]common.Address, error) { ...@@ -65,10 +64,15 @@ func (c *L1Contracts) AsSlice() ([]common.Address, error) {
// ChainConfig configures of the chain being indexed // ChainConfig configures of the chain being indexed
type ChainConfig struct { type ChainConfig struct {
// Configure known chains with the l2 chain id // Configure known chains with the l2 chain id
Preset int Preset int
L1Contracts L1Contracts `toml:"l1-contracts"`
// L1StartingHeight is the block height to start indexing from L1Contracts L1Contracts `toml:"l1-contracts"`
L1StartingHeight uint `toml:"l1-starting-height"` L1StartingHeight uint `toml:"l1-starting-height"`
// These configuration options will be removed once
// native reorg handling is implemented
L1ConfirmationDepth uint `toml:"l1-confirmation-depth"`
L2ConfirmationDepth uint `toml:"l2-confirmation-depth"`
L1PollingInterval uint `toml:"l1-polling-interval"` L1PollingInterval uint `toml:"l1-polling-interval"`
L2PollingInterval uint `toml:"l2-polling-interval"` L2PollingInterval uint `toml:"l2-polling-interval"`
...@@ -77,11 +81,6 @@ type ChainConfig struct { ...@@ -77,11 +81,6 @@ type ChainConfig struct {
L2HeaderBufferSize uint `toml:"l2-header-buffer-size"` L2HeaderBufferSize uint `toml:"l2-header-buffer-size"`
} }
// L1StartHeight returns the block height to start indexing from
func (cc *ChainConfig) L1StartHeight() *big.Int {
return big.NewInt(int64(cc.L1StartingHeight))
}
// RPCsConfig configures the RPC urls // RPCsConfig configures the RPC urls
type RPCsConfig struct { type RPCsConfig struct {
L1RPC string `toml:"l1-rpc"` L1RPC string `toml:"l1-rpc"`
...@@ -97,14 +96,8 @@ type DBConfig struct { ...@@ -97,14 +96,8 @@ type DBConfig struct {
Password string `toml:"password"` Password string `toml:"password"`
} }
// APIConfig configures the API server // Configures the a server
type APIConfig struct { type ServerConfig struct {
Host string `toml:"host"`
Port int `toml:"port"`
}
// MetricsConfig configures the metrics server
type MetricsConfig struct {
Host string `toml:"host"` Host string `toml:"host"`
Port int `toml:"port"` Port int `toml:"port"`
} }
......
...@@ -31,9 +31,9 @@ func TestLoadConfig(t *testing.T) { ...@@ -31,9 +31,9 @@ func TestLoadConfig(t *testing.T) {
port = 5432 port = 5432
user = "postgres" user = "postgres"
password = "postgres" password = "postgres"
name = "indexer" name = "indexer"
[api] [http]
host = "127.0.0.1" host = "127.0.0.1"
port = 8080 port = 8080
...@@ -65,10 +65,10 @@ func TestLoadConfig(t *testing.T) { ...@@ -65,10 +65,10 @@ func TestLoadConfig(t *testing.T) {
require.Equal(t, conf.DB.User, "postgres") require.Equal(t, conf.DB.User, "postgres")
require.Equal(t, conf.DB.Password, "postgres") require.Equal(t, conf.DB.Password, "postgres")
require.Equal(t, conf.DB.Name, "indexer") require.Equal(t, conf.DB.Name, "indexer")
require.Equal(t, conf.API.Host, "127.0.0.1") require.Equal(t, conf.HTTPServer.Host, "127.0.0.1")
require.Equal(t, conf.API.Port, 8080) require.Equal(t, conf.HTTPServer.Port, 8080)
require.Equal(t, conf.Metrics.Host, "127.0.0.1") require.Equal(t, conf.MetricsServer.Host, "127.0.0.1")
require.Equal(t, conf.Metrics.Port, 7300) require.Equal(t, conf.MetricsServer.Port, 7300)
} }
func TestLoadConfig_WithoutPreset(t *testing.T) { func TestLoadConfig_WithoutPreset(t *testing.T) {
......
...@@ -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
}
...@@ -2,6 +2,7 @@ package database ...@@ -2,6 +2,7 @@ package database
import ( import (
"errors" "errors"
"fmt"
"gorm.io/gorm" "gorm.io/gorm"
...@@ -133,21 +134,31 @@ func (db *bridgeTransfersDB) L1BridgeDepositsByAddress(address common.Address, c ...@@ -133,21 +134,31 @@ func (db *bridgeTransfersDB) L1BridgeDepositsByAddress(address common.Address, c
limit = defaultLimit limit = defaultLimit
} }
cursorClause := ""
if cursor != "" {
sourceHash := common.HexToHash(cursor)
txDeposit := new(L1TransactionDeposit)
result := db.gorm.Model(&L1TransactionDeposit{}).Where(&L1TransactionDeposit{SourceHash: sourceHash}).Take(txDeposit)
if result.Error != nil || errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("unable to find transaction with supplied cursor source hash %s: %w", sourceHash, result.Error)
}
cursorClause = fmt.Sprintf("l1_transaction_deposits.timestamp <= %d", txDeposit.Tx.Timestamp)
}
// TODO join with l1_bridged_tokens and l2_bridged_tokens // TODO join with l1_bridged_tokens and l2_bridged_tokens
ethAddressString := predeploys.LegacyERC20ETHAddr.String() ethAddressString := predeploys.LegacyERC20ETHAddr.String()
// Coalesce l1 transaction deposits that are simply ETH sends // Coalesce l1 transaction deposits that are simply ETH sends
ethTransactionDeposits := db.gorm.Model(&L1TransactionDeposit{}) ethTransactionDeposits := db.gorm.Model(&L1TransactionDeposit{})
ethTransactionDeposits = ethTransactionDeposits.Where(Transaction{FromAddress: address}).Where(`data = '0x' AND amount > 0`) ethTransactionDeposits = ethTransactionDeposits.Where(Transaction{FromAddress: address}).Where("data = '0x' AND amount > 0")
ethTransactionDeposits = ethTransactionDeposits.Joins("INNER JOIN l1_contract_events ON l1_contract_events.guid = initiated_l1_event_guid") ethTransactionDeposits = ethTransactionDeposits.Joins("INNER JOIN l1_contract_events ON l1_contract_events.guid = initiated_l1_event_guid")
ethTransactionDeposits = ethTransactionDeposits.Select(` ethTransactionDeposits = ethTransactionDeposits.Select(`
from_address, to_address, amount, data, source_hash AS transaction_source_hash, from_address, to_address, amount, data, source_hash AS transaction_source_hash,
l2_transaction_hash, l1_contract_events.transaction_hash AS l1_transaction_hash, l2_transaction_hash, l1_contract_events.transaction_hash AS l1_transaction_hash,
l1_transaction_deposits.timestamp, NULL AS cross_domain_message_hash, ? AS local_token_address, ? AS remote_token_address`, ethAddressString, ethAddressString) l1_transaction_deposits.timestamp, NULL AS cross_domain_message_hash, ? AS local_token_address, ? AS remote_token_address`, ethAddressString, ethAddressString)
ethTransactionDeposits = ethTransactionDeposits.Order("timestamp DESC").Limit(limit + 1)
if cursor != "" { if cursorClause != "" {
// Probably need to fix this and compare timestamps ethTransactionDeposits = ethTransactionDeposits.Where(cursorClause)
ethTransactionDeposits = ethTransactionDeposits.Where("source_hash < ?", cursor)
} }
depositsQuery := db.gorm.Model(&L1BridgeDeposit{}) depositsQuery := db.gorm.Model(&L1BridgeDeposit{})
...@@ -157,10 +168,9 @@ l1_transaction_deposits.timestamp, NULL AS cross_domain_message_hash, ? AS local ...@@ -157,10 +168,9 @@ l1_transaction_deposits.timestamp, NULL AS cross_domain_message_hash, ? AS local
l1_bridge_deposits.from_address, l1_bridge_deposits.to_address, l1_bridge_deposits.amount, l1_bridge_deposits.data, transaction_source_hash, l1_bridge_deposits.from_address, l1_bridge_deposits.to_address, l1_bridge_deposits.amount, l1_bridge_deposits.data, transaction_source_hash,
l2_transaction_hash, l1_contract_events.transaction_hash AS l1_transaction_hash, l2_transaction_hash, l1_contract_events.transaction_hash AS l1_transaction_hash,
l1_bridge_deposits.timestamp, cross_domain_message_hash, local_token_address, remote_token_address`) l1_bridge_deposits.timestamp, cross_domain_message_hash, local_token_address, remote_token_address`)
depositsQuery = depositsQuery.Order("timestamp DESC").Limit(limit + 1)
if cursor != "" { if cursorClause != "" {
// Probably need to fix this and compare timestamps depositsQuery = depositsQuery.Where(cursorClause)
depositsQuery = depositsQuery.Where("source_hash < ?", cursor)
} }
query := db.gorm.Table("(?) AS deposits", depositsQuery) query := db.gorm.Table("(?) AS deposits", depositsQuery)
...@@ -179,16 +189,11 @@ l1_bridge_deposits.timestamp, cross_domain_message_hash, local_token_address, re ...@@ -179,16 +189,11 @@ l1_bridge_deposits.timestamp, cross_domain_message_hash, local_token_address, re
hasNextPage := false hasNextPage := false
if len(deposits) > limit { if len(deposits) > limit {
hasNextPage = true hasNextPage = true
nextCursor = deposits[limit].L1BridgeDeposit.TransactionSourceHash.String()
deposits = deposits[:limit] deposits = deposits[:limit]
nextCursor = deposits[limit].L1TransactionHash.String()
}
response := &L1BridgeDepositsResponse{
Deposits: deposits,
Cursor: nextCursor,
HasNextPage: hasNextPage,
} }
response := &L1BridgeDepositsResponse{Deposits: deposits, Cursor: nextCursor, HasNextPage: hasNextPage}
return response, nil return response, nil
} }
...@@ -242,6 +247,17 @@ func (db *bridgeTransfersDB) L2BridgeWithdrawalsByAddress(address common.Address ...@@ -242,6 +247,17 @@ func (db *bridgeTransfersDB) L2BridgeWithdrawalsByAddress(address common.Address
limit = defaultLimit limit = defaultLimit
} }
cursorClause := ""
if cursor != "" {
withdrawalHash := common.HexToHash(cursor)
var txWithdrawal L2TransactionWithdrawal
result := db.gorm.Model(&L2TransactionWithdrawal{}).Where(&L2TransactionWithdrawal{WithdrawalHash: withdrawalHash}).Take(&txWithdrawal)
if result.Error != nil || errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("unable to find transaction with supplied cursor withdrawal hash %s: %w", withdrawalHash, result.Error)
}
cursorClause = fmt.Sprintf("l2_transaction_withdrawals.timestamp <= %d", txWithdrawal.Tx.Timestamp)
}
// TODO join with l1_bridged_tokens and l2_bridged_tokens // TODO join with l1_bridged_tokens and l2_bridged_tokens
ethAddressString := predeploys.LegacyERC20ETHAddr.String() ethAddressString := predeploys.LegacyERC20ETHAddr.String()
...@@ -255,10 +271,9 @@ func (db *bridgeTransfersDB) L2BridgeWithdrawalsByAddress(address common.Address ...@@ -255,10 +271,9 @@ func (db *bridgeTransfersDB) L2BridgeWithdrawalsByAddress(address common.Address
from_address, to_address, amount, data, withdrawal_hash AS transaction_withdrawal_hash, from_address, to_address, amount, data, withdrawal_hash AS transaction_withdrawal_hash,
l2_contract_events.transaction_hash AS l2_transaction_hash, proven_l1_events.transaction_hash AS proven_l1_transaction_hash, finalized_l1_events.transaction_hash AS finalized_l1_transaction_hash, l2_contract_events.transaction_hash AS l2_transaction_hash, proven_l1_events.transaction_hash AS proven_l1_transaction_hash, finalized_l1_events.transaction_hash AS finalized_l1_transaction_hash,
l2_transaction_withdrawals.timestamp, NULL AS cross_domain_message_hash, ? AS local_token_address, ? AS remote_token_address`, ethAddressString, ethAddressString) l2_transaction_withdrawals.timestamp, NULL AS cross_domain_message_hash, ? AS local_token_address, ? AS remote_token_address`, ethAddressString, ethAddressString)
ethTransactionWithdrawals = ethTransactionWithdrawals.Order("timestamp DESC").Limit(limit + 1)
if cursor != "" { if cursorClause != "" {
// Probably need to fix this and compare timestamps ethTransactionWithdrawals = ethTransactionWithdrawals.Where(cursorClause)
ethTransactionWithdrawals = ethTransactionWithdrawals.Where("withdrawal_hash < ?", cursor)
} }
withdrawalsQuery := db.gorm.Model(&L2BridgeWithdrawal{}) withdrawalsQuery := db.gorm.Model(&L2BridgeWithdrawal{})
...@@ -270,17 +285,16 @@ l2_transaction_withdrawals.timestamp, NULL AS cross_domain_message_hash, ? AS lo ...@@ -270,17 +285,16 @@ l2_transaction_withdrawals.timestamp, NULL AS cross_domain_message_hash, ? AS lo
l2_bridge_withdrawals.from_address, l2_bridge_withdrawals.to_address, l2_bridge_withdrawals.amount, l2_bridge_withdrawals.data, transaction_withdrawal_hash, l2_bridge_withdrawals.from_address, l2_bridge_withdrawals.to_address, l2_bridge_withdrawals.amount, l2_bridge_withdrawals.data, transaction_withdrawal_hash,
l2_contract_events.transaction_hash AS l2_transaction_hash, proven_l1_events.transaction_hash AS proven_l1_transaction_hash, finalized_l1_events.transaction_hash AS finalized_l1_transaction_hash, l2_contract_events.transaction_hash AS l2_transaction_hash, proven_l1_events.transaction_hash AS proven_l1_transaction_hash, finalized_l1_events.transaction_hash AS finalized_l1_transaction_hash,
l2_bridge_withdrawals.timestamp, cross_domain_message_hash, local_token_address, remote_token_address`) l2_bridge_withdrawals.timestamp, cross_domain_message_hash, local_token_address, remote_token_address`)
withdrawalsQuery = withdrawalsQuery.Order("timestamp DESC").Limit(limit + 1)
if cursor != "" { if cursorClause != "" {
// Probably need to fix this and compare timestamps withdrawalsQuery = withdrawalsQuery.Where(cursorClause)
withdrawalsQuery = withdrawalsQuery.Where("withdrawal_hash < ?", cursor)
} }
query := db.gorm.Table("(?) AS withdrawals", withdrawalsQuery) query := db.gorm.Table("(?) AS withdrawals", withdrawalsQuery)
query = query.Joins("UNION (?)", ethTransactionWithdrawals) query = query.Joins("UNION (?)", ethTransactionWithdrawals)
query = query.Select("*").Order("timestamp DESC").Limit(limit + 1) query = query.Select("*").Order("timestamp DESC").Limit(limit + 1)
withdrawals := []L2BridgeWithdrawalWithTransactionHashes{} withdrawals := []L2BridgeWithdrawalWithTransactionHashes{}
result := query.Scan(&withdrawals) result := query.Find(&withdrawals)
if result.Error != nil { if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) { if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, nil return nil, nil
...@@ -292,21 +306,10 @@ l2_bridge_withdrawals.timestamp, cross_domain_message_hash, local_token_address, ...@@ -292,21 +306,10 @@ l2_bridge_withdrawals.timestamp, cross_domain_message_hash, local_token_address,
hasNextPage := false hasNextPage := false
if len(withdrawals) > limit { if len(withdrawals) > limit {
hasNextPage = true hasNextPage = true
nextCursor = withdrawals[limit].L2BridgeWithdrawal.TransactionWithdrawalHash.String()
withdrawals = withdrawals[:limit] withdrawals = withdrawals[:limit]
nextCursor = withdrawals[limit].L2TransactionHash.String()
}
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, result.Error
}
response := &L2BridgeWithdrawalsResponse{
Withdrawals: withdrawals,
Cursor: nextCursor,
HasNextPage: hasNextPage,
} }
response := &L2BridgeWithdrawalsResponse{Withdrawals: withdrawals, Cursor: nextCursor, HasNextPage: hasNextPage}
return response, nil return response, nil
} }
...@@ -43,9 +43,9 @@ func createE2ETestSuite(t *testing.T) E2ETestSuite { ...@@ -43,9 +43,9 @@ func createE2ETestSuite(t *testing.T) E2ETestSuite {
dbUser := os.Getenv("DB_USER") dbUser := os.Getenv("DB_USER")
dbName := setupTestDatabase(t) dbName := setupTestDatabase(t)
// Replace the handler of the global logger with the testlog // Discard the Global Logger as each component
logger := testlog.Logger(t, log.LvlInfo) // has its own configured logger
log.Root().SetHandler(logger.GetHandler()) log.Root().SetHandler(log.DiscardHandler())
// Rollup System Configuration and Start // Rollup System Configuration and Start
opCfg := op_e2e.DefaultSystemConfig(t) opCfg := op_e2e.DefaultSystemConfig(t)
...@@ -71,8 +71,10 @@ func createE2ETestSuite(t *testing.T) E2ETestSuite { ...@@ -71,8 +71,10 @@ func createE2ETestSuite(t *testing.T) E2ETestSuite {
L2RPC: opSys.EthInstances["sequencer"].HTTPEndpoint(), L2RPC: opSys.EthInstances["sequencer"].HTTPEndpoint(),
}, },
Chain: config.ChainConfig{ Chain: config.ChainConfig{
L1PollingInterval: 1000, L1PollingInterval: uint(opCfg.DeployConfig.L1BlockTime) * 1000,
L2PollingInterval: 1000, L1ConfirmationDepth: 0,
L2PollingInterval: uint(opCfg.DeployConfig.L2BlockTime) * 1000,
L2ConfirmationDepth: 0,
L1Contracts: config.L1Contracts{ L1Contracts: config.L1Contracts{
OptimismPortalProxy: opCfg.L1Deployments.OptimismPortalProxy, OptimismPortalProxy: opCfg.L1Deployments.OptimismPortalProxy,
L2OutputOracleProxy: opCfg.L1Deployments.L2OutputOracleProxy, L2OutputOracleProxy: opCfg.L1Deployments.L2OutputOracleProxy,
...@@ -80,17 +82,16 @@ func createE2ETestSuite(t *testing.T) E2ETestSuite { ...@@ -80,17 +82,16 @@ func createE2ETestSuite(t *testing.T) E2ETestSuite {
L1StandardBridgeProxy: opCfg.L1Deployments.L1StandardBridgeProxy, L1StandardBridgeProxy: opCfg.L1Deployments.L1StandardBridgeProxy,
}, },
}, },
Metrics: config.MetricsConfig{ HTTPServer: config.ServerConfig{Host: "127.0.0.1", Port: 0},
Host: "127.0.0.1", MetricsServer: config.ServerConfig{Host: "127.0.0.1", Port: 0},
Port: 0,
},
} }
db, err := database.NewDB(indexerCfg.DB) db, err := database.NewDB(indexerCfg.DB)
require.NoError(t, err) require.NoError(t, err)
t.Cleanup(func() { db.Close() }) t.Cleanup(func() { db.Close() })
indexer, err := indexer.NewIndexer(logger, db, indexerCfg.Chain, indexerCfg.RPCs, indexerCfg.Metrics) indexerLog := testlog.Logger(t, log.LvlInfo).New("role", "indexer")
indexer, err := indexer.NewIndexer(indexerLog, db, indexerCfg.Chain, indexerCfg.RPCs, indexerCfg.HTTPServer, indexerCfg.MetricsServer)
require.NoError(t, err) require.NoError(t, err)
indexerCtx, indexerStop := context.WithCancel(context.Background()) indexerCtx, indexerStop := context.WithCancel(context.Background())
......
...@@ -16,7 +16,9 @@ import ( ...@@ -16,7 +16,9 @@ import (
type Config struct { type Config struct {
LoopIntervalMsec uint LoopIntervalMsec uint
HeaderBufferSize uint HeaderBufferSize uint
StartHeight *big.Int
StartHeight *big.Int
ConfirmationDepth *big.Int
} }
type ETL struct { type ETL struct {
......
...@@ -3,6 +3,7 @@ package etl ...@@ -3,6 +3,7 @@ package etl
import ( import (
"context" "context"
"fmt" "fmt"
"sync"
"time" "time"
"github.com/ethereum-optimism/optimism/indexer/config" "github.com/ethereum-optimism/optimism/indexer/config"
...@@ -16,7 +17,9 @@ import ( ...@@ -16,7 +17,9 @@ import (
type L1ETL struct { type L1ETL struct {
ETL ETL
db *database.DB db *database.DB
mu *sync.Mutex
listeners []chan interface{}
} }
// NewL1ETL creates a new L1ETL instance that will start indexing from different starting points // NewL1ETL creates a new L1ETL instance that will start indexing from different starting points
...@@ -41,7 +44,7 @@ func NewL1ETL(cfg Config, log log.Logger, db *database.DB, metrics Metricer, cli ...@@ -41,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)
...@@ -50,7 +53,7 @@ func NewL1ETL(cfg Config, log log.Logger, db *database.DB, metrics Metricer, cli ...@@ -50,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
...@@ -62,13 +65,13 @@ func NewL1ETL(cfg Config, log log.Logger, db *database.DB, metrics Metricer, cli ...@@ -62,13 +65,13 @@ func NewL1ETL(cfg Config, log log.Logger, db *database.DB, metrics Metricer, cli
log: log, log: log,
metrics: metrics, metrics: metrics,
headerTraversal: node.NewHeaderTraversal(client, fromHeader), headerTraversal: node.NewHeaderTraversal(client, fromHeader, cfg.ConfirmationDepth),
ethClient: client, ethClient: client,
contracts: cSlice, contracts: cSlice,
etlBatches: etlBatches, etlBatches: etlBatches,
} }
return &L1ETL{ETL: etl, db: db}, nil return &L1ETL{ETL: etl, db: db, mu: new(sync.Mutex)}, nil
} }
func (l1Etl *L1ETL) Start(ctx context.Context) error { func (l1Etl *L1ETL) Start(ctx context.Context) error {
...@@ -129,6 +132,29 @@ func (l1Etl *L1ETL) Start(ctx context.Context) error { ...@@ -129,6 +132,29 @@ func (l1Etl *L1ETL) Start(ctx context.Context) error {
} }
batch.Logger.Info("indexed batch") batch.Logger.Info("indexed batch")
// Notify Listeners
l1Etl.mu.Lock()
for i := range l1Etl.listeners {
select {
case l1Etl.listeners[i] <- struct{}{}:
default:
// do nothing if the listener hasn't picked
// up the previous notif
}
}
l1Etl.mu.Unlock()
} }
} }
} }
// Notify returns a channel that'll receive a value every time new data has
// been persisted by the L1ETL
func (l1Etl *L1ETL) Notify() <-chan interface{} {
receiver := make(chan interface{})
l1Etl.mu.Lock()
defer l1Etl.mu.Unlock()
l1Etl.listeners = append(l1Etl.listeners, receiver)
return receiver
}
...@@ -49,7 +49,7 @@ func NewL2ETL(cfg Config, log log.Logger, db *database.DB, metrics Metricer, cli ...@@ -49,7 +49,7 @@ func NewL2ETL(cfg Config, log log.Logger, db *database.DB, metrics Metricer, cli
log: log, log: log,
metrics: metrics, metrics: metrics,
headerTraversal: node.NewHeaderTraversal(client, fromHeader), headerTraversal: node.NewHeaderTraversal(client, fromHeader, cfg.ConfirmationDepth),
ethClient: client, ethClient: client,
contracts: l2Contracts, contracts: l2Contracts,
etlBatches: etlBatches, etlBatches: etlBatches,
......
...@@ -3,10 +3,16 @@ package indexer ...@@ -3,10 +3,16 @@ package indexer
import ( import (
"context" "context"
"fmt" "fmt"
"math/big"
"net/http"
"runtime/debug" "runtime/debug"
"sync" "sync"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/ethereum-optimism/optimism/indexer/config" "github.com/ethereum-optimism/optimism/indexer/config"
...@@ -14,6 +20,7 @@ import ( ...@@ -14,6 +20,7 @@ import (
"github.com/ethereum-optimism/optimism/indexer/etl" "github.com/ethereum-optimism/optimism/indexer/etl"
"github.com/ethereum-optimism/optimism/indexer/node" "github.com/ethereum-optimism/optimism/indexer/node"
"github.com/ethereum-optimism/optimism/indexer/processors" "github.com/ethereum-optimism/optimism/indexer/processors"
"github.com/ethereum-optimism/optimism/op-service/httputil"
"github.com/ethereum-optimism/optimism/op-service/metrics" "github.com/ethereum-optimism/optimism/op-service/metrics"
) )
...@@ -23,17 +30,24 @@ type Indexer struct { ...@@ -23,17 +30,24 @@ type Indexer struct {
log log.Logger log log.Logger
db *database.DB db *database.DB
metricsConfig config.MetricsConfig httpConfig config.ServerConfig
metricsConfig config.ServerConfig
metricsRegistry *prometheus.Registry metricsRegistry *prometheus.Registry
L1ETL *etl.L1ETL L1ETL *etl.L1ETL
L2ETL *etl.L2ETL L2ETL *etl.L2ETL
BridgeProcessor *processors.BridgeProcessor BridgeProcessor *processors.BridgeProcessor
} }
// NewIndexer initializes an instance of the Indexer // NewIndexer initializes an instance of the Indexer
func NewIndexer(logger log.Logger, db *database.DB, chainConfig config.ChainConfig, rpcsConfig config.RPCsConfig, metricsConfig config.MetricsConfig) (*Indexer, error) { func NewIndexer(
log log.Logger,
db *database.DB,
chainConfig config.ChainConfig,
rpcsConfig config.RPCsConfig,
httpConfig config.ServerConfig,
metricsConfig config.ServerConfig,
) (*Indexer, error) {
metricsRegistry := metrics.NewRegistry() metricsRegistry := metrics.NewRegistry()
// L1 // L1
...@@ -41,8 +55,13 @@ func NewIndexer(logger log.Logger, db *database.DB, chainConfig config.ChainConf ...@@ -41,8 +55,13 @@ func NewIndexer(logger log.Logger, db *database.DB, chainConfig config.ChainConf
if err != nil { if err != nil {
return nil, err return nil, err
} }
l1Cfg := etl.Config{LoopIntervalMsec: chainConfig.L1PollingInterval, HeaderBufferSize: chainConfig.L1HeaderBufferSize, StartHeight: chainConfig.L1StartHeight()} l1Cfg := etl.Config{
l1Etl, err := etl.NewL1ETL(l1Cfg, logger, db, etl.NewMetrics(metricsRegistry, "l1"), l1EthClient, chainConfig.L1Contracts) LoopIntervalMsec: chainConfig.L1PollingInterval,
HeaderBufferSize: chainConfig.L1HeaderBufferSize,
ConfirmationDepth: big.NewInt(int64(chainConfig.L1ConfirmationDepth)),
StartHeight: big.NewInt(int64(chainConfig.L1StartingHeight)),
}
l1Etl, err := etl.NewL1ETL(l1Cfg, log, db, etl.NewMetrics(metricsRegistry, "l1"), l1EthClient, chainConfig.L1Contracts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -52,22 +71,27 @@ func NewIndexer(logger log.Logger, db *database.DB, chainConfig config.ChainConf ...@@ -52,22 +71,27 @@ func NewIndexer(logger log.Logger, db *database.DB, chainConfig config.ChainConf
if err != nil { if err != nil {
return nil, err return nil, err
} }
l2Cfg := etl.Config{LoopIntervalMsec: chainConfig.L2PollingInterval, HeaderBufferSize: chainConfig.L2HeaderBufferSize} l2Cfg := etl.Config{
l2Etl, err := etl.NewL2ETL(l2Cfg, logger, db, etl.NewMetrics(metricsRegistry, "l2"), l2EthClient) LoopIntervalMsec: chainConfig.L2PollingInterval,
HeaderBufferSize: chainConfig.L2HeaderBufferSize,
ConfirmationDepth: big.NewInt(int64(chainConfig.L2ConfirmationDepth)),
}
l2Etl, err := etl.NewL2ETL(l2Cfg, log, db, etl.NewMetrics(metricsRegistry, "l2"), l2EthClient)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Bridge // Bridge
bridgeProcessor, err := processors.NewBridgeProcessor(logger, db, chainConfig) bridgeProcessor, err := processors.NewBridgeProcessor(log, db, l1Etl, chainConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }
indexer := &Indexer{ indexer := &Indexer{
log: logger, log: log,
db: db, db: db,
httpConfig: httpConfig,
metricsConfig: metricsConfig, metricsConfig: metricsConfig,
metricsRegistry: metricsRegistry, metricsRegistry: metricsRegistry,
...@@ -79,6 +103,23 @@ func NewIndexer(logger log.Logger, db *database.DB, chainConfig config.ChainConf ...@@ -79,6 +103,23 @@ func NewIndexer(logger log.Logger, db *database.DB, chainConfig config.ChainConf
return indexer, nil return indexer, nil
} }
func (i *Indexer) startHttpServer(ctx context.Context) error {
i.log.Info("starting http server...", "port", i.httpConfig.Host)
r := chi.NewRouter()
r.Use(middleware.Heartbeat("/healthz"))
server := http.Server{Addr: fmt.Sprintf("%s:%d", i.httpConfig.Host, i.httpConfig.Port), Handler: r}
err := httputil.ListenAndServeContext(ctx, &server)
if err != nil {
i.log.Error("http server stopped", "err", err)
} else {
i.log.Info("http server stopped")
}
return err
}
func (i *Indexer) startMetricsServer(ctx context.Context) error { func (i *Indexer) startMetricsServer(ctx context.Context) error {
i.log.Info("starting metrics server...", "port", i.metricsConfig.Port) i.log.Info("starting metrics server...", "port", i.metricsConfig.Port)
err := metrics.ListenAndServe(ctx, i.metricsRegistry, i.metricsConfig.Host, i.metricsConfig.Port) err := metrics.ListenAndServe(ctx, i.metricsRegistry, i.metricsConfig.Host, i.metricsConfig.Port)
...@@ -94,7 +135,7 @@ func (i *Indexer) startMetricsServer(ctx context.Context) error { ...@@ -94,7 +135,7 @@ func (i *Indexer) startMetricsServer(ctx context.Context) error {
// Start starts the indexing service on L1 and L2 chains // Start starts the indexing service on L1 and L2 chains
func (i *Indexer) Run(ctx context.Context) error { func (i *Indexer) Run(ctx context.Context) error {
var wg sync.WaitGroup var wg sync.WaitGroup
errCh := make(chan error, 4) errCh := make(chan error, 5)
// if any goroutine halts, we stop the entire indexer // if any goroutine halts, we stop the entire indexer
processCtx, processCancel := context.WithCancel(ctx) processCtx, processCancel := context.WithCancel(ctx)
...@@ -121,6 +162,7 @@ func (i *Indexer) Run(ctx context.Context) error { ...@@ -121,6 +162,7 @@ func (i *Indexer) Run(ctx context.Context) error {
runProcess(i.L2ETL.Start) runProcess(i.L2ETL.Start)
runProcess(i.BridgeProcessor.Start) runProcess(i.BridgeProcessor.Start)
runProcess(i.startMetricsServer) runProcess(i.startMetricsServer)
runProcess(i.startHttpServer)
wg.Wait() wg.Wait()
err := <-errCh err := <-errCh
......
# Chain configures l1 chain addresses # Chain configures l1 chain addresses
# Can configure them manually or use a preset l2 ChainId for known chains including OP Mainnet, OP Goerli, Base, Base Goerli, Zora, and Zora goerli # Can configure them manually or use a preset l2 ChainId for known chains including OP Mainnet, OP Goerli, Base, Base Goerli, Zora, and Zora goerli
[chain] [chain]
# OP Goerli
preset = 420
# L1 Config
l1-polling-interval = 0 l1-polling-interval = 0
l1-header-buffer-size = 0 l1-header-buffer-size = 0
l1-confirmation-depth = 0
l1-starting-height = 0
# L2 Config
l2-polling-interval = 0 l2-polling-interval = 0
l2-header-buffer-size = 0 l2-header-buffer-size = 0
l2-confirmation-depth = 0
# OP Goerli
preset = 420
l1-starting-height = 0
[rpcs] [rpcs]
l1-rpc = "${INDEXER_RPC_URL_L1}" 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"
[api] [http]
host = "127.0.0.1" host = "127.0.0.1"
port = 8080 port = 8080
......
...@@ -25,8 +25,6 @@ const ( ...@@ -25,8 +25,6 @@ const (
) )
type EthClient interface { type EthClient interface {
FinalizedBlockHeight() (*big.Int, error)
BlockHeaderByNumber(*big.Int) (*types.Header, error) BlockHeaderByNumber(*big.Int) (*types.Header, error)
BlockHeaderByHash(common.Hash) (*types.Header, error) BlockHeaderByHash(common.Hash) (*types.Header, error)
BlockHeadersByRange(*big.Int, *big.Int) ([]types.Header, error) BlockHeadersByRange(*big.Int, *big.Int) ([]types.Header, error)
...@@ -52,24 +50,6 @@ func DialEthClient(rpcUrl string, metrics Metricer) (EthClient, error) { ...@@ -52,24 +50,6 @@ func DialEthClient(rpcUrl string, metrics Metricer) (EthClient, error) {
return client, nil return client, nil
} }
// FinalizedBlockHeight retrieves the latest block height in a finalized state
func (c *client) FinalizedBlockHeight() (*big.Int, error) {
ctxwt, cancel := context.WithTimeout(context.Background(), defaultRequestTimeout)
defer cancel()
// **NOTE** Local devnet is having issues with the "finalized" block tag. Temp switch
// to "latest" to iterate faster locally but this needs to be updated
var header *types.Header
err := c.rpc.CallContext(ctxwt, &header, "eth_getBlockByNumber", "latest", false)
if err != nil {
return nil, err
} else if header == nil {
return nil, ethereum.NotFound
}
return header.Number, nil
}
// BlockHeaderByHash retrieves the block header attributed to the supplied hash // BlockHeaderByHash retrieves the block header attributed to the supplied hash
func (c *client) BlockHeaderByHash(hash common.Hash) (*types.Header, error) { func (c *client) BlockHeaderByHash(hash common.Hash) (*types.Header, error) {
ctxwt, cancel := context.WithTimeout(context.Background(), defaultRequestTimeout) ctxwt, cancel := context.WithTimeout(context.Background(), defaultRequestTimeout)
......
...@@ -14,15 +14,16 @@ var ( ...@@ -14,15 +14,16 @@ var (
) )
type HeaderTraversal struct { type HeaderTraversal struct {
ethClient EthClient ethClient EthClient
lastHeader *types.Header
lastHeader *types.Header
blockConfirmationDepth *big.Int
} }
// NewHeaderTraversal instantiates a new instance of HeaderTraversal against the supplied rpc client. // NewHeaderTraversal instantiates a new instance of HeaderTraversal against the supplied rpc client.
// The HeaderTraversal will start fetching blocks starting from the supplied header unless // The HeaderTraversal will start fetching blocks starting from the supplied header unless nil, indicating genesis.
// nil, indicating genesis. func NewHeaderTraversal(ethClient EthClient, fromHeader *types.Header, confDepth *big.Int) *HeaderTraversal {
func NewHeaderTraversal(ethClient EthClient, fromHeader *types.Header) *HeaderTraversal { return &HeaderTraversal{ethClient: ethClient, lastHeader: fromHeader, blockConfirmationDepth: confDepth}
return &HeaderTraversal{ethClient: ethClient, lastHeader: fromHeader}
} }
// LastHeader returns the last header that was fetched by the HeaderTraversal // LastHeader returns the last header that was fetched by the HeaderTraversal
...@@ -34,13 +35,19 @@ func (f *HeaderTraversal) LastHeader() *types.Header { ...@@ -34,13 +35,19 @@ func (f *HeaderTraversal) LastHeader() *types.Header {
// NextFinalizedHeaders retrives the next set of headers that have been // NextFinalizedHeaders retrives the next set of headers that have been
// marked as finalized by the connected client, bounded by the supplied size // marked as finalized by the connected client, bounded by the supplied size
func (f *HeaderTraversal) NextFinalizedHeaders(maxSize uint64) ([]types.Header, error) { func (f *HeaderTraversal) NextFinalizedHeaders(maxSize uint64) ([]types.Header, error) {
finalizedBlockHeight, err := f.ethClient.FinalizedBlockHeight() latestBlockHeader, err := f.ethClient.BlockHeaderByNumber(nil)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to query latest finalized height: %w", err) return nil, fmt.Errorf("unable to query latest block: %w", err)
}
endHeight := new(big.Int).Sub(latestBlockHeader.Number, f.blockConfirmationDepth)
if endHeight.Sign() < 0 {
// No blocks with the provided confirmation depth available
return nil, nil
} }
if f.lastHeader != nil { if f.lastHeader != nil {
cmp := f.lastHeader.Number.Cmp(finalizedBlockHeight) cmp := f.lastHeader.Number.Cmp(endHeight)
if cmp == 0 { if cmp == 0 {
return nil, nil return nil, nil
} else if cmp > 0 { } else if cmp > 0 {
...@@ -53,7 +60,7 @@ func (f *HeaderTraversal) NextFinalizedHeaders(maxSize uint64) ([]types.Header, ...@@ -53,7 +60,7 @@ func (f *HeaderTraversal) NextFinalizedHeaders(maxSize uint64) ([]types.Header,
nextHeight = new(big.Int).Add(f.lastHeader.Number, bigOne) nextHeight = new(big.Int).Add(f.lastHeader.Number, bigOne)
} }
endHeight := clampBigInt(nextHeight, finalizedBlockHeight, maxSize) endHeight = clampBigInt(nextHeight, endHeight, maxSize)
headers, err := f.ethClient.BlockHeadersByRange(nextHeight, endHeight) headers, err := f.ethClient.BlockHeadersByRange(nextHeight, endHeight)
if err != nil { if err != nil {
return nil, fmt.Errorf("error querying blocks by range: %w", err) return nil, fmt.Errorf("error querying blocks by range: %w", err)
......
...@@ -37,10 +37,10 @@ func TestHeaderTraversalNextFinalizedHeadersNoOp(t *testing.T) { ...@@ -37,10 +37,10 @@ func TestHeaderTraversalNextFinalizedHeadersNoOp(t *testing.T) {
// start from block 10 as the latest fetched block // start from block 10 as the latest fetched block
lastHeader := &types.Header{Number: big.NewInt(10)} lastHeader := &types.Header{Number: big.NewInt(10)}
headerTraversal := NewHeaderTraversal(client, lastHeader) headerTraversal := NewHeaderTraversal(client, lastHeader, bigZero)
// no new headers when matched with head // no new headers when matched with head
client.On("FinalizedBlockHeight").Return(big.NewInt(10), nil) client.On("BlockHeaderByNumber", (*big.Int)(nil)).Return(lastHeader, nil)
headers, err := headerTraversal.NextFinalizedHeaders(100) headers, err := headerTraversal.NextFinalizedHeaders(100)
require.NoError(t, err) require.NoError(t, err)
require.Empty(t, headers) require.Empty(t, headers)
...@@ -50,11 +50,11 @@ func TestHeaderTraversalNextFinalizedHeadersCursored(t *testing.T) { ...@@ -50,11 +50,11 @@ func TestHeaderTraversalNextFinalizedHeadersCursored(t *testing.T) {
client := new(MockEthClient) client := new(MockEthClient)
// start from genesis // start from genesis
headerTraversal := NewHeaderTraversal(client, nil) headerTraversal := NewHeaderTraversal(client, nil, bigZero)
// blocks [0..4] // blocks [0..4]
headers := makeHeaders(5, nil) headers := makeHeaders(5, nil)
client.On("FinalizedBlockHeight").Return(big.NewInt(4), nil).Times(1) // Times so that we can override next client.On("BlockHeaderByNumber", (*big.Int)(nil)).Return(&headers[4], nil).Times(1) // Times so that we can override next
client.On("BlockHeadersByRange", mock.MatchedBy(BigIntMatcher(0)), mock.MatchedBy(BigIntMatcher(4))).Return(headers, nil) client.On("BlockHeadersByRange", mock.MatchedBy(BigIntMatcher(0)), mock.MatchedBy(BigIntMatcher(4))).Return(headers, nil)
headers, err := headerTraversal.NextFinalizedHeaders(5) headers, err := headerTraversal.NextFinalizedHeaders(5)
require.NoError(t, err) require.NoError(t, err)
...@@ -62,7 +62,7 @@ func TestHeaderTraversalNextFinalizedHeadersCursored(t *testing.T) { ...@@ -62,7 +62,7 @@ func TestHeaderTraversalNextFinalizedHeadersCursored(t *testing.T) {
// blocks [5..9] // blocks [5..9]
headers = makeHeaders(5, &headers[len(headers)-1]) headers = makeHeaders(5, &headers[len(headers)-1])
client.On("FinalizedBlockHeight").Return(big.NewInt(9), nil) client.On("BlockHeaderByNumber", (*big.Int)(nil)).Return(&headers[4], nil)
client.On("BlockHeadersByRange", mock.MatchedBy(BigIntMatcher(5)), mock.MatchedBy(BigIntMatcher(9))).Return(headers, nil) client.On("BlockHeadersByRange", mock.MatchedBy(BigIntMatcher(5)), mock.MatchedBy(BigIntMatcher(9))).Return(headers, nil)
headers, err = headerTraversal.NextFinalizedHeaders(5) headers, err = headerTraversal.NextFinalizedHeaders(5)
require.NoError(t, err) require.NoError(t, err)
...@@ -73,10 +73,10 @@ func TestHeaderTraversalNextFinalizedHeadersMaxSize(t *testing.T) { ...@@ -73,10 +73,10 @@ func TestHeaderTraversalNextFinalizedHeadersMaxSize(t *testing.T) {
client := new(MockEthClient) client := new(MockEthClient)
// start from genesis // start from genesis
headerTraversal := NewHeaderTraversal(client, nil) headerTraversal := NewHeaderTraversal(client, nil, bigZero)
// 100 "available" headers // 100 "available" headers
client.On("FinalizedBlockHeight").Return(big.NewInt(100), nil) client.On("BlockHeaderByNumber", (*big.Int)(nil)).Return(&types.Header{Number: big.NewInt(100)}, nil)
// clamped by the supplied size // clamped by the supplied size
headers := makeHeaders(5, nil) headers := makeHeaders(5, nil)
...@@ -97,11 +97,11 @@ func TestHeaderTraversalMismatchedProviderStateError(t *testing.T) { ...@@ -97,11 +97,11 @@ func TestHeaderTraversalMismatchedProviderStateError(t *testing.T) {
client := new(MockEthClient) client := new(MockEthClient)
// start from genesis // start from genesis
headerTraversal := NewHeaderTraversal(client, nil) headerTraversal := NewHeaderTraversal(client, nil, bigZero)
// blocks [0..4] // blocks [0..4]
headers := makeHeaders(5, nil) headers := makeHeaders(5, nil)
client.On("FinalizedBlockHeight").Return(big.NewInt(4), nil).Times(1) // Times so that we can override next client.On("BlockHeaderByNumber", (*big.Int)(nil)).Return(&headers[4], nil).Times(1) // Times so that we can override next
client.On("BlockHeadersByRange", mock.MatchedBy(BigIntMatcher(0)), mock.MatchedBy(BigIntMatcher(4))).Return(headers, nil) client.On("BlockHeadersByRange", mock.MatchedBy(BigIntMatcher(0)), mock.MatchedBy(BigIntMatcher(4))).Return(headers, nil)
headers, err := headerTraversal.NextFinalizedHeaders(5) headers, err := headerTraversal.NextFinalizedHeaders(5)
require.NoError(t, err) require.NoError(t, err)
...@@ -109,7 +109,7 @@ func TestHeaderTraversalMismatchedProviderStateError(t *testing.T) { ...@@ -109,7 +109,7 @@ func TestHeaderTraversalMismatchedProviderStateError(t *testing.T) {
// blocks [5..9]. Next batch is not chained correctly (starts again from genesis) // blocks [5..9]. Next batch is not chained correctly (starts again from genesis)
headers = makeHeaders(5, nil) headers = makeHeaders(5, nil)
client.On("FinalizedBlockHeight").Return(big.NewInt(9), nil) client.On("BlockHeaderByNumber", (*big.Int)(nil)).Return(&types.Header{Number: big.NewInt(9)}, nil)
client.On("BlockHeadersByRange", mock.MatchedBy(BigIntMatcher(5)), mock.MatchedBy(BigIntMatcher(9))).Return(headers, nil) client.On("BlockHeadersByRange", mock.MatchedBy(BigIntMatcher(5)), mock.MatchedBy(BigIntMatcher(9))).Return(headers, nil)
headers, err = headerTraversal.NextFinalizedHeaders(5) headers, err = headerTraversal.NextFinalizedHeaders(5)
require.Nil(t, headers) require.Nil(t, headers)
......
...@@ -20,9 +20,9 @@ func (m *MockEthClient) BlockHeaderByNumber(number *big.Int) (*types.Header, err ...@@ -20,9 +20,9 @@ func (m *MockEthClient) BlockHeaderByNumber(number *big.Int) (*types.Header, err
return args.Get(0).(*types.Header), args.Error(1) return args.Get(0).(*types.Header), args.Error(1)
} }
func (m *MockEthClient) FinalizedBlockHeight() (*big.Int, error) { func (m *MockEthClient) BlockHeaderByHash(hash common.Hash) (*types.Header, error) {
args := m.Called() args := m.Called(hash)
return args.Get(0).(*big.Int), args.Error(1) return args.Get(0).(*types.Header), args.Error(1)
} }
func (m *MockEthClient) BlockHeadersByRange(from, to *big.Int) ([]types.Header, error) { func (m *MockEthClient) BlockHeadersByRange(from, to *big.Int) ([]types.Header, error) {
...@@ -30,11 +30,6 @@ func (m *MockEthClient) BlockHeadersByRange(from, to *big.Int) ([]types.Header, ...@@ -30,11 +30,6 @@ func (m *MockEthClient) BlockHeadersByRange(from, to *big.Int) ([]types.Header,
return args.Get(0).([]types.Header), args.Error(1) return args.Get(0).([]types.Header), args.Error(1)
} }
func (m *MockEthClient) BlockHeaderByHash(hash common.Hash) (*types.Header, error) {
args := m.Called(hash)
return args.Get(0).(*types.Header), args.Error(1)
}
func (m *MockEthClient) StorageHash(address common.Address, blockNumber *big.Int) (common.Hash, error) { func (m *MockEthClient) StorageHash(address common.Address, blockNumber *big.Int) (common.Hash, error) {
args := m.Called(address, blockNumber) args := m.Called(address, blockNumber)
return args.Get(0).(common.Hash), args.Error(1) return args.Get(0).(common.Hash), args.Error(1)
......
...@@ -4,10 +4,10 @@ import ( ...@@ -4,10 +4,10 @@ import (
"context" "context"
"errors" "errors"
"math/big" "math/big"
"time"
"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/etl"
"github.com/ethereum-optimism/optimism/indexer/processors/bridge" "github.com/ethereum-optimism/optimism/indexer/processors/bridge"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
...@@ -17,6 +17,7 @@ import ( ...@@ -17,6 +17,7 @@ import (
type BridgeProcessor struct { type BridgeProcessor struct {
log log.Logger log log.Logger
db *database.DB db *database.DB
l1Etl *etl.L1ETL
chainConfig config.ChainConfig chainConfig config.ChainConfig
// NOTE: We'll need this processor to handle for reorgs events. // NOTE: We'll need this processor to handle for reorgs events.
...@@ -25,48 +26,40 @@ type BridgeProcessor struct { ...@@ -25,48 +26,40 @@ type BridgeProcessor struct {
LatestL2Header *types.Header LatestL2Header *types.Header
} }
func NewBridgeProcessor(log log.Logger, db *database.DB, 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, 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 {
done := ctx.Done() done := ctx.Done()
// NOTE: This should run on same iterval as L1 ETL rather than as finding the
// lasted epoch is constrained to how much L1 data we've indexed.
pollTicker := time.NewTicker(5 * time.Second)
defer pollTicker.Stop()
// In order to ensure all seen bridge finalization events correspond with seen // In order to ensure all seen bridge finalization events correspond with seen
// bridge initiated events, we establish a shared marker between L1 and L2 when // bridge initiated events, we establish a shared marker between L1 and L2 when
// processing events. // processing events.
...@@ -75,9 +68,7 @@ func (b *BridgeProcessor) Start(ctx context.Context) error { ...@@ -75,9 +68,7 @@ func (b *BridgeProcessor) Start(ctx context.Context) error {
// sequencing epoch and corresponding L1 origin that has also been indexed // sequencing epoch and corresponding L1 origin that has also been indexed
// serves as this shared marker. // serves as this shared marker.
// TODOs: l1EtlUpdates := b.l1Etl.Notify()
// 1. Fix Logging. Should be clear if we're looking at L1 or L2 side of things
b.log.Info("starting bridge processor...") b.log.Info("starting bridge processor...")
for { for {
select { select {
...@@ -85,38 +76,49 @@ func (b *BridgeProcessor) Start(ctx context.Context) error { ...@@ -85,38 +76,49 @@ func (b *BridgeProcessor) Start(ctx context.Context) error {
b.log.Info("stopping bridge processor") b.log.Info("stopping bridge processor")
return nil return nil
case <-pollTicker.C: case <-l1EtlUpdates:
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 satte `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 blocks epochs 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 block state. 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))
} }
batchLog := b.log.New("epoch_start_number", fromL1Height, "epoch_end_number", toL1Height) batchLog := b.log.New("epoch_start_number", fromL1Height, "epoch_end_number", toL1Height)
batchLog.Info("scanning bridge events") batchLog.Info("scanning for new bridge events")
err = b.db.Transaction(func(tx *database.DB) error { err = b.db.Transaction(func(tx *database.DB) error {
l1BridgeLog := b.log.New("from_l1_block_number", fromL1Height, "to_l1_block_number", toL1Height) l1BridgeLog := b.log.New("from_l1_block_number", fromL1Height, "to_l1_block_number", toL1Height)
l2BridgeLog := b.log.New("from_l2_block_number", fromL2Height, "to_l2_block_number", toL2Height) l2BridgeLog := b.log.New("from_l2_block_number", fromL2Height, "to_l2_block_number", toL2Height)
...@@ -145,7 +147,7 @@ func (b *BridgeProcessor) Start(ctx context.Context) error { ...@@ -145,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.
...@@ -4,21 +4,24 @@ ARG VERSION=v0.0.0 ...@@ -4,21 +4,24 @@ ARG VERSION=v0.0.0
RUN apk add --no-cache make gcc musl-dev linux-headers git jq bash RUN apk add --no-cache make gcc musl-dev linux-headers git jq bash
COPY ./go.mod /app/go.mod
COPY ./go.sum /app/go.sum
WORKDIR /app
RUN go mod download
# build op-batcher with the shared go.mod & go.sum files # build op-batcher with the shared go.mod & go.sum files
COPY ./op-batcher /app/op-batcher COPY ./op-batcher /app/op-batcher
COPY ./op-bindings /app/op-bindings COPY ./op-bindings /app/op-bindings
COPY ./op-node /app/op-node COPY ./op-node /app/op-node
COPY ./op-service /app/op-service COPY ./op-service /app/op-service
COPY ./op-signer /app/op-signer COPY ./op-signer /app/op-signer
COPY ./go.mod /app/go.mod
COPY ./go.sum /app/go.sum
COPY ./.git /app/.git COPY ./.git /app/.git
WORKDIR /app/op-batcher WORKDIR /app/op-batcher
RUN go mod download
ARG TARGETOS TARGETARCH ARG TARGETOS TARGETARCH
RUN make op-batcher VERSION="$VERSION" GOOS=$TARGETOS GOARCH=$TARGETARCH RUN make op-batcher VERSION="$VERSION" GOOS=$TARGETOS GOARCH=$TARGETARCH
......
...@@ -12,6 +12,7 @@ version: ...@@ -12,6 +12,7 @@ version:
compile: compile:
cd $(contracts-dir) && \ cd $(contracts-dir) && \
forge clean && \
pnpm build pnpm build
bindings: compile bindings-build bindings: compile bindings-build
......
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 source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
...@@ -141,9 +141,8 @@ func createContractInput(input abi.Argument, inputs []ContractInput) ([]Contract ...@@ -141,9 +141,8 @@ func createContractInput(input abi.Argument, inputs []ContractInput) ([]Contract
return nil, err return nil, err
} }
// TODO: could probably do better than string comparison?
internalType := input.Type.String() internalType := input.Type.String()
if inputType == "tuple" { if input.Type.T == abi.TupleTy {
internalType = input.Type.TupleRawName internalType = input.Type.TupleRawName
} }
......
...@@ -4,6 +4,13 @@ ARG VERSION=v0.0.0 ...@@ -4,6 +4,13 @@ ARG VERSION=v0.0.0
RUN apk add --no-cache make gcc musl-dev linux-headers git jq bash RUN apk add --no-cache make gcc musl-dev linux-headers git jq bash
COPY ./go.mod /app/go.mod
COPY ./go.sum /app/go.sum
WORKDIR /app
RUN go mod download
# build op-challenger with the shared go.mod & go.sum files # build op-challenger with the shared go.mod & go.sum files
COPY ./op-challenger /app/op-challenger COPY ./op-challenger /app/op-challenger
COPY ./op-program /app/op-program COPY ./op-program /app/op-program
...@@ -12,8 +19,6 @@ COPY ./op-bindings /app/op-bindings ...@@ -12,8 +19,6 @@ COPY ./op-bindings /app/op-bindings
COPY ./op-node /app/op-node COPY ./op-node /app/op-node
COPY ./op-service /app/op-service COPY ./op-service /app/op-service
COPY ./op-signer /app/op-signer COPY ./op-signer /app/op-signer
COPY ./go.mod /app/go.mod
COPY ./go.sum /app/go.sum
COPY ./.git /app/.git COPY ./.git /app/.git
# Copy cannon and its dependencies # Copy cannon and its dependencies
...@@ -23,8 +28,6 @@ COPY ./op-chain-ops /app/op-chain-ops ...@@ -23,8 +28,6 @@ COPY ./op-chain-ops /app/op-chain-ops
WORKDIR /app/op-program WORKDIR /app/op-program
RUN go mod download
ARG TARGETOS TARGETARCH ARG TARGETOS TARGETARCH
RUN make op-program-host VERSION="$VERSION" GOOS=$TARGETOS GOARCH=$TARGETARCH RUN make op-program-host VERSION="$VERSION" GOOS=$TARGETOS GOARCH=$TARGETARCH
......
...@@ -7,16 +7,167 @@ games, and validity games. To learn more about dispute games, visit the ...@@ -7,16 +7,167 @@ games, and validity games. To learn more about dispute games, visit the
## Quickstart ## Quickstart
First, clone this repo. Then, run `make`, which will build all required targets. First, clone this repo. Then run:
Alternatively, run `make devnet` to bring up the [devnet](../ops-bedrock/devnet-up.sh)
which deploys the [mock dispute game contracts](./contracts) as well as an
`op-challenger` instance.
Alternatively, you can build the `op-challenger` binary locally using the pre-configured ```shell
[Makefile](./Makefile) target by running `make build`, and then running `./op-challenger --help` cd op-challenger
make alphabet
```
This creates a local devnet, starts a dispute game using the simple alphabet trace type and runs two op-challenger
instances with differing views of the correct alphabet to play the game.
Alternatively, you can build the `op-challenger` binary directly using the pre-configured
[Makefile](./Makefile) target by running `make build`, and then running `./bin/op-challenger --help`
to see a list of available options. to see a list of available options.
## Usage ## Usage
`op-challenger` is configurable via command line flags and environment variables. The help menu `op-challenger` is configurable via command line flags and environment variables. The help menu
shows the available config options and can be accessed by running `./op-challenger --help`. shows the available config options and can be accessed by running `./op-challenger --help`.
### Running with Cannon on Local Devnet
To run `op-challenger` against the local devnet, first ensure the required components are built and the devnet is running.
From the top level of the repository run:
```shell
make devnet-clean
make cannon-prestate op-challenger
make devnet-up
```
Then start `op-challenger` with:
```shell
DISPUTE_GAME_FACTORY=$(jq -r .DisputeGameFactoryProxy .devnet/addresses.json)
./op-challenger/bin/op-challenger \
--trace-type cannon \
--l1-eth-rpc http://localhost:8545 \
--game-factory-address $DISPUTE_GAME_FACTORY \
--agree-with-proposed-output=true \
--datadir temp/challenger-data \
--cannon-rollup-config .devnet/rollup.json \
--cannon-l2-genesis .devnet/genesis-l2.json \
--cannon-bin ./cannon/bin/cannon \
--cannon-server ./op-program/bin/op-program \
--cannon-prestate ./op-program/bin/prestate.json \
--cannon-l2 http://localhost:9545 \
--mnemonic "test test test test test test test test test test test junk" \
--hd-path "m/44'/60'/0'/0/8" \
--num-confirmations 1
```
The mnemonic and hd-path above is a prefunded address on the devnet. The challenger respond to any created games by
posting the correct trace as the counter-claim. The scripts below can then be used to create and interact with games.
## Scripts
The [scripts](scripts) directory contains a collection of scripts to assist with manually creating and playing games.
This are not intended to be used in production, only to support manual testing and to aid with understanding how
dispute games work. They also serve as examples of how to use `cast` to manually interact with the dispute game
contracts.
### Understanding Revert Reasons
When actions performed by these scripts fails, they typically print a message that includes the
abi encoded revert reason provided by the contract. e.g.
```
Error:
(code: 3, message: execution reverted, data: Some(String("0x67fe1950")))
```
The `cast 4byte` command can be used to decode these revert reasons. e.g.
```shell
$ cast 4byte 0x67fe1950
GameNotInProgress()
```
### Dependencies
These scripts assume that the following tools are installed and available on the current `PATH`:
* `cast` (https://book.getfoundry.sh/cast/)
* `jq` (https://jqlang.github.io/jq/)
* `bash`
### [create_game.sh](scripts/create_game.sh)
```shell
./scripts/create_game.sh <RPC_URL> <GAME_FACTORY_ADDRESS> <ROOT_CLAIM> <SIGNER_ARGS>...
```
Starts a new fault dispute game that disputes the latest output proposal in the L2 output oracle.
* `RPC_URL` - the RPC endpoint of the L1 endpoint to use (e.g. `http://localhost:8545`).
* `GAME_FACTORY_ADDRESS` - the address of the dispute game factory contract on L1.
* `ROOT_CLAIM` a hex encoded 32 byte hash to use as the root claim for the created game.
* `SIGNER_ARGS` the remaining args are past as arguments to `cast` when sending transactions.
These arguments must specify a way for `cast` to sign the transactions.
See `cast send --help` for supported options.
Creating a dispute game requires sending two transactions. The first transaction creates a
checkpoint in the `BlockOracle` that records the L1 block that will be used as the L1 head
when generating the cannon execution trace. The second transaction then creates the actual
dispute game, specifying the disputed L2 block number and previously checkpointed L1 head block.
### [move.sh](scripts/move.sh)
```shell
./scripts/move.sh <RPC_URL> <GAME_ADDRESS> (attack|defend) <PARENT_INDEX> <CLAIM> <SIGNER_ARGS>...
```
Performs a move to either attack or defend the latest claim in the specified game.
* `RPC_URL` - the RPC endpoint of the L1 endpoint to use (e.g. `http://localhost:8545`).
* `GAME_ADDRESS` - the address of the dispute game to perform the move in.
* `(attack|defend)` - the type of move to make.
* `attack` indicates that the state hash in your local cannon trace differs to the state
hash included in the latest claim.
* `defend` indicates that the state hash in your local cannon trace matches the state hash
included in the latest claim.
* `PARENT_INDEX` - the index of the parent claim that will be countered by this new claim.
The special value of `latest` will counter the latest claim added to the game.
* `CLAIM` - the state hash to include in the counter-claim you are posting.
* `SIGNER_ARGS` the remaining args are past as arguments to `cast` when sending transactions.
These arguments must specify a way for `cast` to sign the transactions.
See `cast send --help` for supported options.
### [resolve.sh](scripts/resolve.sh)
```shell
./scripts/resolve.sh <RPC_URL> <GAME_ADDRESS> <SIGNER_ARGS>...
```
Resolves a dispute game. Note that this will fail if the dispute game has already been resolved
or if the clocks have not yet expired and further moves are possible.
If the game is resolved successfully, the result is printed.
* `RPC_URL` - the RPC endpoint of the L1 endpoint to use (e.g. `http://localhost:8545`).
* `GAME_ADDRESS` - the address of the dispute game to resolve.
* `SIGNER_ARGS` the remaining args are past as arguments to `cast` when sending transactions.
These arguments must specify a way for `cast` to sign the transactions.
See `cast send --help` for supported options.
### [list_games.sh](scripts/list_games.sh)
```shell
./scripts/list_games.sh <RPC> <GAME_FACTORY_ADDRESS>
```
Prints the games created by the game factory along with their current status.
* `RPC_URL` - the RPC endpoint of the L1 endpoint to use (e.g. `http://localhost:8545`).
* `GAME_FACTORY_ADDRESS` - the address of the dispute game factory contract on L1.
### [list_claims.sh](scripts/list_claims.sh)
```shell
./scripts/list_claims.sh <RPC> <GAME_ADDR>
```
Prints the list of current claims in a dispute game.
* `RPC_URL` - the RPC endpoint of the L1 endpoint to use (e.g. `http://localhost:8545`).
* `GAME_ADDRESS` - the address of the dispute game to list the move in.
...@@ -254,6 +254,28 @@ func TestCannonSnapshotFreq(t *testing.T) { ...@@ -254,6 +254,28 @@ func TestCannonSnapshotFreq(t *testing.T) {
cfg := configForArgs(t, addRequiredArgs(config.TraceTypeCannon, "--cannon-snapshot-freq=1234")) cfg := configForArgs(t, addRequiredArgs(config.TraceTypeCannon, "--cannon-snapshot-freq=1234"))
require.Equal(t, uint(1234), cfg.CannonSnapshotFreq) require.Equal(t, uint(1234), cfg.CannonSnapshotFreq)
}) })
t.Run("Invalid", func(t *testing.T) {
verifyArgsInvalid(t, "invalid value \"abc\" for flag -cannon-snapshot-freq",
addRequiredArgs(config.TraceTypeCannon, "--cannon-snapshot-freq=abc"))
})
}
func TestCannonInfoFreq(t *testing.T) {
t.Run("UsesDefault", func(t *testing.T) {
cfg := configForArgs(t, addRequiredArgs(config.TraceTypeCannon))
require.Equal(t, config.DefaultCannonInfoFreq, cfg.CannonInfoFreq)
})
t.Run("Valid", func(t *testing.T) {
cfg := configForArgs(t, addRequiredArgs(config.TraceTypeCannon, "--cannon-info-freq=1234"))
require.Equal(t, uint(1234), cfg.CannonInfoFreq)
})
t.Run("Invalid", func(t *testing.T) {
verifyArgsInvalid(t, "invalid value \"abc\" for flag -cannon-info-freq",
addRequiredArgs(config.TraceTypeCannon, "--cannon-info-freq=abc"))
})
} }
func TestGameWindow(t *testing.T) { func TestGameWindow(t *testing.T) {
......
...@@ -26,6 +26,7 @@ var ( ...@@ -26,6 +26,7 @@ var (
ErrMissingL1EthRPC = errors.New("missing l1 eth rpc url") ErrMissingL1EthRPC = errors.New("missing l1 eth rpc url")
ErrMissingGameFactoryAddress = errors.New("missing game factory address") ErrMissingGameFactoryAddress = errors.New("missing game factory address")
ErrMissingCannonSnapshotFreq = errors.New("missing cannon snapshot freq") ErrMissingCannonSnapshotFreq = errors.New("missing cannon snapshot freq")
ErrMissingCannonInfoFreq = errors.New("missing cannon info freq")
ErrMissingCannonRollupConfig = errors.New("missing cannon network or rollup config path") ErrMissingCannonRollupConfig = errors.New("missing cannon network or rollup config path")
ErrMissingCannonL2Genesis = errors.New("missing cannon network or l2 genesis path") ErrMissingCannonL2Genesis = errors.New("missing cannon network or l2 genesis path")
ErrCannonNetworkAndRollupConfig = errors.New("only specify one of network or rollup config path") ErrCannonNetworkAndRollupConfig = errors.New("only specify one of network or rollup config path")
...@@ -78,6 +79,7 @@ func ValidTraceType(value TraceType) bool { ...@@ -78,6 +79,7 @@ func ValidTraceType(value TraceType) bool {
const ( const (
DefaultCannonSnapshotFreq = uint(1_000_000_000) DefaultCannonSnapshotFreq = uint(1_000_000_000)
DefaultCannonInfoFreq = uint(10_000_000)
// DefaultGameWindow is the default maximum time duration in the past // DefaultGameWindow is the default maximum time duration in the past
// that the challenger will look for games to progress. // that the challenger will look for games to progress.
// The default value is 11 days, which is a 4 day resolution buffer // The default value is 11 days, which is a 4 day resolution buffer
...@@ -111,6 +113,7 @@ type Config struct { ...@@ -111,6 +113,7 @@ type Config struct {
CannonL2GenesisPath string CannonL2GenesisPath string
CannonL2 string // L2 RPC Url CannonL2 string // L2 RPC Url
CannonSnapshotFreq uint // Frequency of snapshots to create when executing cannon (in VM instructions) CannonSnapshotFreq uint // Frequency of snapshots to create when executing cannon (in VM instructions)
CannonInfoFreq uint // Frequency of cannon progress log messages (in VM instructions)
TxMgrConfig txmgr.CLIConfig TxMgrConfig txmgr.CLIConfig
MetricsConfig opmetrics.CLIConfig MetricsConfig opmetrics.CLIConfig
...@@ -140,6 +143,7 @@ func NewConfig( ...@@ -140,6 +143,7 @@ func NewConfig(
Datadir: datadir, Datadir: datadir,
CannonSnapshotFreq: DefaultCannonSnapshotFreq, CannonSnapshotFreq: DefaultCannonSnapshotFreq,
CannonInfoFreq: DefaultCannonInfoFreq,
GameWindow: DefaultGameWindow, GameWindow: DefaultGameWindow,
} }
} }
...@@ -194,6 +198,9 @@ func (c Config) Check() error { ...@@ -194,6 +198,9 @@ func (c Config) Check() error {
if c.CannonSnapshotFreq == 0 { if c.CannonSnapshotFreq == 0 {
return ErrMissingCannonSnapshotFreq return ErrMissingCannonSnapshotFreq
} }
if c.CannonInfoFreq == 0 {
return ErrMissingCannonInfoFreq
}
} }
if c.TraceType == TraceTypeAlphabet && c.AlphabetTrace == "" { if c.TraceType == TraceTypeAlphabet && c.AlphabetTrace == "" {
return ErrMissingAlphabetTrace return ErrMissingAlphabetTrace
......
...@@ -132,6 +132,14 @@ func TestCannonSnapshotFreq(t *testing.T) { ...@@ -132,6 +132,14 @@ func TestCannonSnapshotFreq(t *testing.T) {
}) })
} }
func TestCannonInfoFreq(t *testing.T) {
t.Run("MustNotBeZero", func(t *testing.T) {
cfg := validConfig(TraceTypeCannon)
cfg.CannonInfoFreq = 0
require.ErrorIs(t, cfg.Check(), ErrMissingCannonInfoFreq)
})
}
func TestCannonNetworkOrRollupConfigRequired(t *testing.T) { func TestCannonNetworkOrRollupConfigRequired(t *testing.T) {
cfg := validConfig(TraceTypeCannon) cfg := validConfig(TraceTypeCannon)
cfg.CannonNetwork = "" cfg.CannonNetwork = ""
......
...@@ -116,6 +116,12 @@ var ( ...@@ -116,6 +116,12 @@ var (
EnvVars: prefixEnvVars("CANNON_SNAPSHOT_FREQ"), EnvVars: prefixEnvVars("CANNON_SNAPSHOT_FREQ"),
Value: config.DefaultCannonSnapshotFreq, Value: config.DefaultCannonSnapshotFreq,
} }
CannonInfoFreqFlag = &cli.UintFlag{
Name: "cannon-info-freq",
Usage: "Frequency of cannon info log messages to generate in VM steps (cannon trace type only)",
EnvVars: prefixEnvVars("CANNON_INFO_FREQ"),
Value: config.DefaultCannonInfoFreq,
}
GameWindowFlag = &cli.DurationFlag{ GameWindowFlag = &cli.DurationFlag{
Name: "game-window", Name: "game-window",
Usage: "The time window which the challenger will look for games to progress.", Usage: "The time window which the challenger will look for games to progress.",
...@@ -146,6 +152,7 @@ var optionalFlags = []cli.Flag{ ...@@ -146,6 +152,7 @@ var optionalFlags = []cli.Flag{
CannonPreStateFlag, CannonPreStateFlag,
CannonL2Flag, CannonL2Flag,
CannonSnapshotFreqFlag, CannonSnapshotFreqFlag,
CannonInfoFreqFlag,
GameWindowFlag, GameWindowFlag,
} }
...@@ -250,6 +257,7 @@ func NewConfigFromCLI(ctx *cli.Context) (*config.Config, error) { ...@@ -250,6 +257,7 @@ func NewConfigFromCLI(ctx *cli.Context) (*config.Config, error) {
Datadir: ctx.String(DatadirFlag.Name), Datadir: ctx.String(DatadirFlag.Name),
CannonL2: ctx.String(CannonL2Flag.Name), CannonL2: ctx.String(CannonL2Flag.Name),
CannonSnapshotFreq: ctx.Uint(CannonSnapshotFreqFlag.Name), CannonSnapshotFreq: ctx.Uint(CannonSnapshotFreqFlag.Name),
CannonInfoFreq: ctx.Uint(CannonInfoFreqFlag.Name),
AgreeWithProposedOutput: ctx.Bool(AgreeWithProposedOutputFlag.Name), AgreeWithProposedOutput: ctx.Bool(AgreeWithProposedOutputFlag.Name),
TxMgrConfig: txMgrConfig, TxMgrConfig: txMgrConfig,
MetricsConfig: metricsConfig, MetricsConfig: metricsConfig,
......
...@@ -7,13 +7,15 @@ import ( ...@@ -7,13 +7,15 @@ 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/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
) )
// 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
...@@ -24,6 +26,7 @@ type ClaimLoader interface { ...@@ -24,6 +26,7 @@ type ClaimLoader interface {
} }
type Agent struct { type Agent struct {
metrics metrics.Metricer
solver *solver.Solver solver *solver.Solver
loader ClaimLoader loader ClaimLoader
responder Responder responder Responder
...@@ -33,8 +36,9 @@ type Agent struct { ...@@ -33,8 +36,9 @@ type Agent struct {
log log.Logger log log.Logger
} }
func NewAgent(loader ClaimLoader, maxDepth int, trace types.TraceProvider, responder Responder, updater types.OracleUpdater, agreeWithProposedOutput bool, log log.Logger) *Agent { func NewAgent(m metrics.Metricer, loader ClaimLoader, maxDepth int, trace types.TraceProvider, responder Responder, updater types.OracleUpdater, agreeWithProposedOutput bool, log log.Logger) *Agent {
return &Agent{ return &Agent{
metrics: m,
solver: solver.NewSolver(maxDepth, trace), solver: solver.NewSolver(maxDepth, trace),
loader: loader, loader: loader,
responder: responder, responder: responder,
...@@ -71,10 +75,10 @@ func (a *Agent) Act(ctx context.Context) error { ...@@ -71,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(ctx context.Context, 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)
...@@ -82,20 +86,19 @@ func (a *Agent) shouldResolve(ctx context.Context, status types.GameStatus) bool ...@@ -82,20 +86,19 @@ func (a *Agent) shouldResolve(ctx context.Context, status types.GameStatus) bool
return expected == status return expected == status
} }
// tryResolve resolves the game if it is in a terminal state // tryResolve resolves the game if it is in a winning state
// and returns true if the game resolves successfully. // 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 { if err != nil || status == gameTypes.GameStatusInProgress {
return false return false
} }
if !a.shouldResolve(ctx, status) { if !a.shouldResolve(status) {
return false return true
} }
a.log.Info("Resolving game") a.log.Info("Resolving game")
if err := a.responder.Resolve(ctx); err != nil { if err := a.responder.Resolve(ctx); err != nil {
a.log.Error("Failed to resolve the game", "err", err) a.log.Error("Failed to resolve the game", "err", err)
return false
} }
return true return true
} }
...@@ -134,6 +137,7 @@ func (a *Agent) move(ctx context.Context, claim types.Claim, game types.Game) er ...@@ -134,6 +137,7 @@ func (a *Agent) move(ctx context.Context, claim types.Claim, game types.Game) er
log.Debug("Skipping duplicate move") log.Debug("Skipping duplicate move")
return nil return nil
} }
a.metrics.RecordGameMove()
log.Info("Performing move") log.Info("Performing move")
return a.responder.Respond(ctx, move) return a.responder.Respond(ctx, move)
} }
...@@ -170,6 +174,7 @@ func (a *Agent) step(ctx context.Context, claim types.Claim, game types.Game) er ...@@ -170,6 +174,7 @@ func (a *Agent) step(ctx context.Context, claim types.Claim, game types.Game) er
a.log.Info("Performing step", "is_attack", step.IsAttack, a.log.Info("Performing step", "is_attack", step.IsAttack,
"depth", step.LeafClaim.Depth(), "index_at_depth", step.LeafClaim.IndexAtDepth(), "value", step.LeafClaim.Value) "depth", step.LeafClaim.Depth(), "index_at_depth", step.LeafClaim.IndexAtDepth(), "value", step.LeafClaim.Value)
a.metrics.RecordGameStep()
callData := types.StepCallData{ callData := types.StepCallData{
ClaimIndex: uint64(step.LeafClaim.ContractIndex), ClaimIndex: uint64(step.LeafClaim.ContractIndex),
IsAttack: step.IsAttack, IsAttack: step.IsAttack,
......
...@@ -2,9 +2,14 @@ package fault ...@@ -2,9 +2,14 @@ package fault
import ( import (
"context" "context"
"errors"
"testing" "testing"
"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/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/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
...@@ -13,19 +18,143 @@ import ( ...@@ -13,19 +18,143 @@ import (
// TestShouldResolve tests the resolution logic. // TestShouldResolve tests the resolution logic.
func TestShouldResolve(t *testing.T) { func TestShouldResolve(t *testing.T) {
log := testlog.Logger(t, log.LvlCrit)
t.Run("AgreeWithProposedOutput", func(t *testing.T) { t.Run("AgreeWithProposedOutput", func(t *testing.T) {
agent := NewAgent(nil, 0, nil, nil, nil, true, log) agent, _, _ := setupTestAgent(t, true)
require.False(t, agent.shouldResolve(context.Background(), types.GameStatusDefenderWon)) require.False(t, agent.shouldResolve(gameTypes.GameStatusDefenderWon))
require.True(t, agent.shouldResolve(context.Background(), types.GameStatusChallengerWon)) require.True(t, agent.shouldResolve(gameTypes.GameStatusChallengerWon))
require.False(t, agent.shouldResolve(context.Background(), types.GameStatusInProgress)) require.False(t, agent.shouldResolve(gameTypes.GameStatusInProgress))
}) })
t.Run("DisagreeWithProposedOutput", func(t *testing.T) { t.Run("DisagreeWithProposedOutput", func(t *testing.T) {
agent := NewAgent(nil, 0, nil, nil, nil, false, log) agent, _, _ := setupTestAgent(t, false)
require.True(t, agent.shouldResolve(context.Background(), types.GameStatusDefenderWon)) require.True(t, agent.shouldResolve(gameTypes.GameStatusDefenderWon))
require.False(t, agent.shouldResolve(context.Background(), types.GameStatusChallengerWon)) require.False(t, agent.shouldResolve(gameTypes.GameStatusChallengerWon))
require.False(t, agent.shouldResolve(context.Background(), types.GameStatusInProgress)) require.False(t, agent.shouldResolve(gameTypes.GameStatusInProgress))
}) })
} }
func TestDoNotMakeMovesWhenGameIsResolvable(t *testing.T) {
ctx := context.Background()
tests := []struct {
name string
agreeWithProposedOutput bool
callResolveStatus gameTypes.GameStatus
shouldResolve bool
}{
{
name: "Agree_Losing",
agreeWithProposedOutput: true,
callResolveStatus: gameTypes.GameStatusDefenderWon,
shouldResolve: false,
},
{
name: "Agree_Winning",
agreeWithProposedOutput: true,
callResolveStatus: gameTypes.GameStatusChallengerWon,
shouldResolve: true,
},
{
name: "Disagree_Losing",
agreeWithProposedOutput: false,
callResolveStatus: gameTypes.GameStatusChallengerWon,
shouldResolve: false,
},
{
name: "Disagree_Winning",
agreeWithProposedOutput: false,
callResolveStatus: gameTypes.GameStatusDefenderWon,
shouldResolve: true,
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
agent, claimLoader, responder := setupTestAgent(t, test.agreeWithProposedOutput)
responder.callResolveStatus = test.callResolveStatus
require.NoError(t, agent.Act(ctx))
require.Equal(t, 1, responder.callResolveCount, "should check if game is resolvable")
require.Zero(t, claimLoader.callCount, "should not fetch claims for resolvable game")
if test.shouldResolve {
require.EqualValues(t, 1, responder.resolveCount, "should resolve winning game")
} else {
require.Zero(t, responder.resolveCount, "should not resolve losing game")
}
})
}
}
func TestLoadClaimsWhenGameNotResolvable(t *testing.T) {
// Checks that if the game isn't resolvable, that the agent continues on to start checking claims
agent, claimLoader, responder := setupTestAgent(t, false)
responder.callResolveErr = errors.New("game is not resolvable")
depth := 4
claimBuilder := test.NewClaimBuilder(t, depth, alphabet.NewTraceProvider("abcdefg", uint64(depth)))
claimLoader.claims = []types.Claim{
claimBuilder.CreateRootClaim(true),
}
require.NoError(t, agent.Act(context.Background()))
require.EqualValues(t, 1, claimLoader.callCount, "should load claims for unresolvable game")
}
func setupTestAgent(t *testing.T, agreeWithProposedOutput bool) (*Agent, *stubClaimLoader, *stubResponder) {
logger := testlog.Logger(t, log.LvlInfo)
claimLoader := &stubClaimLoader{}
depth := 4
trace := alphabet.NewTraceProvider("abcd", uint64(depth))
responder := &stubResponder{}
updater := &stubUpdater{}
agent := NewAgent(metrics.NoopMetrics, claimLoader, depth, trace, responder, updater, agreeWithProposedOutput, logger)
return agent, claimLoader, responder
}
type stubClaimLoader struct {
callCount int
claims []types.Claim
}
func (s *stubClaimLoader) FetchClaims(ctx context.Context) ([]types.Claim, error) {
s.callCount++
return s.claims, nil
}
type stubResponder struct {
callResolveCount int
callResolveStatus gameTypes.GameStatus
callResolveErr error
resolveCount int
resolveErr error
}
func (s *stubResponder) CallResolve(ctx context.Context) (gameTypes.GameStatus, error) {
s.callResolveCount++
return s.callResolveStatus, s.callResolveErr
}
func (s *stubResponder) Resolve(ctx context.Context) error {
s.resolveCount++
return s.resolveErr
}
func (s *stubResponder) Respond(ctx context.Context, response types.Claim) error {
panic("Not implemented")
}
func (s *stubResponder) Step(ctx context.Context, stepData types.StepCallData) error {
panic("Not implemented")
}
type stubUpdater struct {
}
func (s *stubUpdater) UpdateOracle(ctx context.Context, data *types.PreimageOracleData) error {
panic("Not implemented")
}
...@@ -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,17 +11,18 @@ import ( ...@@ -11,17 +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-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)
} }
...@@ -30,13 +31,13 @@ type GamePlayer struct { ...@@ -30,13 +31,13 @@ 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(
ctx context.Context, ctx context.Context,
logger log.Logger, logger log.Logger,
m metrics.Metricer,
cfg *config.Config, cfg *config.Config,
dir string, dir string,
addr common.Address, addr common.Address,
...@@ -55,14 +56,14 @@ func NewGamePlayer( ...@@ -55,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
...@@ -79,7 +80,7 @@ func NewGamePlayer( ...@@ -79,7 +80,7 @@ func NewGamePlayer(
var updater types.OracleUpdater var updater types.OracleUpdater
switch cfg.TraceType { switch cfg.TraceType {
case config.TraceTypeCannon: case config.TraceTypeCannon:
cannonProvider, err := cannon.NewTraceProvider(ctx, logger, cfg, client, dir, addr) cannonProvider, err := cannon.NewTraceProvider(ctx, logger, m, cfg, client, dir, addr)
if err != nil { if err != nil {
return nil, fmt.Errorf("create cannon trace provider: %w", err) return nil, fmt.Errorf("create cannon trace provider: %w", err)
} }
...@@ -105,36 +106,36 @@ func NewGamePlayer( ...@@ -105,36 +106,36 @@ func NewGamePlayer(
} }
return &GamePlayer{ return &GamePlayer{
act: NewAgent(loader, int(gameDepth), provider, responder, updater, cfg.AgreeWithProposedOutput, logger).Act, act: NewAgent(m, loader, int(gameDepth), provider, responder, updater, cfg.AgreeWithProposedOutput, logger).Act,
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)
...@@ -143,11 +144,11 @@ func (g *GamePlayer) logGameStatus(ctx context.Context, status types.GameStatus) ...@@ -143,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)
...@@ -157,22 +158,21 @@ func (g *GamePlayer) logGameStatus(ctx context.Context, status types.GameStatus) ...@@ -157,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)
......
...@@ -11,6 +11,7 @@ import ( ...@@ -11,6 +11,7 @@ import (
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/ethereum-optimism/optimism/op-challenger/config" "github.com/ethereum-optimism/optimism/op-challenger/config"
oplog "github.com/ethereum-optimism/optimism/op-service/log" oplog "github.com/ethereum-optimism/optimism/op-service/log"
...@@ -30,6 +31,7 @@ type cmdExecutor func(ctx context.Context, l log.Logger, binary string, args ... ...@@ -30,6 +31,7 @@ type cmdExecutor func(ctx context.Context, l log.Logger, binary string, args ...
type Executor struct { type Executor struct {
logger log.Logger logger log.Logger
metrics CannonMetricer
l1 string l1 string
l2 string l2 string
inputs LocalGameInputs inputs LocalGameInputs
...@@ -40,13 +42,15 @@ type Executor struct { ...@@ -40,13 +42,15 @@ type Executor struct {
l2Genesis string l2Genesis string
absolutePreState string absolutePreState string
snapshotFreq uint snapshotFreq uint
infoFreq uint
selectSnapshot snapshotSelect selectSnapshot snapshotSelect
cmdExecutor cmdExecutor cmdExecutor cmdExecutor
} }
func NewExecutor(logger log.Logger, cfg *config.Config, inputs LocalGameInputs) *Executor { func NewExecutor(logger log.Logger, m CannonMetricer, cfg *config.Config, inputs LocalGameInputs) *Executor {
return &Executor{ return &Executor{
logger: logger, logger: logger,
metrics: m,
l1: cfg.L1EthRpc, l1: cfg.L1EthRpc,
l2: cfg.CannonL2, l2: cfg.CannonL2,
inputs: inputs, inputs: inputs,
...@@ -57,6 +61,7 @@ func NewExecutor(logger log.Logger, cfg *config.Config, inputs LocalGameInputs) ...@@ -57,6 +61,7 @@ func NewExecutor(logger log.Logger, cfg *config.Config, inputs LocalGameInputs)
l2Genesis: cfg.CannonL2GenesisPath, l2Genesis: cfg.CannonL2GenesisPath,
absolutePreState: cfg.CannonAbsolutePreState, absolutePreState: cfg.CannonAbsolutePreState,
snapshotFreq: cfg.CannonSnapshotFreq, snapshotFreq: cfg.CannonSnapshotFreq,
infoFreq: cfg.CannonInfoFreq,
selectSnapshot: findStartingSnapshot, selectSnapshot: findStartingSnapshot,
cmdExecutor: runCmd, cmdExecutor: runCmd,
} }
...@@ -76,6 +81,7 @@ func (e *Executor) GenerateProof(ctx context.Context, dir string, i uint64) erro ...@@ -76,6 +81,7 @@ func (e *Executor) GenerateProof(ctx context.Context, dir string, i uint64) erro
"--input", start, "--input", start,
"--output", lastGeneratedState, "--output", lastGeneratedState,
"--meta", "", "--meta", "",
"--info-at", "%" + strconv.FormatUint(uint64(e.infoFreq), 10),
"--proof-at", "=" + strconv.FormatUint(i, 10), "--proof-at", "=" + strconv.FormatUint(i, 10),
"--proof-fmt", filepath.Join(proofDir, "%d.json.gz"), "--proof-fmt", filepath.Join(proofDir, "%d.json.gz"),
"--snapshot-at", "%" + strconv.FormatUint(uint64(e.snapshotFreq), 10), "--snapshot-at", "%" + strconv.FormatUint(uint64(e.snapshotFreq), 10),
...@@ -116,7 +122,13 @@ func (e *Executor) GenerateProof(ctx context.Context, dir string, i uint64) erro ...@@ -116,7 +122,13 @@ func (e *Executor) GenerateProof(ctx context.Context, dir string, i uint64) erro
return fmt.Errorf("could not create proofs directory %v: %w", proofDir, err) return fmt.Errorf("could not create proofs directory %v: %w", proofDir, err)
} }
e.logger.Info("Generating trace", "proof", i, "cmd", e.cannon, "args", strings.Join(args, ", ")) e.logger.Info("Generating trace", "proof", i, "cmd", e.cannon, "args", strings.Join(args, ", "))
return e.cmdExecutor(ctx, e.logger.New("proof", i), e.cannon, args...) execStart := time.Now()
err = e.cmdExecutor(ctx, e.logger.New("proof", i), e.cannon, args...)
if err != nil {
execDuration := time.Since(execStart).Seconds()
e.metrics.RecordCannonExecutionTime(execDuration)
}
return err
} }
func runCmd(ctx context.Context, l log.Logger, binary string, args ...string) error { func runCmd(ctx context.Context, l log.Logger, binary string, args ...string) error {
......
...@@ -11,6 +11,7 @@ import ( ...@@ -11,6 +11,7 @@ import (
"time" "time"
"github.com/ethereum-optimism/optimism/op-challenger/config" "github.com/ethereum-optimism/optimism/op-challenger/config"
"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"
...@@ -29,6 +30,7 @@ func TestGenerateProof(t *testing.T) { ...@@ -29,6 +30,7 @@ func TestGenerateProof(t *testing.T) {
cfg.CannonServer = "./bin/op-program" cfg.CannonServer = "./bin/op-program"
cfg.CannonL2 = "http://localhost:9999" cfg.CannonL2 = "http://localhost:9999"
cfg.CannonSnapshotFreq = 500 cfg.CannonSnapshotFreq = 500
cfg.CannonInfoFreq = 900
inputs := LocalGameInputs{ inputs := LocalGameInputs{
L1Head: common.Hash{0x11}, L1Head: common.Hash{0x11},
...@@ -38,7 +40,7 @@ func TestGenerateProof(t *testing.T) { ...@@ -38,7 +40,7 @@ func TestGenerateProof(t *testing.T) {
L2BlockNumber: big.NewInt(3333), L2BlockNumber: big.NewInt(3333),
} }
captureExec := func(t *testing.T, cfg config.Config, proofAt uint64) (string, string, map[string]string) { captureExec := func(t *testing.T, cfg config.Config, proofAt uint64) (string, string, map[string]string) {
executor := NewExecutor(testlog.Logger(t, log.LvlInfo), &cfg, inputs) executor := NewExecutor(testlog.Logger(t, log.LvlInfo), metrics.NoopMetrics, &cfg, inputs)
executor.selectSnapshot = func(logger log.Logger, dir string, absolutePreState string, i uint64) (string, error) { executor.selectSnapshot = func(logger log.Logger, dir string, absolutePreState string, i uint64) (string, error) {
return input, nil return input, nil
} }
...@@ -81,6 +83,7 @@ func TestGenerateProof(t *testing.T) { ...@@ -81,6 +83,7 @@ func TestGenerateProof(t *testing.T) {
require.Equal(t, "=150000000", args["--proof-at"]) require.Equal(t, "=150000000", args["--proof-at"])
require.Equal(t, "=150000001", args["--stop-at"]) require.Equal(t, "=150000001", args["--stop-at"])
require.Equal(t, "%500", args["--snapshot-at"]) require.Equal(t, "%500", args["--snapshot-at"])
require.Equal(t, "%900", args["--info-at"])
// Slight quirk of how we pair off args // Slight quirk of how we pair off args
// The server binary winds up as the key and the first arg --server as the value which has no value // The server binary winds up as the key and the first arg --server as the value which has no value
// Then everything else pairs off correctly again // Then everything else pairs off correctly again
......
...@@ -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"`
...@@ -33,6 +34,10 @@ type proofData struct { ...@@ -33,6 +34,10 @@ type proofData struct {
OracleOffset uint32 `json:"oracle-offset,omitempty"` OracleOffset uint32 `json:"oracle-offset,omitempty"`
} }
type CannonMetricer interface {
RecordCannonExecutionTime(t float64)
}
type ProofGenerator interface { type ProofGenerator interface {
// GenerateProof executes cannon to generate a proof at the specified trace index in dataDir. // GenerateProof executes cannon to generate a proof at the specified trace index in dataDir.
GenerateProof(ctx context.Context, dataDir string, proofAt uint64) error GenerateProof(ctx context.Context, dataDir string, proofAt uint64) error
...@@ -51,7 +56,7 @@ type CannonTraceProvider struct { ...@@ -51,7 +56,7 @@ type CannonTraceProvider struct {
lastProof *proofData lastProof *proofData
} }
func NewTraceProvider(ctx context.Context, logger log.Logger, cfg *config.Config, l1Client bind.ContractCaller, dir string, gameAddr common.Address) (*CannonTraceProvider, error) { func NewTraceProvider(ctx context.Context, logger log.Logger, m CannonMetricer, cfg *config.Config, l1Client bind.ContractCaller, dir string, gameAddr common.Address) (*CannonTraceProvider, error) {
l2Client, err := ethclient.DialContext(ctx, cfg.CannonL2) l2Client, err := ethclient.DialContext(ctx, cfg.CannonL2)
if err != nil { if err != nil {
return nil, fmt.Errorf("dial l2 client %v: %w", cfg.CannonL2, err) return nil, fmt.Errorf("dial l2 client %v: %w", cfg.CannonL2, err)
...@@ -65,15 +70,15 @@ func NewTraceProvider(ctx context.Context, logger log.Logger, cfg *config.Config ...@@ -65,15 +70,15 @@ func NewTraceProvider(ctx context.Context, logger log.Logger, cfg *config.Config
if err != nil { if err != nil {
return nil, fmt.Errorf("fetch local game inputs: %w", err) return nil, fmt.Errorf("fetch local game inputs: %w", err)
} }
return NewTraceProviderFromInputs(logger, cfg, localInputs, dir), nil return NewTraceProviderFromInputs(logger, m, cfg, localInputs, dir), nil
} }
func NewTraceProviderFromInputs(logger log.Logger, cfg *config.Config, localInputs LocalGameInputs, dir string) *CannonTraceProvider { func NewTraceProviderFromInputs(logger log.Logger, m CannonMetricer, cfg *config.Config, localInputs LocalGameInputs, dir string) *CannonTraceProvider {
return &CannonTraceProvider{ return &CannonTraceProvider{
logger: logger, logger: logger,
dir: dir, dir: dir,
prestate: cfg.CannonAbsolutePreState, prestate: cfg.CannonAbsolutePreState,
generator: NewExecutor(logger, cfg, localInputs), generator: NewExecutor(logger, m, cfg, localInputs),
} }
} }
...@@ -82,7 +87,7 @@ func (p *CannonTraceProvider) Get(ctx context.Context, i uint64) (common.Hash, e ...@@ -82,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")
...@@ -118,6 +123,18 @@ func (p *CannonTraceProvider) AbsolutePreState(ctx context.Context) ([]byte, err ...@@ -118,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) {
...@@ -147,9 +164,13 @@ func (p *CannonTraceProvider) loadProof(ctx context.Context, i uint64) (*proofDa ...@@ -147,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)
......
...@@ -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,10 +69,11 @@ func NewService(ctx context.Context, logger log.Logger, cfg *config.Config) (*Se ...@@ -69,10 +69,11 @@ 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, cfg, dir, addr, txMgr, client) return fault.NewGamePlayer(ctx, logger, m, cfg, dir, addr, txMgr, client)
}) })
monitor := newGameMonitor(logger, cl, loader, sched, cfg.GameWindow, client.BlockNumber, cfg.GameAllowlist) monitor := newGameMonitor(logger, cl, loader, sched, cfg.GameWindow, client.BlockNumber, cfg.GameAllowlist)
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
...@@ -12,3 +12,13 @@ var NoopMetrics Metricer = new(noopMetrics) ...@@ -12,3 +12,13 @@ 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) RecordCannonExecutionTime(t float64) {}
func (*noopMetrics) RecordGamesStatus(inProgress, defenderWon, challengerWon int) {}
func (*noopMetrics) RecordGameUpdateScheduled() {}
func (*noopMetrics) RecordGameUpdateCompleted() {}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
#!/bin/bash
set -euo pipefail
RPC=${1:?Must specify RPC URL}
GAME_ADDR=${2:?Must specify game address}
SIGNER_ARGS="${@:3}"
# Perform the move.
RESULT_DATA=$(cast send --rpc-url "${RPC}" ${SIGNER_ARGS} "${GAME_ADDR}" "resolve()" --json)
RESULT=$(echo "${RESULT_DATA}" | jq -r '.logs[0].topics[1]' | cast to-dec)
if [[ "${RESULT}" == "0" ]]
then
RESULT="In Progress"
elif [[ "${RESULT}" == "1" ]]
then
RESULT="Challenger Wins"
elif [[ "${RESULT}" == "2" ]]
then
RESULT="Defender Wins"
fi
echo "Result: $RESULT"
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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