Commit 07d3836c authored by mergify[bot]'s avatar mergify[bot] Committed by GitHub

Merge branch 'develop' into inphi/fpvm-oob

parents 1c846a83 5bbfc8ec
---
'@eth-optimism/sdk': patch
---
Fixed missing indexes for multicall support
......@@ -24,7 +24,7 @@ import (
)
var (
StepBytes4 = crypto.Keccak256([]byte("Step(bytes,bytes)"))[:4]
StepBytes4 = crypto.Keccak256([]byte("step(bytes,bytes)"))[:4]
CheatBytes4 = crypto.Keccak256([]byte("cheat(uint256,bytes32,bytes32,uint256)"))[:4]
LoadKeccak256PreimagePartBytes4 = crypto.Keccak256([]byte("loadKeccak256PreimagePart(uint256,bytes)"))[:4]
)
......
......@@ -63,6 +63,8 @@ func TestEVM(t *testing.T) {
if strings.HasPrefix(f.Name(), "oracle") {
oracle = staticOracle(t, []byte("hello world"))
}
// Short-circuit early for exit_group.bin
exitGroup := f.Name() == "exit_group.bin"
env, evmState := NewEVMEnv(contracts, addrs)
env.Config.Tracer = tracer
......@@ -83,6 +85,9 @@ func TestEVM(t *testing.T) {
if us.state.PC == endAddr {
break
}
if exitGroup && us.state.Exited {
break
}
insn := state.Memory.GetMemory(state.PC)
t.Logf("step: %4d pc: 0x%08x insn: 0x%08x", state.Step, state.PC, insn)
......@@ -122,11 +127,17 @@ func TestEVM(t *testing.T) {
require.Equal(t, hexutil.Bytes(uniPost).String(), hexutil.Bytes(evmPost).String(),
"mipsevm produced different state than EVM")
}
require.Equal(t, uint32(endAddr), state.PC, "must reach end")
// inspect test result
done, result := state.Memory.GetMemory(baseAddrEnd+4), state.Memory.GetMemory(baseAddrEnd+8)
require.Equal(t, done, uint32(1), "must be done")
require.Equal(t, result, uint32(1), "must have success result")
if exitGroup {
require.NotEqual(t, uint32(endAddr), us.state.PC, "must not reach end")
require.True(t, us.state.Exited, "must set exited state")
require.Equal(t, uint8(1), us.state.ExitCode, "must exit with 1")
} else {
require.Equal(t, uint32(endAddr), state.PC, "must reach end")
// inspect test result
done, result := state.Memory.GetMemory(baseAddrEnd+4), state.Memory.GetMemory(baseAddrEnd+8)
require.Equal(t, done, uint32(1), "must be done")
require.Equal(t, result, uint32(1), "must have success result")
}
})
}
}
......
.section .test, "x"
.balign 4
.set noreorder
.global test
.ent test
test:
li $v0, 4045
syscall
lui $t0, 0x4000
subu $v0, $v0, $t0
sltiu $v0, $v0, 1
# save results
lui $s0, 0xbfff # Load the base address 0xbffffff0
ori $s0, 0xfff0
ori $s1, $0, 1 # Prepare the 'done' status
sw $v0, 8($s0) # Set the test result
sw $s1, 4($s0) # Set 'done'
$done:
jr $ra
nop
.end test
.section .test, "x"
.balign 4
.set noreorder
.global test
.ent test
test:
li $v0, 4120
syscall
li $t0, 0x1
subu $v0, $v0, $t0
sltiu $v0, $v0, 1
# save results
lui $s0, 0xbfff # Load the base address 0xbffffff0
ori $s0, 0xfff0
ori $s1, $0, 1 # Prepare the 'done' status
sw $v0, 8($s0) # Set the test result
sw $s1, 4($s0) # Set 'done'
$done:
jr $ra
nop
.end test
.section .test, "x"
.balign 4
.set noreorder
.global test
.ent test
test:
li $a0, 1
li $v0, 4246
syscall
# Unreachable ....
# set test result to fail.
# Test runner should short-circuit before reaching this point.
li $v0, 0
# save results
lui $s0, 0xbfff # Load the base address 0xbffffff0
ori $s0, 0xfff0
ori $s1, $0, 1 # Prepare the 'done' status
sw $v0, 8($s0) # Set the test result
sw $s1, 4($s0) # Set 'done'
$done:
jr $ra
nop
.end test
.section .test, "x"
.balign 4
.set noreorder
.global test
.ent test
test:
# fnctl(0, 3)
li $v0, 4055
li $a0, 0x0
li $a1, 0x3
syscall
sltiu $v0, $v0, 1
# save results
lui $s0, 0xbfff # Load the base address 0xbffffff0
ori $s0, 0xfff0
ori $s1, $0, 1 # Prepare the 'done' status
sw $v0, 8($s0) # Set the test result
sw $s1, 4($s0) # Set 'done'
$done:
jr $ra
nop
.end test
.section .test, "x"
.balign 4
.set noreorder
.global test
.ent test
test:
li $v0, 4090
lui $a0, 0x3000
li $a1, 4096
syscall
lui $t0, 0x3000
subu $v0, $v0, $t0
sltiu $v0, $v0, 1
# save results
lui $s0, 0xbfff # Load the base address 0xbffffff0
ori $s0, 0xfff0
ori $s1, $0, 1 # Prepare the 'done' status
sw $v0, 8($s0) # Set the test result
sw $s1, 4($s0) # Set 'done'
$done:
jr $ra
nop
.end test
......@@ -35,6 +35,9 @@ func TestState(t *testing.T) {
if strings.HasPrefix(f.Name(), "oracle") {
oracle = staticOracle(t, []byte("hello world"))
}
// Short-circuit early for exit_group.bin
exitGroup := f.Name() == "exit_group.bin"
// TODO: currently tests are compiled as flat binary objects
// We can use more standard tooling to compile them to ELF files and get remove maketests.py
fn := path.Join("open_mips_tests/test/bin", f.Name())
......@@ -57,14 +60,24 @@ func TestState(t *testing.T) {
if us.state.PC == endAddr {
break
}
if exitGroup && us.state.Exited {
break
}
_, err := us.Step(false)
require.NoError(t, err)
}
require.Equal(t, uint32(endAddr), us.state.PC, "must reach end")
// inspect test result
done, result := state.Memory.GetMemory(baseAddrEnd+4), state.Memory.GetMemory(baseAddrEnd+8)
require.Equal(t, done, uint32(1), "must be done")
require.Equal(t, result, uint32(1), "must have success result")
if exitGroup {
require.NotEqual(t, uint32(endAddr), us.state.PC, "must not reach end")
require.True(t, us.state.Exited, "must set exited state")
require.Equal(t, uint8(1), us.state.ExitCode, "must exit with 1")
} else {
require.Equal(t, uint32(endAddr), us.state.PC, "must reach end")
done, result := state.Memory.GetMemory(baseAddrEnd+4), state.Memory.GetMemory(baseAddrEnd+8)
// inspect test result
require.Equal(t, done, uint32(1), "must be done")
require.Equal(t, result, uint32(1), "must have success result")
}
})
}
}
......
......@@ -30,7 +30,7 @@ You’ll need the following software installed to follow this tutorial:
- [Git](https://git-scm.com/)
- [Go](https://go.dev/)
- [Node](https://nodejs.org/en/)
- [Yarn](https://classic.yarnpkg.com/lang/en/docs/install/)
- [Pnpm](https://classic.yarnpkg.com/lang/en/docs/install/)
- [Foundry](https://github.com/foundry-rs/foundry#installation)
- [Make](https://linux.die.net/man/1/make)
- [jq](https://github.com/jqlang/jq)
......@@ -44,7 +44,7 @@ This tutorial was checked on:
| git, curl, jq, and make | OS default | `sudo apt install -y git curl make jq` |
| Go | 1.20 | `sudo apt update` <br> `wget https://go.dev/dl/go1.20.linux-amd64.tar.gz` <br> `tar xvzf go1.20.linux-amd64.tar.gz` <br> `sudo cp go/bin/go /usr/bin/go` <br> `sudo mv go /usr/lib` <br> `echo export GOROOT=/usr/lib/go >> ~/.bashrc`
| Node | 16.19.0 | `curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash -` <br> `sudo apt-get install -y nodejs npm`
| yarn | 1.22.19 | `sudo npm install -g yarn`
| pnpm | 8.5.6 | `sudo npm install -g pnpm`
| Foundry | 0.2.0 | `yarn install:foundry`
## Build the Source Code
......@@ -69,14 +69,14 @@ We’re going to be spinning up an EVM Rollup from the OP Stack source code. Yo
1. Install required modules. This is a slow process, while it is running you can already start building `op-geth`, as shown below.
```bash
yarn install
pnpm install
```
1. Build the various packages inside of the Optimism Monorepo.
```bash
make op-node op-batcher op-proposer
yarn build
pnpm build
```
### Build op-geth
......@@ -278,7 +278,7 @@ We’ve set up the L1 side of things, but now we need to set up the L2 side of t
You should then see the `genesis.json` and `rollup.json` files inside the `op-node` package.
1. Next, generate the `jwt.txt` file with the following command:
1. Next, generate the `jwt.txt` file with the following command:
```bash
openssl rand -hex 32 > jwt.txt
......@@ -307,24 +307,6 @@ We’re almost ready to run our chain! Now we just need to run a few commands to
mkdir datadir
```
1. Put a password file into the data directory folder:
```bash
echo "pwd" > datadir/password
```
1. Put the `Sequencer` private key into the data directory folder (don’t include a “0x” prefix):
```bash
echo "<SEQUENCER KEY HERE>" > datadir/block-signer-key
```
1. Import the key into `op-geth`:
```bash
./build/bin/geth account import --datadir=datadir --password=datadir/password datadir/block-signer-key
```
1. Next we need to initialize `op-geth` with the genesis file we generated and copied earlier:
```bash
......@@ -344,7 +326,6 @@ Set these environment variables for the configuration
| Variable | Value |
| -------------- | -
| `SEQ_ADDR` | Address of the `Sequencer` account
| `SEQ_KEY` | Private key of the `Sequencer` account
| `BATCHER_KEY` | Private key of the `Batcher` accounts, which should have at least 1 ETH
| `PROPOSER_KEY` | Private key of the `Proposer` account
......@@ -360,32 +341,27 @@ Run `op-geth` with the following commands.
cd ~/op-geth
./build/bin/geth \
--datadir ./datadir \
--http \
--http.corsdomain="*" \
--http.vhosts="*" \
--http.addr=0.0.0.0 \
--http.api=web3,debug,eth,txpool,net,engine \
--ws \
--ws.addr=0.0.0.0 \
--ws.port=8546 \
--ws.origins="*" \
--ws.api=debug,eth,txpool,net,engine \
--syncmode=full \
--gcmode=archive \
--nodiscover \
--maxpeers=0 \
--networkid=42069 \
--authrpc.vhosts="*" \
--authrpc.addr=0.0.0.0 \
--authrpc.port=8551 \
--authrpc.jwtsecret=./jwt.txt \
--rollup.disabletxpoolgossip=true \
--password=./datadir/password \
--allow-insecure-unlock \
--mine \
--miner.etherbase=$SEQ_ADDR \
--unlock=$SEQ_ADDR
--datadir ./datadir \
--http \
--http.corsdomain="*" \
--http.vhosts="*" \
--http.addr=0.0.0.0 \
--http.api=web3,debug,eth,txpool,net,engine \
--ws \
--ws.addr=0.0.0.0 \
--ws.port=8546 \
--ws.origins="*" \
--ws.api=debug,eth,txpool,net,engine \
--syncmode=full \
--gcmode=archive \
--nodiscover \
--maxpeers=0 \
--networkid=42069 \
--authrpc.vhosts="*" \
--authrpc.addr=0.0.0.0 \
--authrpc.port=8551 \
--authrpc.jwtsecret=./jwt.txt \
--rollup.disabletxpoolgossip=true
```
And `op-geth` should be running! You should see some output, but you won’t see any blocks being created yet because `op-geth` is driven by the `op-node`. We’ll need to get that running next.
......@@ -443,7 +419,7 @@ cd ~/optimism/op-node
./bin/op-node \
--l2=http://localhost:8551 \
--l2.jwt-secret=./jwt.txt \
--l2.jwt-secret=./jwt.txt \
--sequencer.enabled \
--sequencer.l1-confs=3 \
--verifier.l1-confs=3 \
......
package api
import (
"math/big"
"net/http"
"net/http/httptest"
"testing"
......@@ -25,7 +26,7 @@ func (mbv *MockBridgeView) DepositsByAddress(address common.Address) ([]*databas
{
Deposit: database.Deposit{
GUID: uuid.MustParse(guid1),
InitiatedL1EventGUID: guid2,
InitiatedL1EventGUID: uuid.MustParse(guid2),
Tx: database.Transaction{},
TokenPair: database.TokenPair{},
},
......@@ -34,13 +35,28 @@ func (mbv *MockBridgeView) DepositsByAddress(address common.Address) ([]*databas
}, nil
}
// DepositsByAddress mocks returning deposits by an address
func (mbv *MockBridgeView) DepositByMessageNonce(nonce *big.Int) (*database.Deposit, error) {
return &database.Deposit{
GUID: uuid.MustParse(guid1),
InitiatedL1EventGUID: uuid.MustParse(guid2),
Tx: database.Transaction{},
TokenPair: database.TokenPair{},
}, nil
}
// LatestDepositMessageNonce mocks returning the latest cross domain message nonce for a deposit
func (mbv *MockBridgeView) LatestDepositMessageNonce() (*big.Int, error) {
return big.NewInt(0), nil
}
// WithdrawalsByAddress mocks returning withdrawals by an address
func (mbv *MockBridgeView) WithdrawalsByAddress(address common.Address) ([]*database.WithdrawalWithTransactionHashes, error) {
return []*database.WithdrawalWithTransactionHashes{
{
Withdrawal: database.Withdrawal{
GUID: uuid.MustParse(guid2),
InitiatedL2EventGUID: guid1,
InitiatedL2EventGUID: uuid.MustParse(guid1),
WithdrawalHash: common.HexToHash("0x456"),
Tx: database.Transaction{},
TokenPair: database.TokenPair{},
......@@ -50,6 +66,33 @@ func (mbv *MockBridgeView) WithdrawalsByAddress(address common.Address) ([]*data
}, nil
}
// WithdrawalsByMessageNonce mocks returning withdrawals by a withdrawal hash
func (mbv *MockBridgeView) WithdrawalByMessageNonce(nonce *big.Int) (*database.Withdrawal, error) {
return &database.Withdrawal{
GUID: uuid.MustParse(guid2),
InitiatedL2EventGUID: uuid.MustParse(guid1),
WithdrawalHash: common.HexToHash("0x456"),
Tx: database.Transaction{},
TokenPair: database.TokenPair{},
}, nil
}
// WithdrawalsByHash mocks returning withdrawals by a withdrawal hash
func (mbv *MockBridgeView) WithdrawalByHash(address common.Hash) (*database.Withdrawal, error) {
return &database.Withdrawal{
GUID: uuid.MustParse(guid2),
InitiatedL2EventGUID: uuid.MustParse(guid1),
WithdrawalHash: common.HexToHash("0x456"),
Tx: database.Transaction{},
TokenPair: database.TokenPair{},
}, nil
}
// LatestWithdrawalMessageNonce mocks returning the latest cross domain message nonce for a withdrawal
func (mbv *MockBridgeView) LatestWithdrawalMessageNonce() (*big.Int, error) {
return big.NewInt(0), nil
}
func TestHealthz(t *testing.T) {
api := NewApi(&MockBridgeView{})
request, err := http.NewRequest("GET", "/healthz", nil)
......
......@@ -5,6 +5,8 @@ import (
"errors"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/google/uuid"
"gorm.io/gorm"
......@@ -21,6 +23,15 @@ type BlockHeader struct {
Timestamp uint64
}
func BlockHeaderFromGethHeader(header *types.Header) BlockHeader {
return BlockHeader{
Hash: header.Hash(),
ParentHash: header.ParentHash,
Number: U256{Int: header.Number},
Timestamp: header.Time,
}
}
type L1BlockHeader struct {
BlockHeader
}
......@@ -41,8 +52,9 @@ type LegacyStateBatch struct {
}
type OutputProposal struct {
OutputRoot common.Hash `gorm:"primaryKey;serializer:json"`
L2BlockNumber U256
OutputRoot common.Hash `gorm:"primaryKey;serializer:json"`
L2BlockNumber U256
L1ContractEventGUID uuid.UUID
}
......
......@@ -2,6 +2,8 @@ package database
import (
"errors"
"fmt"
"math/big"
"gorm.io/gorm"
......@@ -30,7 +32,17 @@ type TokenPair struct {
type Deposit struct {
GUID uuid.UUID `gorm:"primaryKey"`
InitiatedL1EventGUID string
InitiatedL1EventGUID uuid.UUID
// Since we're only currently indexing a single StandardBridge,
// the message nonce serves as a unique identifier for this
// deposit. Once this generalizes to more than 1 deployed
// bridge, we need to include the `CrossDomainMessenger` address
// such that the (messenger_addr, nonce) is the unique identifier
// for a bridge msg
SentMessageNonce U256
FinalizedL2EventGUID *uuid.UUID
Tx Transaction `gorm:"embedded"`
TokenPair TokenPair `gorm:"embedded"`
......@@ -43,11 +55,19 @@ type DepositWithTransactionHash struct {
type Withdrawal struct {
GUID uuid.UUID `gorm:"primaryKey"`
InitiatedL2EventGUID string
InitiatedL2EventGUID uuid.UUID
// Since we're only currently indexing a single StandardBridge,
// the message nonce serves as a unique identifier for this
// withdrawal. Once this generalizes to more than 1 deployed
// bridge, we need to include the `CrossDomainMessenger` address
// such that the (messenger_addr, nonce) is the unique identifier
// for a bridge msg
SentMessageNonce U256
WithdrawalHash common.Hash `gorm:"serializer:json"`
ProvenL1EventGUID *string
FinalizedL1EventGUID *string
ProvenL1EventGUID *uuid.UUID
FinalizedL1EventGUID *uuid.UUID
Tx Transaction `gorm:"embedded"`
TokenPair TokenPair `gorm:"embedded"`
......@@ -63,16 +83,24 @@ type WithdrawalWithTransactionHashes struct {
type BridgeView interface {
DepositsByAddress(address common.Address) ([]*DepositWithTransactionHash, error)
DepositByMessageNonce(*big.Int) (*Deposit, error)
LatestDepositMessageNonce() (*big.Int, error)
WithdrawalsByAddress(address common.Address) ([]*WithdrawalWithTransactionHashes, error)
WithdrawalByMessageNonce(*big.Int) (*Withdrawal, error)
WithdrawalByHash(common.Hash) (*Withdrawal, error)
LatestWithdrawalMessageNonce() (*big.Int, error)
}
type BridgeDB interface {
BridgeView
StoreDeposits([]*Deposit) error
MarkFinalizedDepositEvent(uuid.UUID, uuid.UUID) error
StoreWithdrawals([]*Withdrawal) error
MarkProvenWithdrawalEvent(string, string) error
MarkFinalizedWithdrawalEvent(string, string) error
MarkProvenWithdrawalEvent(uuid.UUID, uuid.UUID) error
MarkFinalizedWithdrawalEvent(uuid.UUID, uuid.UUID) error
}
/**
......@@ -114,6 +142,46 @@ func (db *bridgeDB) DepositsByAddress(address common.Address) ([]*DepositWithTra
return deposits, nil
}
func (db *bridgeDB) DepositByMessageNonce(nonce *big.Int) (*Deposit, error) {
var deposit Deposit
result := db.gorm.First(&deposit, "sent_message_nonce = ?", U256{Int: nonce})
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, result.Error
}
return &deposit, nil
}
func (db *bridgeDB) LatestDepositMessageNonce() (*big.Int, error) {
var deposit Deposit
result := db.gorm.Order("sent_message_nonce DESC").Take(&deposit)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, result.Error
}
return deposit.SentMessageNonce.Int, nil
}
func (db *bridgeDB) MarkFinalizedDepositEvent(guid, finalizationEventGUID uuid.UUID) error {
var deposit Deposit
result := db.gorm.First(&deposit, "guid = ?", guid)
if result.Error != nil {
return result.Error
}
deposit.FinalizedL2EventGUID = &finalizationEventGUID
result = db.gorm.Save(&deposit)
return result.Error
}
// Withdrawals
func (db *bridgeDB) StoreWithdrawals(withdrawals []*Withdrawal) error {
......@@ -121,7 +189,7 @@ func (db *bridgeDB) StoreWithdrawals(withdrawals []*Withdrawal) error {
return result.Error
}
func (db *bridgeDB) MarkProvenWithdrawalEvent(guid, provenL1EventGuid string) error {
func (db *bridgeDB) MarkProvenWithdrawalEvent(guid, provenL1EventGuid uuid.UUID) error {
var withdrawal Withdrawal
result := db.gorm.First(&withdrawal, "guid = ?", guid)
if result.Error != nil {
......@@ -133,13 +201,17 @@ func (db *bridgeDB) MarkProvenWithdrawalEvent(guid, provenL1EventGuid string) er
return result.Error
}
func (db *bridgeDB) MarkFinalizedWithdrawalEvent(guid, finalizedL1EventGuid string) error {
func (db *bridgeDB) MarkFinalizedWithdrawalEvent(guid, finalizedL1EventGuid uuid.UUID) error {
var withdrawal Withdrawal
result := db.gorm.First(&withdrawal, "guid = ?", guid)
if result.Error != nil {
return result.Error
}
if withdrawal.ProvenL1EventGUID == nil {
return fmt.Errorf("withdrawal %s marked finalized prior to being proven", guid)
}
withdrawal.FinalizedL1EventGUID = &finalizedL1EventGuid
result = db.gorm.Save(&withdrawal)
return result.Error
......@@ -167,3 +239,45 @@ func (db *bridgeDB) WithdrawalsByAddress(address common.Address) ([]*WithdrawalW
return withdrawals, nil
}
func (db *bridgeDB) WithdrawalByMessageNonce(nonce *big.Int) (*Withdrawal, error) {
var withdrawal Withdrawal
result := db.gorm.First(&withdrawal, "sent_message_nonce = ?", U256{Int: nonce})
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, result.Error
}
return &withdrawal, nil
}
func (db *bridgeDB) WithdrawalByHash(hash common.Hash) (*Withdrawal, error) {
var withdrawal Withdrawal
result := db.gorm.First(&withdrawal, "withdrawal_hash = ?", hash.String())
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, result.Error
}
return &withdrawal, nil
}
func (db *bridgeDB) LatestWithdrawalMessageNonce() (*big.Int, error) {
var withdrawal Withdrawal
result := db.gorm.Order("sent_message_nonce DESC").Take(&withdrawal)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, result.Error
}
return withdrawal.SentMessageNonce.Int, nil
}
......@@ -4,6 +4,7 @@ import (
"gorm.io/gorm"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/google/uuid"
)
......@@ -22,6 +23,20 @@ type ContractEvent struct {
Timestamp uint64
}
func ContractEventFromGethLog(log *types.Log, timestamp uint64) ContractEvent {
return ContractEvent{
GUID: uuid.New(),
BlockHash: log.BlockHash,
TransactionHash: log.TxHash,
EventSignature: log.Topics[0],
LogIndex: uint64(log.Index),
Timestamp: timestamp,
}
}
type L1ContractEvent struct {
ContractEvent `gorm:"embedded"`
}
......
......@@ -10,14 +10,15 @@ CREATE TABLE IF NOT EXISTS l1_block_headers (
hash VARCHAR NOT NULL PRIMARY KEY,
parent_hash VARCHAR NOT NULL,
number UINT256,
timestamp INTEGER NOT NULL
timestamp INTEGER NOT NULL CHECK (timestamp > 0)
);
CREATE TABLE IF NOT EXISTS l2_block_headers (
hash VARCHAR NOT NULL PRIMARY KEY,
parent_hash VARCHAR NOT NULL,
number UINT256,
timestamp INTEGER NOT NULL
-- Block header
hash VARCHAR NOT NULL PRIMARY KEY,
parent_hash VARCHAR NOT NULL,
number UINT256,
timestamp INTEGER NOT NULL CHECK (timestamp > 0)
);
/**
......@@ -30,7 +31,7 @@ CREATE TABLE IF NOT EXISTS l1_contract_events (
transaction_hash VARCHAR NOT NULL,
event_signature VARCHAR NOT NULL,
log_index INTEGER NOT NULL,
timestamp INTEGER NOT NULL
timestamp INTEGER NOT NULL CHECK (timestamp > 0)
);
CREATE TABLE IF NOT EXISTS l2_contract_events (
......@@ -39,7 +40,7 @@ CREATE TABLE IF NOT EXISTS l2_contract_events (
transaction_hash VARCHAR NOT NULL,
event_signature VARCHAR NOT NULL,
log_index INTEGER NOT NULL,
timestamp INTEGER NOT NULL
timestamp INTEGER NOT NULL CHECK (timestamp > 0)
);
-- Tables that index finalization markers for L2 blocks.
......@@ -69,6 +70,10 @@ CREATE TABLE IF NOT EXISTS deposits (
-- Event causing the deposit
initiated_l1_event_guid VARCHAR NOT NULL REFERENCES l1_contract_events(guid),
sent_message_nonce UINT256 UNIQUE,
-- Finalization marker for the deposit
finalized_l2_event_guid VARCHAR REFERENCES l2_contract_events(guid),
-- Deposit information (do we need indexes on from/to?)
from_address VARCHAR NOT NULL,
......@@ -78,7 +83,7 @@ CREATE TABLE IF NOT EXISTS deposits (
l2_token_address VARCHAR NOT NULL,
amount UINT256,
data VARCHAR NOT NULL,
timestamp INTEGER NOT NULL
timestamp INTEGER NOT NULL CHECK (timestamp > 0)
);
CREATE TABLE IF NOT EXISTS withdrawals (
......@@ -86,6 +91,7 @@ CREATE TABLE IF NOT EXISTS withdrawals (
-- Event causing this withdrawal
initiated_l2_event_guid VARCHAR NOT NULL REFERENCES l2_contract_events(guid),
sent_message_nonce UINT256 UNIQUE,
-- Multistep (bedrock) process of a withdrawal
withdrawal_hash VARCHAR NOT NULL,
......@@ -101,5 +107,5 @@ CREATE TABLE IF NOT EXISTS withdrawals (
l2_token_address VARCHAR NOT NULL,
amount UINT256,
data VARCHAR NOT NULL,
timestamp INTEGER NOT NULL
timestamp INTEGER NOT NULL CHECK (timestamp > 0)
);
package processor
import (
"errors"
"fmt"
"github.com/ethereum-optimism/optimism/indexer/database"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/google/uuid"
)
type ProcessedContractEventLogIndexKey struct {
header common.Hash
index uint
}
type ProcessedContractEvents struct {
events []*database.ContractEvent
eventsBySignature map[common.Hash][]*database.ContractEvent
eventByLogIndex map[ProcessedContractEventLogIndexKey]*database.ContractEvent
eventLog map[uuid.UUID]*types.Log
}
func NewProcessedContractEvents() *ProcessedContractEvents {
return &ProcessedContractEvents{
events: []*database.ContractEvent{},
eventsBySignature: make(map[common.Hash][]*database.ContractEvent),
eventByLogIndex: make(map[ProcessedContractEventLogIndexKey]*database.ContractEvent),
eventLog: make(map[uuid.UUID]*types.Log),
}
}
func (p *ProcessedContractEvents) AddLog(log *types.Log, time uint64) *database.ContractEvent {
contractEvent := database.ContractEventFromGethLog(log, time)
p.events = append(p.events, &contractEvent)
p.eventsBySignature[contractEvent.EventSignature] = append(p.eventsBySignature[contractEvent.EventSignature], &contractEvent)
p.eventByLogIndex[ProcessedContractEventLogIndexKey{log.BlockHash, log.Index}] = &contractEvent
p.eventLog[contractEvent.GUID] = log
return &contractEvent
}
func UnpackLog(out interface{}, log *types.Log, name string, contractAbi *abi.ABI) error {
eventAbi, ok := contractAbi.Events[name]
if !ok {
return fmt.Errorf("event %s not present in supplied ABI", name)
} else if len(log.Topics) == 0 {
return errors.New("anonymous events are not supported")
} else if log.Topics[0] != eventAbi.ID {
return errors.New("event signature mismatch")
}
err := contractAbi.UnpackIntoInterface(out, name, log.Data)
if err != nil {
return err
}
// handle topics if present
if len(log.Topics) > 1 {
var indexedArgs abi.Arguments
for _, arg := range eventAbi.Inputs {
if arg.Indexed {
indexedArgs = append(indexedArgs, arg)
}
}
// The first topic (event signature) is omitted
err := abi.ParseTopics(out, indexedArgs, log.Topics[1:])
if err != nil {
return err
}
}
return nil
}
This diff is collapsed.
......@@ -7,6 +7,7 @@ import (
"github.com/ethereum-optimism/optimism/indexer/database"
"github.com/ethereum-optimism/optimism/indexer/node"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/google/uuid"
"github.com/ethereum/go-ethereum"
......@@ -124,8 +125,8 @@ func l2ProcessFn(processLog log.Logger, ethClient node.EthClient, l2Contracts L2
return err
}
numLogs := len(logs)
l2ContractEvents := make([]*database.L2ContractEvent, numLogs)
l2ContractEvents := make([]*database.L2ContractEvent, len(logs))
processedContractEvents := NewProcessedContractEvents()
for i, log := range logs {
header, ok := l2HeaderMap[log.BlockHash]
if !ok {
......@@ -133,16 +134,8 @@ func l2ProcessFn(processLog log.Logger, ethClient node.EthClient, l2Contracts L2
return errors.New("parsed log with a block hash not in this batch")
}
l2ContractEvents[i] = &database.L2ContractEvent{
ContractEvent: database.ContractEvent{
GUID: uuid.New(),
BlockHash: log.BlockHash,
TransactionHash: log.TxHash,
EventSignature: log.Topics[0],
LogIndex: uint64(log.Index),
Timestamp: header.Time,
},
}
contractEvent := processedContractEvents.AddLog(&logs[i], header.Time)
l2ContractEvents[i] = &database.L2ContractEvent{ContractEvent: *contractEvent}
}
/** Update Database **/
......@@ -153,15 +146,118 @@ func l2ProcessFn(processLog log.Logger, ethClient node.EthClient, l2Contracts L2
return err
}
numLogs := len(l2ContractEvents)
if numLogs > 0 {
processLog.Info("detected contract logs", "size", numLogs)
err = db.ContractEvents.StoreL2ContractEvents(l2ContractEvents)
if err != nil {
return err
}
// forward along contract events to the bridge processor
err = l2BridgeProcessContractEvents(processLog, db, ethClient, processedContractEvents)
if err != nil {
return err
}
}
// a-ok!
return nil
}
}
func l2BridgeProcessContractEvents(processLog log.Logger, db *database.DB, ethClient node.EthClient, events *ProcessedContractEvents) error {
rawEthClient := ethclient.NewClient(ethClient.RawRpcClient())
l2ToL1MessagePasserABI, err := bindings.L2ToL1MessagePasserMetaData.GetAbi()
if err != nil {
return err
}
messagePassedEventAbi := l2ToL1MessagePasserABI.Events["MessagePassed"]
// Process New Withdrawals
initiatedWithdrawalEvents, err := StandardBridgeInitiatedEvents(events)
if err != nil {
return err
}
withdrawals := make([]*database.Withdrawal, len(initiatedWithdrawalEvents))
for i, initiatedBridgeEvent := range initiatedWithdrawalEvents {
log := events.eventLog[initiatedBridgeEvent.RawEvent.GUID]
// extract the withdrawal hash from the MessagePassed event
var msgPassedData bindings.L2ToL1MessagePasserMessagePassed
msgPassedLog := events.eventLog[events.eventByLogIndex[ProcessedContractEventLogIndexKey{log.BlockHash, log.Index + 1}].GUID]
err := UnpackLog(&msgPassedData, msgPassedLog, messagePassedEventAbi.Name, l2ToL1MessagePasserABI)
if err != nil {
return err
}
withdrawals[i] = &database.Withdrawal{
GUID: uuid.New(),
InitiatedL2EventGUID: initiatedBridgeEvent.RawEvent.GUID,
SentMessageNonce: database.U256{Int: initiatedBridgeEvent.CrossDomainMessengerNonce},
WithdrawalHash: msgPassedData.WithdrawalHash,
TokenPair: database.TokenPair{L1TokenAddress: initiatedBridgeEvent.LocalToken, L2TokenAddress: initiatedBridgeEvent.RemoteToken},
Tx: database.Transaction{
FromAddress: initiatedBridgeEvent.From,
ToAddress: initiatedBridgeEvent.To,
Amount: database.U256{Int: initiatedBridgeEvent.Amount},
Data: initiatedBridgeEvent.ExtraData,
Timestamp: initiatedBridgeEvent.RawEvent.Timestamp,
},
}
}
if len(withdrawals) > 0 {
processLog.Info("detected L2StandardBridge withdrawals", "num", len(withdrawals))
err := db.Bridge.StoreWithdrawals(withdrawals)
if err != nil {
return err
}
}
// Finalize Deposits
finalizationBridgeEvents, err := StandardBridgeFinalizedEvents(rawEthClient, events)
if err != nil {
return err
}
for _, finalizedBridgeEvent := range finalizationBridgeEvents {
nonce := finalizedBridgeEvent.CrossDomainMessengerNonce
deposit, err := db.Bridge.DepositByMessageNonce(nonce)
if err != nil {
processLog.Error("error querying associated deposit messsage using nonce", "cross_domain_messenger_nonce", nonce)
return err
} else if deposit == nil {
latestNonce, err := db.Bridge.LatestDepositMessageNonce()
if err != nil {
return err
}
// Check if the L1Processor is behind or really has missed an event
if latestNonce == nil || nonce.Cmp(latestNonce) > 0 {
processLog.Warn("behind on indexed L1 deposits")
return errors.New("waiting for L1Processor to catch up")
} else {
processLog.Crit("missing indexed deposit for this finalization event")
return errors.New("missing deposit message")
}
}
err = db.Bridge.MarkFinalizedDepositEvent(deposit.GUID, finalizedBridgeEvent.RawEvent.GUID)
if err != nil {
processLog.Error("error finalizing deposit", "err", err)
return err
}
}
if len(finalizationBridgeEvents) > 0 {
processLog.Info("finalized L1StandardBridge deposits", "size", len(finalizationBridgeEvents))
}
// a-ok
return nil
}
package processor
import (
"context"
"math/big"
"github.com/ethereum-optimism/optimism/indexer/database"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
)
type OptimismPortalWithdrawalProvenEvent struct {
*bindings.OptimismPortalWithdrawalProven
RawEvent *database.ContractEvent
}
type OptimismPortalProvenWithdrawal struct {
OutputRoot [32]byte
Timestamp *big.Int
L2OutputIndex *big.Int
}
func OptimismPortalWithdrawalProvenEvents(events *ProcessedContractEvents) ([]OptimismPortalWithdrawalProvenEvent, error) {
optimismPortalAbi, err := bindings.OptimismPortalMetaData.GetAbi()
if err != nil {
return nil, err
}
processedWithdrawalProvenEvents := events.eventsBySignature[optimismPortalAbi.Events["WithdrawalProven"].ID]
provenEvents := make([]OptimismPortalWithdrawalProvenEvent, len(processedWithdrawalProvenEvents))
for i, provenEvent := range processedWithdrawalProvenEvents {
provenEvents[i] = OptimismPortalWithdrawalProvenEvent{nil, provenEvent}
}
return provenEvents, nil
}
func OptimismPortalQueryProvenWithdrawal(ethClient *ethclient.Client, portalAddress common.Address, withdrawalHash common.Hash) (OptimismPortalProvenWithdrawal, error) {
var provenWithdrawal OptimismPortalProvenWithdrawal
optimismPortalAbi, err := bindings.OptimismPortalMetaData.GetAbi()
if err != nil {
return provenWithdrawal, err
}
name := "provenWithdrawals"
txData, err := optimismPortalAbi.Pack(name, withdrawalHash)
if err != nil {
return provenWithdrawal, err
}
callMsg := ethereum.CallMsg{To: &portalAddress, Data: txData}
data, err := ethClient.CallContract(context.Background(), callMsg, nil)
if err != nil {
return provenWithdrawal, err
}
err = optimismPortalAbi.UnpackIntoInterface(&provenWithdrawal, name, data)
if err != nil {
return provenWithdrawal, err
}
return provenWithdrawal, nil
}
......@@ -55,8 +55,8 @@ func (p processor) Start() {
firstHeader := unprocessedHeaders[0]
lastHeader := unprocessedHeaders[len(unprocessedHeaders)-1]
batchLog := p.processLog.New("batch_start_block_number", firstHeader.Number, "batch_end_block_number", lastHeader.Number)
batchLog.Info("processing batch")
err := p.db.Transaction(func(db *database.DB) error {
batchLog.Info("processing batch")
return p.processFn(db, unprocessedHeaders)
})
......
package processor
import (
"bytes"
"context"
"errors"
"math/big"
"github.com/ethereum-optimism/optimism/indexer/database"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
)
var (
ethAddress = common.HexToAddress("0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000")
)
type StandardBridgeInitiatedEvent struct {
// We hardcode to ERC20 since ETH can be pseudo-represented as an ERC20 utilizing
// the hardcoded ETH address
*bindings.L1StandardBridgeERC20BridgeInitiated
CrossDomainMessengerNonce *big.Int
RawEvent *database.ContractEvent
}
type StandardBridgeFinalizedEvent struct {
// We hardcode to ERC20 since ETH can be pseudo-represented as an ERC20 utilizing
// the hardcoded ETH address
*bindings.L1StandardBridgeERC20BridgeFinalized
CrossDomainMessengerNonce *big.Int
RawEvent *database.ContractEvent
}
// StandardBridgeInitiatedEvents extracts all initiated bridge events from the contracts that follow the StandardBridge ABI. The
// correlated CrossDomainMessenger nonce is also parsed from the associated messenger events.
func StandardBridgeInitiatedEvents(events *ProcessedContractEvents) ([]StandardBridgeInitiatedEvent, error) {
ethBridgeInitiatedEvents, err := _standardBridgeInitiatedEvents[bindings.L1StandardBridgeETHBridgeInitiated](events)
if err != nil {
return nil, err
}
erc20BridgeInitiatedEvents, err := _standardBridgeInitiatedEvents[bindings.L1StandardBridgeERC20BridgeInitiated](events)
if err != nil {
return nil, err
}
return append(ethBridgeInitiatedEvents, erc20BridgeInitiatedEvents...), nil
}
// StandardBridgeFinalizedEvents extracts all finalization bridge events from the contracts that follow the StandardBridge ABI. The
// correlated CrossDomainMessenger nonce is also parsed by looking at the parameters of the corresponding relayMessage transaction data.
func StandardBridgeFinalizedEvents(rawEthClient *ethclient.Client, events *ProcessedContractEvents) ([]StandardBridgeFinalizedEvent, error) {
ethBridgeFinalizedEvents, err := _standardBridgeFinalizedEvents[bindings.L1StandardBridgeETHBridgeFinalized](rawEthClient, events)
if err != nil {
return nil, err
}
erc20BridgeFinalizedEvents, err := _standardBridgeFinalizedEvents[bindings.L1StandardBridgeERC20BridgeFinalized](rawEthClient, events)
if err != nil {
return nil, err
}
return append(ethBridgeFinalizedEvents, erc20BridgeFinalizedEvents...), nil
}
// parse out eth or erc20 bridge initiated events
func _standardBridgeInitiatedEvents[BridgeEvent bindings.L1StandardBridgeETHBridgeInitiated | bindings.L1StandardBridgeERC20BridgeInitiated](
events *ProcessedContractEvents,
) ([]StandardBridgeInitiatedEvent, error) {
l1StandardBridgeABI, err := bindings.L1StandardBridgeMetaData.GetAbi()
if err != nil {
return nil, err
}
l1CrossDomainMessengerABI, err := bindings.L1CrossDomainMessengerMetaData.GetAbi()
if err != nil {
return nil, err
}
sentMessageEventAbi := l1CrossDomainMessengerABI.Events["SentMessage"]
var tmp BridgeEvent
var eventName string
var finalizeMethodName string
switch any(tmp).(type) {
case bindings.L1StandardBridgeETHBridgeInitiated:
eventName = "ETHBridgeInitiated"
finalizeMethodName = "finalizeBridgeETH"
case bindings.L1StandardBridgeERC20BridgeInitiated:
eventName = "ERC20BridgeInitiated"
finalizeMethodName = "finalizeBridgeERC20"
default:
panic("should not be here")
}
processedInitiatedBridgeEvents := events.eventsBySignature[l1StandardBridgeABI.Events[eventName].ID]
initiatedBridgeEvents := make([]StandardBridgeInitiatedEvent, len(processedInitiatedBridgeEvents))
for i, bridgeInitiatedEvent := range processedInitiatedBridgeEvents {
log := events.eventLog[bridgeInitiatedEvent.GUID]
bridgeData := new(BridgeEvent)
err := UnpackLog(bridgeData, log, eventName, l1StandardBridgeABI)
if err != nil {
return nil, err
}
// Look for the sent message event to extract the associated messager nonce
// - L1: BridgeInitiated -> Portal#DepositTransaction -> SentMessage ...
// - L1: BridgeInitiated -> L2ToL1MessagePasser#MessagePassed -> SentMessage ...
var sentMsgData bindings.L1CrossDomainMessengerSentMessage
sentMsgLog := events.eventLog[events.eventByLogIndex[ProcessedContractEventLogIndexKey{log.BlockHash, log.Index + 2}].GUID]
err = UnpackLog(&sentMsgData, sentMsgLog, sentMessageEventAbi.Name, l1CrossDomainMessengerABI)
if err != nil {
return nil, err
}
var erc20BridgeData *bindings.L1StandardBridgeERC20BridgeInitiated
var expectedCrossDomainMessage []byte
switch any(bridgeData).(type) {
case *bindings.L1StandardBridgeETHBridgeInitiated:
ethBridgeData := any(bridgeData).(*bindings.L1StandardBridgeETHBridgeInitiated)
expectedCrossDomainMessage, err = l1StandardBridgeABI.Pack(finalizeMethodName, ethBridgeData.From, ethBridgeData.To, ethBridgeData.Amount, ethBridgeData.ExtraData)
if err != nil {
return nil, err
}
// represent eth bridge as an erc20
erc20BridgeData = &bindings.L1StandardBridgeERC20BridgeInitiated{
// Represent ETH using the hardcoded address
LocalToken: ethAddress, RemoteToken: ethAddress,
// Bridge data
From: ethBridgeData.From, To: ethBridgeData.To, Amount: ethBridgeData.Amount, ExtraData: ethBridgeData.ExtraData,
}
case *bindings.L1StandardBridgeERC20BridgeInitiated:
_temp := any(bridgeData).(bindings.L1StandardBridgeERC20BridgeInitiated)
erc20BridgeData = &_temp
expectedCrossDomainMessage, err = l1StandardBridgeABI.Pack(finalizeMethodName, erc20BridgeData.RemoteToken, erc20BridgeData.LocalToken, erc20BridgeData.From, erc20BridgeData.To, erc20BridgeData.Amount, erc20BridgeData.ExtraData)
if err != nil {
return nil, err
}
}
if !bytes.Equal(sentMsgData.Message, expectedCrossDomainMessage) {
return nil, errors.New("bridge cross domain message mismatch")
}
initiatedBridgeEvents[i] = StandardBridgeInitiatedEvent{erc20BridgeData, sentMsgData.MessageNonce, bridgeInitiatedEvent}
}
return initiatedBridgeEvents, nil
}
// parse out eth or erc20 bridge finalization events
func _standardBridgeFinalizedEvents[BridgeEvent bindings.L1StandardBridgeETHBridgeFinalized | bindings.L1StandardBridgeERC20BridgeFinalized](
rawEthClient *ethclient.Client,
events *ProcessedContractEvents,
) ([]StandardBridgeFinalizedEvent, error) {
l1StandardBridgeABI, err := bindings.L1StandardBridgeMetaData.GetAbi()
if err != nil {
return nil, err
}
l1CrossDomainMessengerABI, err := bindings.L1CrossDomainMessengerMetaData.GetAbi()
if err != nil {
return nil, err
}
relayedMessageEventAbi := l1CrossDomainMessengerABI.Events["RelayedMessage"]
relayMessageMethodAbi := l1CrossDomainMessengerABI.Methods["relayMessage"]
var bridgeData BridgeEvent
var eventName string
switch any(bridgeData).(type) {
case bindings.L1StandardBridgeETHBridgeFinalized:
eventName = "ETHBridgeFinalized"
case bindings.L1StandardBridgeERC20BridgeFinalized:
eventName = "ERC20BridgeFinalized"
default:
panic("should not be here")
}
processedFinalizedBridgeEvents := events.eventsBySignature[l1StandardBridgeABI.Events[eventName].ID]
finalizedBridgeEvents := make([]StandardBridgeFinalizedEvent, len(processedFinalizedBridgeEvents))
for i, bridgeFinalizedEvent := range processedFinalizedBridgeEvents {
log := events.eventLog[bridgeFinalizedEvent.GUID]
var bridgeData BridgeEvent
err := UnpackLog(&bridgeData, log, eventName, l1StandardBridgeABI)
if err != nil {
return nil, err
}
// Look for the RelayedMessage event that follows right after the BridgeFinalized Event
relayedMsgLog := events.eventLog[events.eventByLogIndex[ProcessedContractEventLogIndexKey{log.BlockHash, log.Index + 1}].GUID]
if relayedMsgLog.Topics[0] != relayedMessageEventAbi.ID {
return nil, errors.New("unexpected bridge event ordering")
}
// There's no way to extract the nonce on the relayed message event. we can extract
// the nonce by unpacking the transaction input for the `relayMessage` transaction
tx, isPending, err := rawEthClient.TransactionByHash(context.Background(), relayedMsgLog.TxHash)
if err != nil || isPending {
return nil, errors.New("unable to query relayMessage tx for bridge finalization event")
}
txData := tx.Data()
if !bytes.Equal(txData[:4], relayMessageMethodAbi.ID) {
return nil, errors.New("bridge finalization event does not match relayMessage tx invocation")
}
inputsMap := make(map[string]interface{})
err = relayMessageMethodAbi.Inputs.UnpackIntoMap(inputsMap, txData[4:])
if err != nil {
return nil, err
}
nonce, ok := inputsMap["_nonce"].(*big.Int)
if !ok {
return nil, errors.New("unable to extract `_nonce` parameter from relayMessage transaction")
}
var erc20BridgeData *bindings.L1StandardBridgeERC20BridgeFinalized
switch any(bridgeData).(type) {
case bindings.L1StandardBridgeETHBridgeInitiated:
ethBridgeData := any(bridgeData).(bindings.L1StandardBridgeETHBridgeFinalized)
erc20BridgeData = &bindings.L1StandardBridgeERC20BridgeFinalized{
// Represent ETH using the hardcoded address
LocalToken: ethAddress, RemoteToken: ethAddress,
// Bridge data
From: ethBridgeData.From, To: ethBridgeData.To, Amount: ethBridgeData.Amount, ExtraData: ethBridgeData.ExtraData,
}
case bindings.L1StandardBridgeERC20BridgeInitiated:
_temp := any(bridgeData).(bindings.L1StandardBridgeERC20BridgeFinalized)
erc20BridgeData = &_temp
}
finalizedBridgeEvents[i] = StandardBridgeFinalizedEvent{erc20BridgeData, nonce, bridgeFinalizedEvent}
}
return finalizedBridgeEvents, nil
}
......@@ -4,7 +4,12 @@
"defaultBase": "develop"
},
"implicitDependencies": {
"nx.json": "*"
"nx.json": "*",
"tsconfig.json": "*",
".foundryrc": "*",
"pnpm.lock.yaml": "*",
".npmrc": "*",
".nvmrc": "*"
},
"tasksRunnerOptions": {
"default": {
......
This diff is collapsed.
This diff is collapsed.
package op_challenger
import (
"fmt"
"time"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-challenger/config"
"github.com/ethereum-optimism/optimism/op-challenger/fault"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum-optimism/optimism/op-service/txmgr/metrics"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
)
// Main is the programmatic entry-point for running op-challenger
func Main(logger log.Logger, cfg *config.Config) error {
client, err := ethclient.Dial(cfg.L1EthRpc)
if err != nil {
return fmt.Errorf("failed to dial L1: %w", err)
}
txMgr, err := txmgr.NewSimpleTxManager("challenger", logger, &metrics.NoopTxMetrics{}, cfg.TxMgrConfig)
if err != nil {
return fmt.Errorf("failed to create the transaction manager: %w", err)
}
contract, err := bindings.NewFaultDisputeGameCaller(cfg.GameAddress, client)
if err != nil {
return fmt.Errorf("failed to bind the fault dispute game contract: %w", err)
}
loader := fault.NewLoader(logger, contract)
responder, err := fault.NewFaultResponder(logger, txMgr, cfg.GameAddress)
if err != nil {
return fmt.Errorf("failed to create the responder: %w", err)
}
gameDepth := 4
trace := fault.NewAlphabetProvider(cfg.AlphabetTrace, uint64(gameDepth))
agent := fault.NewAgent(loader, gameDepth, trace, responder, logger)
logger.Info("Fault game started")
return nil
for {
logger.Info("Performing action")
_ = agent.Act()
time.Sleep(300 * time.Millisecond)
}
}
#!/bin/bash
set -euo pipefail
DIR=$(cd $(dirname "${BASH_SOURCE[0]}") && pwd)
cd "$DIR"
DISPUTE_GAME_PROXY="0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e"
CHARLIE_ADDRESS="0xF45B7537828CB2fffBC69996B054c2Aaf36DC778"
CHARLIE_KEY="74feb147d72bfae943e6b4e483410933d9e447d5dc47d52432dcc2c1454dabb7"
MALLORY_ADDRESS="0x4641c704a6c743f73ee1f36C7568Fbf4b80681e4"
MALLORY_KEY="28d7045146193f5f4eeb151c4843544b1b0d30a7ac1680c845a416fac65a7715"
FAULT_GAME_ADDRESS="0x8daf17a20c9dba35f005b6324f493785d239719d"
./bin/op-challenger --l1-eth-rpc http://localhost:8545 --alphabet "abcdefgh" --game-address $FAULT_GAME_ADDRESS --private-key $CHARLIE_KEY --num-confirmations 1
package fault
import (
"math/big"
"testing"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/accounts/abi/bind/backends"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/params"
"github.com/stretchr/testify/require"
)
// setupFaultDisputeGame deploys the FaultDisputeGame contract to a simulated backend
func setupFaultDisputeGame() (common.Address, *bind.TransactOpts, *backends.SimulatedBackend, *bindings.FaultDisputeGame, error) {
privateKey, err := crypto.GenerateKey()
from := crypto.PubkeyToAddress(privateKey.PublicKey)
if err != nil {
return common.Address{}, nil, nil, nil, err
}
opts, err := bind.NewKeyedTransactorWithChainID(privateKey, big.NewInt(1337))
if err != nil {
return common.Address{}, nil, nil, nil, err
}
backend := backends.NewSimulatedBackend(core.GenesisAlloc{from: {Balance: big.NewInt(params.Ether)}}, 50_000_000)
_, _, contract, err := bindings.DeployFaultDisputeGame(
opts,
backend,
[32]byte{0x01},
big.NewInt(15),
common.Address{0xdd},
)
if err != nil {
return common.Address{}, nil, nil, nil, err
}
return from, opts, backend, contract, nil
}
// TestBuildFaultDefendData ensures that the manual ABI packing is the same as going through the bound contract.
func TestBuildFaultDefendData(t *testing.T) {
_, opts, _, contract, err := setupFaultDisputeGame()
require.NoError(t, err)
responder, _ := newTestFaultResponder(t, false)
data, err := responder.buildFaultDefendData(1, [32]byte{0x02, 0x03})
require.NoError(t, err)
opts.GasLimit = 100_000
tx, err := contract.Defend(opts, big.NewInt(1), [32]byte{0x02, 0x03})
require.NoError(t, err)
require.Equal(t, data, tx.Data())
}
// TestBuildFaultAttackData ensures that the manual ABI packing is the same as going through the bound contract.
func TestBuildFaultAttackData(t *testing.T) {
_, opts, _, contract, err := setupFaultDisputeGame()
require.NoError(t, err)
responder, _ := newTestFaultResponder(t, false)
data, err := responder.buildFaultAttackData(1, [32]byte{0x02, 0x03})
require.NoError(t, err)
opts.GasLimit = 100_000
tx, err := contract.Attack(opts, big.NewInt(1), [32]byte{0x02, 0x03})
require.NoError(t, err)
require.Equal(t, data, tx.Data())
}
// TestBuildFaultStepData ensures that the manual ABI packing is the same as going through the bound contract.
func TestBuildFaultStepData(t *testing.T) {
_, opts, _, contract, err := setupFaultDisputeGame()
require.NoError(t, err)
responder, _ := newTestFaultResponder(t, false)
data, err := responder.buildStepTxData(StepCallData{
ClaimIndex: 2,
IsAttack: false,
StateData: []byte{0x01},
Proof: []byte{0x02},
})
require.NoError(t, err)
opts.GasLimit = 100_000
tx, err := contract.Step(opts, big.NewInt(2), false, []byte{0x01}, []byte{0x02})
require.NoError(t, err)
require.Equal(t, data, tx.Data())
}
......@@ -2,70 +2,117 @@ package fault
import (
"context"
"sync"
"errors"
"fmt"
"github.com/ethereum/go-ethereum/log"
)
type Agent struct {
mu sync.Mutex
game Game
solver *Solver
trace TraceProvider
loader Loader
responder Responder
maxDepth int
log log.Logger
}
func NewAgent(game Game, maxDepth int, trace TraceProvider, responder Responder, log log.Logger) Agent {
func NewAgent(loader Loader, maxDepth int, trace TraceProvider, responder Responder, log log.Logger) Agent {
return Agent{
game: game,
solver: NewSolver(maxDepth, trace),
trace: trace,
loader: loader,
responder: responder,
maxDepth: maxDepth,
log: log,
}
}
// AddClaim stores a claim in the local state.
// This function shares a lock with PerformActions.
func (a *Agent) AddClaim(claim Claim) error {
a.mu.Lock()
defer a.mu.Unlock()
return a.game.Put(claim)
// Act iterates the game & performs all of the next actions.
func (a *Agent) Act() error {
game, err := a.newGameFromContracts(context.Background())
if err != nil {
a.log.Error("Failed to create new game", "err", err)
return err
}
// Create counter claims
for _, claim := range game.Claims() {
if err := a.move(claim, game); err != nil {
log.Error("Failed to move", "err", err)
}
}
// Step on all leaf claims
for _, claim := range game.Claims() {
if err := a.step(claim, game); err != nil {
log.Error("Failed to step", "err", err)
}
}
return nil
}
// PerformActions iterates the game & performs all of the next actions.
// Note: PerformActions & AddClaim share a lock so the responder cannot
// call AddClaim on the same thread.
func (a *Agent) PerformActions() {
a.mu.Lock()
defer a.mu.Unlock()
for _, claim := range a.game.Claims() {
_ = a.move(claim)
// newGameFromContracts initializes a new game state from the state in the contract
func (a *Agent) newGameFromContracts(ctx context.Context) (Game, error) {
claims, err := a.loader.FetchClaims(ctx)
if err != nil {
return nil, fmt.Errorf("failed to fetch claims: %w", err)
}
if len(claims) == 0 {
return nil, errors.New("no claims")
}
game := NewGameState(claims[0], uint64(a.maxDepth))
if err := game.PutAll(claims[1:]); err != nil {
return nil, fmt.Errorf("failed to load claims into the local state: %w", err)
}
return game, nil
}
// move determines & executes the next move given a claim pair
func (a *Agent) move(claim Claim) error {
// move determines & executes the next move given a claim
func (a *Agent) move(claim Claim, game Game) error {
nextMove, err := a.solver.NextMove(claim)
if err != nil {
a.log.Warn("Failed to execute the next move", "err", err)
return err
}
if nextMove == nil {
a.log.Info("No next move")
a.log.Debug("No next move")
return nil
}
move := *nextMove
log := a.log.New("is_defend", move.DefendsParent(), "depth", move.Depth(), "index_at_depth", move.IndexAtDepth(), "value", move.Value,
"letter", string(move.Value[31:]), "trace_index", move.Value[30],
"parent_letter", string(claim.Value[31:]), "parent_trace_index", claim.Value[30])
if a.game.IsDuplicate(move) {
log := a.log.New("is_defend", move.DefendsParent(), "depth", move.Depth(), "index_at_depth", move.IndexAtDepth(),
"value", move.Value, "trace_index", move.TraceIndex(a.maxDepth),
"parent_value", claim.Value, "parent_trace_index", claim.TraceIndex(a.maxDepth))
if game.IsDuplicate(move) {
log.Debug("Duplicate move")
return nil
}
log.Info("Performing move")
return a.responder.Respond(context.TODO(), move)
}
// step determines & executes the next step against a leaf claim through the responder
func (a *Agent) step(claim Claim, game Game) error {
if claim.Depth() != a.maxDepth {
return nil
}
a.log.Info("Attempting step", "claim_depth", claim.Depth(), "maxDepth", a.maxDepth)
step, err := a.solver.AttemptStep(claim)
if err != nil {
a.log.Warn("Failed to get a step", "err", err)
return err
}
stateData, err := a.trace.GetPreimage(step.PreStateTraceIndex)
if err != nil {
a.log.Warn("Failed to get a state data", "err", err)
return err
}
a.log.Info("Performing step",
"depth", step.LeafClaim.Depth(), "index_at_depth", step.LeafClaim.IndexAtDepth(), "value", step.LeafClaim.Value,
"is_attack", step.IsAttack, "prestate_trace_index", step.PreStateTraceIndex)
callData := StepCallData{
ClaimIndex: uint64(step.LeafClaim.ContractIndex),
IsAttack: step.IsAttack,
StateData: stateData,
}
return a.responder.Step(context.TODO(), callData)
}
......@@ -5,6 +5,7 @@ import (
"strings"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
)
// AlphabetProvider is a [TraceProvider] that provides claims for specific
......@@ -32,7 +33,7 @@ func (ap *AlphabetProvider) GetPreimage(i uint64) ([]byte, error) {
if i >= uint64(len(ap.state)) {
return ap.GetPreimage(uint64(len(ap.state)) - 1)
}
return buildAlphabetClaimBytes(i, ap.state[i]), nil
return BuildAlphabetPreimage(i, ap.state[i]), nil
}
// Get returns the claim value at the given index in the trace.
......@@ -41,17 +42,25 @@ func (ap *AlphabetProvider) Get(i uint64) (common.Hash, error) {
if err != nil {
return common.Hash{}, err
}
return common.BytesToHash(claimBytes), nil
return crypto.Keccak256Hash(claimBytes), nil
}
// buildAlphabetClaimBytes constructs the claim bytes for the index and state item.
func buildAlphabetClaimBytes(i uint64, letter string) []byte {
return append(IndexToBytes(i), []byte(letter)...)
// BuildAlphabetPreimage constructs the claim bytes for the index and state item.
func BuildAlphabetPreimage(i uint64, letter string) []byte {
return append(IndexToBytes(i), LetterToBytes(letter)...)
}
// IndexToBytes converts an index to a byte slice big endian
func IndexToBytes(i uint64) []byte {
big := new(big.Int)
big.SetUint64(i)
return big.Bytes()
out := make([]byte, 32)
return big.FillBytes(out)
}
// LetterToBytes converts a letter to a 32 byte array
func LetterToBytes(letter string) []byte {
out := make([]byte, 32)
out[31] = byte(letter[0])
return out
}
......@@ -20,15 +20,15 @@ func TestAlphabetProvider_Get_ClaimsByTraceIndex(t *testing.T) {
}{
{
7,
common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000768"),
alphabetClaim(7, "h"),
},
{
3,
common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000364"),
alphabetClaim(3, "d"),
},
{
5,
common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000566"),
alphabetClaim(5, "f"),
},
}
......@@ -54,7 +54,7 @@ func FuzzIndexToBytes(f *testing.F) {
// returns the correct pre-image for a index.
func TestGetPreimage_Succeeds(t *testing.T) {
ap := NewAlphabetProvider("abc", 2)
expected := append(IndexToBytes(uint64(0)), []byte("a")...)
expected := BuildAlphabetPreimage(0, "a'")
retrieved, err := ap.GetPreimage(uint64(0))
require.NoError(t, err)
require.Equal(t, expected, retrieved)
......@@ -73,8 +73,7 @@ func TestGet_Succeeds(t *testing.T) {
ap := NewAlphabetProvider("abc", 2)
claim, err := ap.Get(0)
require.NoError(t, err)
concatenated := append(IndexToBytes(0), []byte("a")...)
expected := common.BytesToHash(concatenated)
expected := alphabetClaim(0, "a")
require.Equal(t, expected, claim)
}
......@@ -92,7 +91,6 @@ func TestGet_Extends(t *testing.T) {
ap := NewAlphabetProvider("abc", 2)
claim, err := ap.Get(3)
require.NoError(t, err)
concatenated := append(IndexToBytes(2), []byte("c")...)
expected := common.BytesToHash(concatenated)
expected := alphabetClaim(2, "c")
require.Equal(t, expected, claim)
}
......@@ -36,9 +36,8 @@ type Game interface {
}
type extendedClaim struct {
self Claim
contractIndex int
children []ClaimData
self Claim
children []ClaimData
}
// gameState is a struct that represents the state of a dispute game.
......@@ -54,9 +53,8 @@ type gameState struct {
func NewGameState(root Claim, depth uint64) *gameState {
claims := make(map[ClaimData]*extendedClaim)
claims[root.ClaimData] = &extendedClaim{
self: root,
contractIndex: 0,
children: make([]ClaimData, 0),
self: root,
children: make([]ClaimData, 0),
}
return &gameState{
root: root.ClaimData,
......@@ -87,9 +85,8 @@ func (g *gameState) Put(claim Claim) error {
parent.children = append(parent.children, claim.ClaimData)
}
g.claims[claim.ClaimData] = &extendedClaim{
self: claim,
contractIndex: claim.ContractIndex,
children: make([]ClaimData, 0),
self: claim,
children: make([]ClaimData, 0),
}
return nil
}
......
......@@ -28,18 +28,14 @@ type Loader interface {
// loader pulls in fault dispute game claim data periodically and over subscriptions.
type loader struct {
log log.Logger
state Game
log log.Logger
claimFetcher ClaimFetcher
}
// NewLoader creates a new [loader].
func NewLoader(log log.Logger, state Game, claimFetcher ClaimFetcher) *loader {
func NewLoader(log log.Logger, claimFetcher ClaimFetcher) *loader {
return &loader{
log: log,
state: state,
claimFetcher: claimFetcher,
}
}
......@@ -60,6 +56,8 @@ func (l *loader) fetchClaim(ctx context.Context, arrIndex uint64) (Claim, error)
Value: fetchedClaim.Claim,
Position: NewPositionFromGIndex(fetchedClaim.Position.Uint64()),
},
ContractIndex: int(arrIndex),
ParentContractIndex: int(fetchedClaim.ParentIndex),
}
if !claim.IsRootPosition() {
......
......@@ -15,46 +15,8 @@ import (
var (
mockClaimDataError = fmt.Errorf("claim data errored")
mockClaimLenError = fmt.Errorf("claim len errored")
mockPutError = fmt.Errorf("put errored")
)
type mockGameState struct {
putCalled int
putErrors bool
}
func (m *mockGameState) Put(claim Claim) error {
m.putCalled++
if m.putErrors {
return mockPutError
}
return nil
}
func (m *mockGameState) PutAll(claims []Claim) error {
m.putCalled += len(claims)
if m.putErrors {
return mockPutError
}
return nil
}
func (m *mockGameState) Claims() []Claim {
return []Claim{}
}
func (m *mockGameState) IsDuplicate(claim Claim) bool {
return false
}
func (m *mockGameState) PreStateClaim(claim Claim) (Claim, error) {
panic("unimplemented")
}
func (m *mockGameState) PostStateClaim(claim Claim) (Claim, error) {
panic("unimplemented")
}
type mockClaimFetcher struct {
claimDataError bool
claimLenError bool
......@@ -126,7 +88,7 @@ func TestLoader_FetchClaims_Succeeds(t *testing.T) {
log := testlog.Logger(t, log.LvlError)
mockClaimFetcher := newMockClaimFetcher()
expectedClaims := mockClaimFetcher.returnClaims
loader := NewLoader(log, &mockGameState{}, mockClaimFetcher)
loader := NewLoader(log, mockClaimFetcher)
claims, err := loader.FetchClaims(context.Background())
require.NoError(t, err)
require.ElementsMatch(t, []Claim{
......@@ -139,6 +101,7 @@ func TestLoader_FetchClaims_Succeeds(t *testing.T) {
Value: expectedClaims[0].Claim,
Position: NewPositionFromGIndex(expectedClaims[0].Position.Uint64()),
},
ContractIndex: 0,
},
{
ClaimData: ClaimData{
......@@ -149,6 +112,7 @@ func TestLoader_FetchClaims_Succeeds(t *testing.T) {
Value: expectedClaims[0].Claim,
Position: NewPositionFromGIndex(expectedClaims[1].Position.Uint64()),
},
ContractIndex: 1,
},
{
ClaimData: ClaimData{
......@@ -159,6 +123,7 @@ func TestLoader_FetchClaims_Succeeds(t *testing.T) {
Value: expectedClaims[0].Claim,
Position: NewPositionFromGIndex(expectedClaims[2].Position.Uint64()),
},
ContractIndex: 2,
},
}, claims)
}
......@@ -169,7 +134,7 @@ func TestLoader_FetchClaims_ClaimDataErrors(t *testing.T) {
log := testlog.Logger(t, log.LvlError)
mockClaimFetcher := newMockClaimFetcher()
mockClaimFetcher.claimDataError = true
loader := NewLoader(log, &mockGameState{}, mockClaimFetcher)
loader := NewLoader(log, mockClaimFetcher)
claims, err := loader.FetchClaims(context.Background())
require.ErrorIs(t, err, mockClaimDataError)
require.Empty(t, claims)
......@@ -181,7 +146,7 @@ func TestLoader_FetchClaims_ClaimLenErrors(t *testing.T) {
log := testlog.Logger(t, log.LvlError)
mockClaimFetcher := newMockClaimFetcher()
mockClaimFetcher.claimLenError = true
loader := NewLoader(log, &mockGameState{}, mockClaimFetcher)
loader := NewLoader(log, mockClaimFetcher)
claims, err := loader.FetchClaims(context.Background())
require.ErrorIs(t, err, mockClaimLenError)
require.Empty(t, claims)
......
......@@ -2,74 +2,74 @@ package fault
import (
"context"
"os"
"time"
"github.com/ethereum/go-ethereum/log"
)
type Orchestrator struct {
agents []Agent
outputChs []chan Claim
responses chan Claim
agents []Agent
claims []Claim
steps []StepCallData
// tracking when to exit
claimLen, stepLen, step int
}
func NewOrchestrator(maxDepth uint64, traces []TraceProvider, names []string, root Claim) Orchestrator {
o := Orchestrator{
responses: make(chan Claim, 100),
outputChs: make([]chan Claim, len(traces)),
agents: make([]Agent, len(traces)),
agents: make([]Agent, len(traces)),
claims: []Claim{root},
steps: make([]StepCallData, 0),
}
log.Info("Starting game", "root_letter", string(root.Value[31:]))
for i, trace := range traces {
game := NewGameState(root, maxDepth)
o.agents[i] = NewAgent(game, int(maxDepth), trace, &o, log.New("role", names[i]))
o.outputChs[i] = make(chan Claim)
o.agents[i] = NewAgent(&o, int(maxDepth), trace, &o, log.New("role", names[i]))
}
return o
}
func (o *Orchestrator) Respond(_ context.Context, response Claim) error {
o.responses <- response
response.ContractIndex = len(o.claims)
o.claims = append(o.claims, response)
return nil
}
func (o *Orchestrator) Step(ctx context.Context, stepData StepCallData) error {
func (o *Orchestrator) Step(_ context.Context, stepData StepCallData) error {
log.Info("Step recorded", "step", stepData)
o.steps = append(o.steps, stepData)
return nil
}
func (o *Orchestrator) Start() {
for i := 0; i < len(o.agents); i++ {
go runAgent(&o.agents[i], o.outputChs[i])
}
o.responderThread()
func (o *Orchestrator) FetchClaims(ctx context.Context) ([]Claim, error) {
c := make([]Claim, len(o.claims))
copy(c, o.claims)
return c, nil
}
func runAgent(agent *Agent, claimCh <-chan Claim) {
func (o *Orchestrator) Start() {
for {
agent.PerformActions()
// Note: Should drain the channel here
claim := <-claimCh
_ = agent.AddClaim(claim)
for _, a := range o.agents {
_ = a.Act()
}
if o.shouldExit() {
log.Info("exiting")
return
}
}
}
func (o *Orchestrator) responderThread() {
timer := time.NewTimer(200 * time.Millisecond)
defer timer.Stop()
for {
select {
case resp := <-o.responses:
timer.Reset(200 * time.Millisecond)
for _, ch := range o.outputChs {
// Copy it. Should be immutable, but be sure.
resp := resp
ch <- resp
}
case <-timer.C:
os.Exit(0)
}
func (o *Orchestrator) shouldExit() bool {
cl := o.claimLen
sl := o.stepLen
o.claimLen = len(o.claims)
o.stepLen = len(o.steps)
noProgress := o.claimLen == cl && o.stepLen == sl
if noProgress {
o.step = o.step + 1
} else {
o.step = 0
}
return noProgress && o.step == 1
}
......@@ -104,7 +104,6 @@ func (r *faultResponder) sendTxAndWait(ctx context.Context, txData []byte) error
func (r *faultResponder) buildStepTxData(stepData StepCallData) ([]byte, error) {
return r.fdgAbi.Pack(
"step",
big.NewInt(int64(stepData.StateIndex)),
big.NewInt(int64(stepData.ClaimIndex)),
stepData.IsAttack,
stepData.StateData,
......
......@@ -9,7 +9,6 @@ import (
// Solver uses a [TraceProvider] to determine the moves to make in a dispute game.
type Solver struct {
TraceProvider
gameDepth int
}
......@@ -31,14 +30,14 @@ func (s *Solver) NextMove(claim Claim) (*Claim, error) {
}
type StepData struct {
LeafClaim Claim
StateClaim Claim
IsAttack bool
LeafClaim Claim
IsAttack bool
PreStateTraceIndex uint64
}
// AttemptStep determines what step should occur for a given leaf claim.
// An error will be returned if the claim is not at the max depth.
func (s *Solver) AttemptStep(claim Claim, state Game) (StepData, error) {
func (s *Solver) AttemptStep(claim Claim) (StepData, error) {
if claim.Depth() != s.gameDepth {
return StepData{}, errors.New("cannot step on non-leaf claims")
}
......@@ -46,20 +45,15 @@ func (s *Solver) AttemptStep(claim Claim, state Game) (StepData, error) {
if err != nil {
return StepData{}, err
}
var selectorFn func(Claim) (Claim, error)
if claimCorrect {
selectorFn = state.PostStateClaim
} else {
selectorFn = state.PreStateClaim
}
stateClaim, err := selectorFn(claim)
if err != nil {
return StepData{}, err
index := claim.TraceIndex(s.gameDepth)
// TODO(CLI-4198): Handle case where we dispute trace index 0
if !claimCorrect {
index -= 1
}
return StepData{
LeafClaim: claim,
StateClaim: stateClaim,
IsAttack: claimCorrect,
LeafClaim: claim,
IsAttack: !claimCorrect,
PreStateTraceIndex: index,
}, nil
}
......@@ -117,8 +111,9 @@ func (s *Solver) attack(claim Claim) (*Claim, error) {
return nil, err
}
return &Claim{
ClaimData: ClaimData{Value: value, Position: position},
Parent: claim.ClaimData,
ClaimData: ClaimData{Value: value, Position: position},
Parent: claim.ClaimData,
ParentContractIndex: claim.ContractIndex,
}, nil
}
......@@ -130,8 +125,9 @@ func (s *Solver) defend(claim Claim) (*Claim, error) {
return nil, err
}
return &Claim{
ClaimData: ClaimData{Value: value, Position: position},
Parent: claim.ClaimData,
ClaimData: ClaimData{Value: value, Position: position},
Parent: claim.ClaimData,
ParentContractIndex: claim.ContractIndex,
}, nil
}
......
......@@ -4,9 +4,14 @@ import (
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/stretchr/testify/require"
)
func alphabetClaim(index uint64, letter string) common.Hash {
return crypto.Keccak256Hash(BuildAlphabetPreimage(index, letter))
}
// TestSolver_NextMove_Opponent tests the [Solver] NextMove function
// with an [fault.AlphabetProvider] as the [TraceProvider].
func TestSolver_NextMove_Opponent(t *testing.T) {
......@@ -18,55 +23,51 @@ func TestSolver_NextMove_Opponent(t *testing.T) {
// The following claims are created using the state: "abcdexyz".
// The responses are the responses we expect from the solver.
indices := []struct {
traceIndex int
claim Claim
response ClaimData
claim Claim
response ClaimData
}{
{
7,
Claim{
ClaimData: ClaimData{
Value: common.HexToHash("0x000000000000000000000000000000000000000000000000000000000000077a"),
Value: alphabetClaim(7, "z"),
Position: NewPosition(0, 0),
},
// Root claim has no parent
},
ClaimData{
Value: common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000364"),
Value: alphabetClaim(3, "d"),
Position: NewPosition(1, 0),
},
},
{
3,
Claim{
ClaimData: ClaimData{
Value: common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000364"),
Value: alphabetClaim(3, "d"),
Position: NewPosition(1, 0),
},
Parent: ClaimData{
Value: common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000768"),
Value: alphabetClaim(7, "h"),
Position: NewPosition(0, 0),
},
},
ClaimData{
Value: common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000566"),
Value: alphabetClaim(5, "f"),
Position: NewPosition(2, 2),
},
},
{
5,
Claim{
ClaimData: ClaimData{
Value: common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000578"),
Value: alphabetClaim(5, "x"),
Position: NewPosition(2, 2),
},
Parent: ClaimData{
Value: common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000768"),
Value: alphabetClaim(7, "h"),
Position: NewPosition(1, 1),
},
},
ClaimData{
Value: common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000465"),
Value: alphabetClaim(4, "e"),
Position: NewPosition(3, 4),
},
},
......@@ -89,12 +90,11 @@ func TestAttemptStep(t *testing.T) {
require.NoError(t, g.Put(middle))
require.NoError(t, g.Put(bottom))
step, err := solver.AttemptStep(bottom, g)
step, err := solver.AttemptStep(bottom)
require.NoError(t, err)
require.Equal(t, bottom, step.LeafClaim)
require.Equal(t, middle, step.StateClaim)
require.True(t, step.IsAttack)
_, err = solver.AttemptStep(middle, g)
_, err = solver.AttemptStep(middle)
require.Error(t, err)
}
......@@ -14,7 +14,6 @@ var (
// StepCallData encapsulates the data needed to perform a step.
type StepCallData struct {
StateIndex uint64
ClaimIndex uint64
IsAttack bool
StateData []byte
......
#!/bin/bash
set -euo pipefail
DIR=$(cd $(dirname "${BASH_SOURCE[0]}") && pwd)
cd "$DIR"
# build the challenger to keep it up to date
make
cd ..
make devnet-clean
make devnet-up-deploy
DEVNET_SPONSOR="ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
DISPUTE_GAME_PROXY="0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e"
CHARLIE_ADDRESS="0xF45B7537828CB2fffBC69996B054c2Aaf36DC778"
CHARLIE_KEY="74feb147d72bfae943e6b4e483410933d9e447d5dc47d52432dcc2c1454dabb7"
MALLORY_ADDRESS="0x4641c704a6c743f73ee1f36C7568Fbf4b80681e4"
MALLORY_KEY="28d7045146193f5f4eeb151c4843544b1b0d30a7ac1680c845a416fac65a7715"
echo "----------------------------------------------------------------"
echo " - Fetching balance of the sponsor"
echo " - Balance: $(cast balance 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266)"
echo "----------------------------------------------------------------"
echo "Funding Charlie"
cast send $CHARLIE_ADDRESS --value 5ether --private-key $DEVNET_SPONSOR
echo "Funding Mallory"
cast send $MALLORY_ADDRESS --value 5ether --private-key $DEVNET_SPONSOR
# Fault game type = 0
GAME_TYPE=0
# Root claim commits to the entire trace.
# Alphabet game claim construction: keccak256(abi.encode(trace_index, trace[trace_index]))
ROOT_CLAIM=$(cast keccak $(cast abi-encode "f(uint256,uint256)" 15 122))
# Extra data is a dynamic `bytes` type that contains the L2 Block Number of the output proposal that the root claim disagrees with
# Doesn't matter right now since we're not deleting outputs, so just set it to 1
EXTRA_DATA=$(cast to-bytes32 1)
echo "Initializing the game"
cast call --private-key $MALLORY_KEY $DISPUTE_GAME_PROXY "create(uint8,bytes32,bytes)" $GAME_TYPE $ROOT_CLAIM $EXTRA_DATA
cast send --private-key $MALLORY_KEY $DISPUTE_GAME_PROXY "create(uint8,bytes32,bytes)" $GAME_TYPE $ROOT_CLAIM $EXTRA_DATA
#!/bin/bash
set -euo pipefail
DIR=$(cd $(dirname "${BASH_SOURCE[0]}") && pwd)
cd "$DIR"
DISPUTE_GAME_PROXY="0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e"
CHARLIE_ADDRESS="0xF45B7537828CB2fffBC69996B054c2Aaf36DC778"
CHARLIE_KEY="74feb147d72bfae943e6b4e483410933d9e447d5dc47d52432dcc2c1454dabb7"
MALLORY_ADDRESS="0x4641c704a6c743f73ee1f36C7568Fbf4b80681e4"
MALLORY_KEY="28d7045146193f5f4eeb151c4843544b1b0d30a7ac1680c845a416fac65a7715"
FAULT_GAME_ADDRESS="0x8daf17a20c9dba35f005b6324f493785d239719d"
./bin/op-challenger --l1-eth-rpc http://localhost:8545 --alphabet "abcdexyz" --game-address $FAULT_GAME_ADDRESS --private-key $MALLORY_KEY --num-confirmations 1
#!/bin/bash
set -euo pipefail
DISPUTE_GAME_PROXY="0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e"
FAULT_GAME_ADDRESS="0x8daf17a20c9dba35f005b6324f493785d239719d"
dir=$(cd $(dirname "${BASH_SOURCE[0]}") && pwd)
cd "$dir"
cd ../packages/contracts-bedrock
forge script scripts/FaultDisputeGameViz.s.sol --sig "remote(address)" $FAULT_GAME_ADDRESS --fork-url http://localhost:8545
mv dispute_game.svg "$dir"
......@@ -38,11 +38,11 @@ func LightApplicationScoreParams(cfg *rollup.Config) ApplicationScoreParams {
ValidResponseWeight: 0.5,
ValidResponseDecay: ScoreDecay(tenEpochs, slot),
// Takes 20 error responses to reach the default ban threshold of -100
// But at most we track 10. These errors include not supporting p2p sync
// so we don't (yet) want to ban a peer based on this measure alone.
ErrorResponseCap: 10,
ErrorResponseWeight: -5,
// Takes 10 error responses to reach the default gossip threshold of -10
// But at most we track 9. These errors include not supporting p2p sync
// so we don't (yet) want to ignore gossip from a peer based on this measure alone.
ErrorResponseCap: 9,
ErrorResponseWeight: -1,
ErrorResponseDecay: ScoreDecay(tenEpochs, slot),
// Takes 5 rejected payloads to reach the default ban threshold of -100
......
......@@ -15,7 +15,7 @@ type Metrics interface {
RecordL1Ref(name string, ref eth.L1BlockRef)
RecordL2Ref(name string, ref eth.L2BlockRef)
RecordUnsafePayloadsBuffer(length uint64, memSize uint64, next eth.BlockID)
RecordChannelInputBytes(inputCompresedBytes int)
RecordChannelInputBytes(inputCompressedBytes int)
}
type L1Fetcher interface {
......
......@@ -21,7 +21,7 @@ type Metrics interface {
RecordL1Ref(name string, ref eth.L1BlockRef)
RecordL2Ref(name string, ref eth.L2BlockRef)
RecordChannelInputBytes(inputCompresedBytes int)
RecordChannelInputBytes(inputCompressedBytes int)
RecordUnsafePayloadsBuffer(length uint64, memSize uint64, next eth.BlockID)
......
......@@ -11,7 +11,7 @@ type TestDerivationMetrics struct {
FnRecordL1Ref func(name string, ref eth.L1BlockRef)
FnRecordL2Ref func(name string, ref eth.L2BlockRef)
FnRecordUnsafePayloads func(length uint64, memSize uint64, next eth.BlockID)
FnRecordChannelInputBytes func(inputCompresedBytes int)
FnRecordChannelInputBytes func(inputCompressedBytes int)
}
func (t *TestDerivationMetrics) RecordL1ReorgDepth(d uint64) {
......@@ -38,9 +38,9 @@ func (t *TestDerivationMetrics) RecordUnsafePayloadsBuffer(length uint64, memSiz
}
}
func (t *TestDerivationMetrics) RecordChannelInputBytes(inputCompresedBytes int) {
func (t *TestDerivationMetrics) RecordChannelInputBytes(inputCompressedBytes int) {
if t.FnRecordChannelInputBytes != nil {
t.FnRecordChannelInputBytes(inputCompresedBytes)
t.FnRecordChannelInputBytes(inputCompressedBytes)
}
}
......
......@@ -2,7 +2,7 @@ FROM ethereum/client-go:v1.11.2
RUN apk add --no-cache jq
COPY entrypoint.sh /entrypoint.sh
COPY entrypoint-l1.sh /entrypoint.sh
VOLUME ["/db"]
......
......@@ -2,7 +2,7 @@ FROM us-docker.pkg.dev/oplabs-tools-artifacts/images/op-geth:optimism
RUN apk add --no-cache jq
COPY entrypoint.sh /entrypoint.sh
COPY entrypoint-l2.sh /entrypoint.sh
VOLUME ["/db"]
......
#!/bin/sh
set -exu
VERBOSITY=${GETH_VERBOSITY:-3}
GETH_DATA_DIR=/db
GETH_CHAINDATA_DIR="$GETH_DATA_DIR/geth/chaindata"
GENESIS_FILE_PATH="${GENESIS_FILE_PATH:-/genesis.json}"
CHAIN_ID=$(cat "$GENESIS_FILE_PATH" | jq -r .config.chainId)
RPC_PORT="${RPC_PORT:-8545}"
WS_PORT="${WS_PORT:-8546}"
if [ ! -d "$GETH_CHAINDATA_DIR" ]; then
echo "$GETH_CHAINDATA_DIR missing, running init"
echo "Initializing genesis."
geth --verbosity="$VERBOSITY" init \
--datadir="$GETH_DATA_DIR" \
"$GENESIS_FILE_PATH"
else
echo "$GETH_CHAINDATA_DIR exists."
fi
# Warning: Archive mode is required, otherwise old trie nodes will be
# pruned within minutes of starting the devnet.
exec geth \
--datadir="$GETH_DATA_DIR" \
--verbosity="$VERBOSITY" \
--http \
--http.corsdomain="*" \
--http.vhosts="*" \
--http.addr=0.0.0.0 \
--http.port="$RPC_PORT" \
--http.api=web3,debug,eth,txpool,net,engine \
--ws \
--ws.addr=0.0.0.0 \
--ws.port="$WS_PORT" \
--ws.origins="*" \
--ws.api=debug,eth,txpool,net,engine \
--syncmode=full \
--nodiscover \
--maxpeers=0 \
--networkid=$CHAIN_ID \
--authrpc.addr="0.0.0.0" \
--authrpc.port="8551" \
--authrpc.vhosts="*" \
--authrpc.jwtsecret=/config/jwt-secret.txt \
--gcmode=archive \
--metrics \
--metrics.addr=0.0.0.0 \
--metrics.port=6060 \
"$@"
......@@ -3,6 +3,7 @@ import os
import re
import subprocess
import sys
import json
from github import Github
......@@ -13,9 +14,11 @@ REBUILD_ALL_PATTERNS = [
r'ops/check-changed/.*',
r'^go\.mod',
r'^go\.sum',
r'^pnpm-lock\.yaml',
r'ops/check-changed/.*'
]
with open("../../nx.json") as file:
nx_json_data = json.load(file)
REBUILD_ALL_PATTERNS += nx_json_data["implicitDependencies"].keys()
WHITELISTED_BRANCHES = {
'master',
......
......@@ -6,10 +6,10 @@ WORKDIR /opt
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && \
apt-get install -y curl build-essential git && \
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > rustup.sh && \
chmod +x ./rustup.sh && \
./rustup.sh -y
apt-get install -y curl build-essential git && \
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > rustup.sh && \
chmod +x ./rustup.sh && \
./rustup.sh -y
COPY ./.foundryrc ./.foundryrc
......@@ -21,10 +21,10 @@ RUN git clone https://github.com/foundry-rs/foundry.git ./foundry \
WORKDIR /opt/foundry
RUN source $HOME/.profile && \
cargo build --release && \
strip /opt/foundry/target/release/forge && \
strip /opt/foundry/target/release/cast && \
strip /opt/foundry/target/release/anvil
cargo build --release && \
strip /opt/foundry/target/release/forge && \
strip /opt/foundry/target/release/cast && \
strip /opt/foundry/target/release/anvil
FROM ethereum/client-go:alltools-v1.10.25 as geth
......@@ -105,3 +105,8 @@ RUN echo "downloading mockery tool" && \
cp mockery-tmp-dir/mockery /usr/local/bin/mockery && \
chmod +x /usr/local/bin/mockery && \
rm -rf mockery-tmp-dir
RUN echo "installing mips binutils" && \
apt-get install -y binutils-mips-linux-gnu python3 python3-pip && \
pip3 install capstone pyelftools
......@@ -7,7 +7,7 @@ import {
waitForProvider,
} from '@eth-optimism/common-ts'
import { getChainId, compareAddrs } from '@eth-optimism/core-utils'
import { Provider } from '@ethersproject/abstract-provider'
import { Provider, TransactionResponse } from '@ethersproject/abstract-provider'
import mainnetConfig from '@eth-optimism/contracts-bedrock/deploy-config/mainnet.json'
import goerliConfig from '@eth-optimism/contracts-bedrock/deploy-config/goerli.json'
import l2OutputOracleArtifactsMainnet from '@eth-optimism/contracts-bedrock/deployments/mainnet/L2OutputOracleProxy.json'
......@@ -101,7 +101,7 @@ export class WalletMonService extends BaseServiceV2<
unexpectedCalls: {
type: Counter,
desc: 'Number of unexpected wallets',
labels: ['wallet', 'target', 'nickname'],
labels: ['wallet', 'target', 'nickname', 'transactionHash'],
},
unexpectedRpcErrors: {
type: Counter,
......@@ -150,7 +150,7 @@ export class WalletMonService extends BaseServiceV2<
number: block.number,
})
const transactions = []
const transactions: TransactionResponse[] = []
for (const txHash of block.transactions) {
const t = await this.options.rpc.getTransaction(txHash)
transactions.push(t)
......@@ -175,11 +175,13 @@ export class WalletMonService extends BaseServiceV2<
nickname: account.label,
wallet: account.address,
target: transaction.to,
transactionHash: transaction.hash,
})
this.logger.error('Unexpected call detected', {
nickname: account.label,
address: account.address,
target: transaction.to,
transactionHash: transaction.hash,
})
}
}
......
......@@ -379,7 +379,7 @@ contract MIPS {
}
// will revert if any required input state is missing
function Step(bytes calldata stateData, bytes calldata proof) public returns (bytes32) {
function step(bytes calldata stateData, bytes calldata proof) public returns (bytes32) {
State memory state;
// packed data is ~6 times smaller
assembly {
......
......@@ -1563,9 +1563,17 @@ export class CrossChainMessenger {
opts?: {
signer?: Signer
overrides?: Overrides
}
},
/**
* The index of the withdrawal if multiple are made with multicall
*/
messageIndex: number = 0
): Promise<TransactionResponse> {
const tx = await this.populateTransaction.proveMessage(message, opts)
const tx = await this.populateTransaction.proveMessage(
message,
opts,
messageIndex
)
return (opts?.signer || this.l1Signer).sendTransaction(tx)
}
......@@ -1584,10 +1592,18 @@ export class CrossChainMessenger {
opts?: {
signer?: Signer
overrides?: PayableOverrides
}
},
/**
* The index of the withdrawal if multiple are made with multicall
*/
messageIndex = 0
): Promise<TransactionResponse> {
return (opts?.signer || this.l1Signer).sendTransaction(
await this.populateTransaction.finalizeMessage(message, opts)
await this.populateTransaction.finalizeMessage(
message,
opts,
messageIndex
)
)
}
......@@ -1810,7 +1826,6 @@ export class CrossChainMessenger {
opts?: {
overrides?: Overrides
},
/**
* The index of the withdrawal if multiple are made with multicall
*/
......@@ -1822,13 +1837,17 @@ export class CrossChainMessenger {
}
if (this.bedrock) {
return this.populateTransaction.finalizeMessage(resolved, {
...(opts || {}),
overrides: {
...opts?.overrides,
gasLimit: messageGasLimit,
return this.populateTransaction.finalizeMessage(
resolved,
{
...(opts || {}),
overrides: {
...opts?.overrides,
gasLimit: messageGasLimit,
},
},
})
messageIndex
)
} else {
const legacyL1XDM = new ethers.Contract(
this.contracts.l1.L1CrossDomainMessenger.address,
......@@ -1861,7 +1880,6 @@ export class CrossChainMessenger {
opts?: {
overrides?: PayableOverrides
},
/**
* The index of the withdrawal if multiple are made with multicall
*/
......@@ -1921,7 +1939,6 @@ export class CrossChainMessenger {
opts?: {
overrides?: PayableOverrides
},
/**
* The index of the withdrawal if multiple are made with multicall
*/
......@@ -2229,10 +2246,18 @@ export class CrossChainMessenger {
message: MessageLike,
opts?: {
overrides?: CallOverrides
}
},
/**
* The index of the withdrawal if multiple are made with multicall
*/
messageIndex = 0
): Promise<BigNumber> => {
return this.l1Provider.estimateGas(
await this.populateTransaction.finalizeMessage(message, opts)
await this.populateTransaction.finalizeMessage(
message,
opts,
messageIndex
)
)
},
......
......@@ -123,6 +123,9 @@ importers:
prettier-plugin-solidity:
specifier: ^1.0.0-beta.13
version: 1.0.0-beta.18(prettier@2.8.1)
rimraf:
specifier: ^5.0.1
version: 5.0.1
ts-mocha:
specifier: ^10.0.0
version: 10.0.0(mocha@8.4.0)
......@@ -9833,7 +9836,7 @@ packages:
fs.realpath: 1.0.0
minimatch: 8.0.4
minipass: 4.2.8
path-scurry: 1.9.2
path-scurry: 1.10.0
dev: true
/global-modules@2.0.0:
......@@ -12149,11 +12152,6 @@ packages:
engines: {node: '>=12'}
dev: true
/lru-cache@9.1.2:
resolution: {integrity: sha512-ERJq3FOzJTxBbFjZ7iDs+NiK4VI9Wz+RdrrAB8dio1oV+YvdPzUEE4QNiT2VD51DkIbCYRUUzCRkssXCHqSnKQ==}
engines: {node: 14 || >=16.14}
dev: true
/lru_map@0.3.3:
resolution: {integrity: sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==}
......@@ -14531,14 +14529,6 @@ packages:
minipass: 6.0.2
dev: true
/path-scurry@1.9.2:
resolution: {integrity: sha512-qSDLy2aGFPm8i4rsbHd4MNyTcrzHFsLQykrtbuGRknZZCBBVXSv2tSCDN2Cg6Rt/GFRw8GoW9y9Ecw5rIPG1sg==}
engines: {node: '>=16 || 14 >=14.17'}
dependencies:
lru-cache: 9.1.2
minipass: 6.0.2
dev: true
/path-to-regexp@0.1.7:
resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==}
......@@ -15807,6 +15797,14 @@ packages:
glob: 9.3.5
dev: true
/rimraf@5.0.1:
resolution: {integrity: sha512-OfFZdwtd3lZ+XZzYP/6gTACubwFcHdLRqS9UX3UwpU2dnGQYkPFISRwvM3w9IiB2w7bW5qGo/uAwE4SmXXSKvg==}
engines: {node: '>=14'}
hasBin: true
dependencies:
glob: 10.3.1
dev: true
/ripemd160@2.0.2:
resolution: {integrity: sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==}
dependencies:
......
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