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

Merge branch 'develop' into inphi/mips-bin

parents 7618e0da 35ff4b47
......@@ -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")
}
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
......@@ -69,6 +69,10 @@ $readloop:
addiu $t0, $t0, -1
bnez $t0, $readloop
nop
# reading the pre-image stream at EOF should have no effect
li $a1, 0x31000008
li $v0, 4003
syscall
# length at 0x31000000. We also check that the lower 32 bits are zero
lui $s1, 0x3100
......
......@@ -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)
}
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")
// inspect test result
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
......@@ -380,12 +361,7 @@ cd ~/op-geth
--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
--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.
......
......@@ -7,6 +7,8 @@
"nx.json": "*",
"tsconfig.json": "*",
".foundryrc": "*",
"pnpm.lock.yaml": "*",
".npmrc": "*",
".nvmrc": "*"
},
"tasksRunnerOptions": {
......
This diff is collapsed.
This diff is collapsed.
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, cfg.AgreeWithProposedOutput, 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 --agree-with-proposed-output=true
package main
import (
"fmt"
"testing"
"github.com/ethereum-optimism/optimism/op-challenger/config"
......@@ -14,6 +15,7 @@ var (
l1EthRpc = "http://example.com:8545"
gameAddressValue = "0xaa00000000000000000000000000000000000000"
alphabetTrace = "abcdefghijz"
agreeWithProposedOutput = "true"
)
func TestLogLevel(t *testing.T) {
......@@ -33,12 +35,12 @@ func TestLogLevel(t *testing.T) {
func TestDefaultCLIOptionsMatchDefaultConfig(t *testing.T) {
cfg := configForArgs(t, addRequiredArgs())
defaultCfg := config.NewConfig(l1EthRpc, common.HexToAddress(gameAddressValue), alphabetTrace)
defaultCfg := config.NewConfig(l1EthRpc, common.HexToAddress(gameAddressValue), alphabetTrace, true)
require.Equal(t, defaultCfg, cfg)
}
func TestDefaultConfigIsValid(t *testing.T) {
cfg := config.NewConfig(l1EthRpc, common.HexToAddress(gameAddressValue), alphabetTrace)
cfg := config.NewConfig(l1EthRpc, common.HexToAddress(gameAddressValue), alphabetTrace, true)
require.NoError(t, cfg.Check())
}
......@@ -89,6 +91,24 @@ func TestTxManagerFlagsSupported(t *testing.T) {
require.Equal(t, uint64(7), cfg.TxMgrConfig.NumConfirmations)
}
func TestAgreeWithProposedOutput(t *testing.T) {
t.Run("MustBeProvided", func(t *testing.T) {
verifyArgsInvalid(t, "flag agree-with-proposed-output is required", addRequiredArgsExcept("--agree-with-proposed-output"))
})
t.Run("Enabled", func(t *testing.T) {
cfg := configForArgs(t, addRequiredArgs("--agree-with-proposed-output"))
require.True(t, cfg.AgreeWithProposedOutput)
})
t.Run("EnabledWithArg", func(t *testing.T) {
cfg := configForArgs(t, addRequiredArgs("--agree-with-proposed-output=true"))
require.True(t, cfg.AgreeWithProposedOutput)
})
t.Run("Disabled", func(t *testing.T) {
cfg := configForArgs(t, addRequiredArgs("--agree-with-proposed-output=false"))
require.False(t, cfg.AgreeWithProposedOutput)
})
}
func verifyArgsInvalid(t *testing.T, messageContains string, cliArgs []string) {
_, _, err := runWithArgs(cliArgs)
require.ErrorContains(t, err, messageContains)
......@@ -103,7 +123,7 @@ func configForArgs(t *testing.T, cliArgs []string) config.Config {
func runWithArgs(cliArgs []string) (log.Logger, config.Config, error) {
cfg := new(config.Config)
var logger log.Logger
fullArgs := append([]string{"op-program"}, cliArgs...)
fullArgs := append([]string{"op-challenger"}, cliArgs...)
err := run(fullArgs, func(log log.Logger, config *config.Config) error {
logger = log
cfg = config
......@@ -126,6 +146,7 @@ func addRequiredArgsExcept(name string, optionalArgs ...string) []string {
func requiredArgs() map[string]string {
return map[string]string{
"--agree-with-proposed-output": agreeWithProposedOutput,
"--l1-eth-rpc": l1EthRpc,
"--game-address": gameAddressValue,
"--alphabet": alphabetTrace,
......@@ -135,8 +156,7 @@ func requiredArgs() map[string]string {
func toArgList(req map[string]string) []string {
var combined []string
for name, value := range req {
combined = append(combined, name)
combined = append(combined, value)
combined = append(combined, fmt.Sprintf("%s=%s", name, value))
}
return combined
}
......@@ -24,19 +24,23 @@ type Config struct {
L1EthRpc string // L1 RPC Url
GameAddress common.Address // Address of the fault game
AlphabetTrace string // String for the AlphabetTraceProvider
AgreeWithProposedOutput bool // Temporary config if we agree or disagree with the posted output
TxMgrConfig txmgr.CLIConfig
}
func NewConfig(l1EthRpc string,
func NewConfig(
l1EthRpc string,
GameAddress common.Address,
AlphabetTrace string,
AgreeWithProposedOutput bool,
) Config {
return Config{
L1EthRpc: l1EthRpc,
GameAddress: GameAddress,
AlphabetTrace: AlphabetTrace,
TxMgrConfig: txmgr.NewCLIConfig(l1EthRpc),
AgreeWithProposedOutput: AgreeWithProposedOutput,
}
}
......@@ -73,6 +77,7 @@ func NewConfigFromCLI(ctx *cli.Context) (*Config, error) {
L1EthRpc: ctx.String(flags.L1EthRpcFlag.Name),
GameAddress: dgfAddress,
AlphabetTrace: ctx.String(flags.AlphabetFlag.Name),
AgreeWithProposedOutput: ctx.Bool(flags.AgreeWithProposedOutputFlag.Name),
TxMgrConfig: txMgrConfig,
}, nil
}
......@@ -12,10 +12,11 @@ var (
validL1EthRpc = "http://localhost:8545"
validGameAddress = common.HexToAddress("0x7bdd3b028C4796eF0EAf07d11394d0d9d8c24139")
validAlphabetTrace = "abcdefgh"
agreeWithProposedOutput = true
)
func validConfig() Config {
cfg := NewConfig(validL1EthRpc, validGameAddress, validAlphabetTrace)
cfg := NewConfig(validL1EthRpc, validGameAddress, validAlphabetTrace, agreeWithProposedOutput)
return cfg
}
......
......@@ -3,6 +3,7 @@ package fault
import (
"context"
"errors"
"fmt"
"github.com/ethereum/go-ethereum/log"
)
......@@ -13,16 +14,18 @@ type Agent struct {
loader Loader
responder Responder
maxDepth int
agreeWithProposedOutput bool
log log.Logger
}
func NewAgent(loader Loader, maxDepth int, trace TraceProvider, responder Responder, log log.Logger) Agent {
func NewAgent(loader Loader, maxDepth int, trace TraceProvider, responder Responder, agreeWithProposedOutput bool, log log.Logger) Agent {
return Agent{
solver: NewSolver(maxDepth, trace),
trace: trace,
loader: loader,
responder: responder,
maxDepth: maxDepth,
agreeWithProposedOutput: agreeWithProposedOutput,
log: log,
}
}
......@@ -36,11 +39,15 @@ func (a *Agent) Act() error {
}
// Create counter claims
for _, claim := range game.Claims() {
_ = a.move(claim, game)
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() {
_ = a.step(claim, game)
if err := a.step(claim, game); err != nil {
log.Error("Failed to step", "err", err)
}
}
return nil
}
......@@ -49,34 +56,33 @@ func (a *Agent) Act() error {
func (a *Agent) newGameFromContracts(ctx context.Context) (Game, error) {
claims, err := a.loader.FetchClaims(ctx)
if err != nil {
return nil, err
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))
game := NewGameState(a.agreeWithProposedOutput, claims[0], uint64(a.maxDepth))
if err := game.PutAll(claims[1:]); err != nil {
return nil, err
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
// move determines & executes the next move given a claim
func (a *Agent) move(claim Claim, game Game) error {
a.log.Info("Fetching claims")
nextMove, err := a.solver.NextMove(claim)
nextMove, err := a.solver.NextMove(claim, game.AgreeWithClaimLevel(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])
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
......@@ -85,25 +91,30 @@ func (a *Agent) move(claim Claim, game Game) error {
return a.responder.Respond(context.TODO(), move)
}
// step attempts to execute the step through the responder
// 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.Info("Failed to get a step", "err", err)
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)
"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)
}
......@@ -26,6 +26,6 @@ func FullGame() {
},
}
o := fault.NewOrchestrator(maxDepth, []fault.TraceProvider{canonicalProvider, disputedProvider}, []string{"charlie", "mallory"}, root)
o := fault.NewOrchestrator(maxDepth, []fault.TraceProvider{canonicalProvider, disputedProvider}, []string{"charlie", "mallory"}, []bool{false, true}, root)
o.Start()
}
......@@ -53,19 +53,19 @@ func SolverExampleOne() {
fmt.Println()
PrettyPrintAlphabetClaim("Root claim", root)
claim1, err := cannonicalSolver.NextMove(root)
claim1, err := cannonicalSolver.NextMove(root, false)
if err != nil {
fmt.Printf("error getting claim from provider: %v", err)
}
PrettyPrintAlphabetClaim("Cannonical move", *claim1)
claim2, err := disputedSolver.NextMove(*claim1)
claim2, err := disputedSolver.NextMove(*claim1, false)
if err != nil {
fmt.Printf("error getting claim from provider: %v", err)
}
PrettyPrintAlphabetClaim("Disputed moved", *claim2)
claim3, err := cannonicalSolver.NextMove(*claim2)
claim3, err := cannonicalSolver.NextMove(*claim2, false)
if err != nil {
fmt.Printf("error getting claim from provider: %v", err)
}
......
......@@ -33,17 +33,20 @@ type Game interface {
// PostStateClaim gets the claim which commits to the post-state of this specific claim.
// This will return an error if it is called with a non-leaf claim.
PostStateClaim(claim Claim) (Claim, error)
// AgreeWithLevel returns if the game state agrees with the provided claim level.
AgreeWithClaimLevel(claim Claim) bool
}
type extendedClaim struct {
self Claim
contractIndex int
children []ClaimData
}
// gameState is a struct that represents the state of a dispute game.
// The game state implements the [Game] interface.
type gameState struct {
agreeWithProposedOutput bool
root ClaimData
claims map[ClaimData]*extendedClaim
depth uint64
......@@ -51,20 +54,32 @@ type gameState struct {
// NewGameState returns a new game state.
// The provided [Claim] is used as the root node.
func NewGameState(root Claim, depth uint64) *gameState {
func NewGameState(agreeWithProposedOutput bool, root Claim, depth uint64) *gameState {
claims := make(map[ClaimData]*extendedClaim)
claims[root.ClaimData] = &extendedClaim{
self: root,
contractIndex: 0,
children: make([]ClaimData, 0),
}
return &gameState{
agreeWithProposedOutput: agreeWithProposedOutput,
root: root.ClaimData,
claims: claims,
depth: depth,
}
}
// AgreeWithLevel returns if the game state agrees with the provided claim level.
func (g *gameState) AgreeWithClaimLevel(claim Claim) bool {
isOddLevel := claim.Depth()%2 == 1
// If we agree with the proposed output, we agree with odd levels
// If we disagree with the proposed output, we agree with the root claim level & even levels
if g.agreeWithProposedOutput {
return isOddLevel
} else {
return !isOddLevel
}
}
// PutAll adds a list of claims into the [Game] state.
// If any of the claims already exist in the game state, an error is returned.
func (g *gameState) PutAll(claims []Claim) error {
......@@ -81,14 +96,14 @@ func (g *gameState) Put(claim Claim) error {
if claim.IsRoot() || g.IsDuplicate(claim) {
return ErrClaimExists
}
if parent, ok := g.claims[claim.Parent]; !ok {
parent, ok := g.claims[claim.Parent]
if !ok {
return errors.New("no parent claim")
} else {
parent.children = append(parent.children, claim.ClaimData)
}
g.claims[claim.ClaimData] = &extendedClaim{
self: claim,
contractIndex: claim.ContractIndex,
children: make([]ClaimData, 0),
}
return nil
......
......@@ -48,7 +48,7 @@ func createTestClaims() (Claim, Claim, Claim, Claim) {
func TestIsDuplicate(t *testing.T) {
// Setup the game state.
root, top, middle, bottom := createTestClaims()
g := NewGameState(root, testMaxDepth)
g := NewGameState(false, root, testMaxDepth)
require.NoError(t, g.Put(top))
// Root + Top should be duplicates
......@@ -65,7 +65,7 @@ func TestIsDuplicate(t *testing.T) {
func TestGame_Put_RootAlreadyExists(t *testing.T) {
// Setup the game state.
top, _, _, _ := createTestClaims()
g := NewGameState(top, testMaxDepth)
g := NewGameState(false, top, testMaxDepth)
// Try to put the root claim into the game state again.
err := g.Put(top)
......@@ -77,7 +77,7 @@ func TestGame_Put_RootAlreadyExists(t *testing.T) {
func TestGame_PutAll_RootAlreadyExists(t *testing.T) {
// Setup the game state.
root, _, _, _ := createTestClaims()
g := NewGameState(root, testMaxDepth)
g := NewGameState(false, root, testMaxDepth)
// Try to put the root claim into the game state again.
err := g.PutAll([]Claim{root})
......@@ -88,7 +88,7 @@ func TestGame_PutAll_RootAlreadyExists(t *testing.T) {
// instance errors when the given claim already exists in state.
func TestGame_PutAll_AlreadyExists(t *testing.T) {
root, top, middle, bottom := createTestClaims()
g := NewGameState(root, testMaxDepth)
g := NewGameState(false, root, testMaxDepth)
err := g.PutAll([]Claim{top, middle})
require.NoError(t, err)
......@@ -101,7 +101,7 @@ func TestGame_PutAll_AlreadyExists(t *testing.T) {
func TestGame_PutAll_ParentsAndChildren(t *testing.T) {
// Setup the game state.
root, top, middle, bottom := createTestClaims()
g := NewGameState(root, testMaxDepth)
g := NewGameState(false, root, testMaxDepth)
// We should not be able to get the parent of the root claim.
parent, err := g.getParent(root)
......@@ -127,7 +127,7 @@ func TestGame_PutAll_ParentsAndChildren(t *testing.T) {
func TestGame_Put_AlreadyExists(t *testing.T) {
// Setup the game state.
top, middle, _, _ := createTestClaims()
g := NewGameState(top, testMaxDepth)
g := NewGameState(false, top, testMaxDepth)
// Put the next claim into state.
err := g.Put(middle)
......@@ -142,7 +142,7 @@ func TestGame_Put_AlreadyExists(t *testing.T) {
func TestGame_Put_ParentsAndChildren(t *testing.T) {
// Setup the game state.
root, top, middle, bottom := createTestClaims()
g := NewGameState(root, testMaxDepth)
g := NewGameState(false, root, testMaxDepth)
// We should not be able to get the parent of the root claim.
parent, err := g.getParent(root)
......@@ -175,7 +175,7 @@ func TestGame_Put_ParentsAndChildren(t *testing.T) {
func TestGame_ClaimPairs(t *testing.T) {
// Setup the game state.
root, top, middle, bottom := createTestClaims()
g := NewGameState(root, testMaxDepth)
g := NewGameState(false, root, testMaxDepth)
// Add top claim to the game state.
err := g.Put(top)
......@@ -199,7 +199,7 @@ func TestGame_ClaimPairs(t *testing.T) {
// those functions return an error.
func TestPrePostStateOnlyOnLeafClaim(t *testing.T) {
root, top, middle, bottom := createTestClaims()
g := NewGameState(root, testMaxDepth)
g := NewGameState(false, root, testMaxDepth)
require.NoError(t, g.PutAll([]Claim{top, middle, bottom}))
_, err := g.PreStateClaim(middle)
......@@ -210,7 +210,7 @@ func TestPrePostStateOnlyOnLeafClaim(t *testing.T) {
func TestPreStateClaim(t *testing.T) {
root, top, middle, bottom := createTestClaims()
g := NewGameState(root, testMaxDepth)
g := NewGameState(false, root, testMaxDepth)
require.NoError(t, g.Put(top))
require.NoError(t, g.Put(middle))
require.NoError(t, g.Put(bottom))
......@@ -224,7 +224,7 @@ func TestPreStateClaim(t *testing.T) {
func TestPostStateClaim(t *testing.T) {
root, top, middle, bottom := createTestClaims()
g := NewGameState(root, testMaxDepth)
g := NewGameState(false, root, testMaxDepth)
require.NoError(t, g.Put(top))
require.NoError(t, g.Put(middle))
require.NoError(t, g.Put(bottom))
......@@ -234,3 +234,27 @@ func TestPostStateClaim(t *testing.T) {
require.NoError(t, err)
require.Equal(t, middle, post)
}
func TestAgreeWithClaimLevelDisagreeWithOutput(t *testing.T) {
// Setup the game state.
root, top, middle, bottom := createTestClaims()
g := NewGameState(false, root, testMaxDepth)
require.NoError(t, g.PutAll([]Claim{top, middle, bottom}))
require.True(t, g.AgreeWithClaimLevel(root))
require.False(t, g.AgreeWithClaimLevel(top))
require.True(t, g.AgreeWithClaimLevel(middle))
require.False(t, g.AgreeWithClaimLevel(bottom))
}
func TestAgreeWithClaimLevelAgreeWithOutput(t *testing.T) {
// Setup the game state.
root, top, middle, bottom := createTestClaims()
g := NewGameState(true, root, testMaxDepth)
require.NoError(t, g.PutAll([]Claim{top, middle, bottom}))
require.False(t, g.AgreeWithClaimLevel(root))
require.True(t, g.AgreeWithClaimLevel(top))
require.False(t, g.AgreeWithClaimLevel(middle))
require.True(t, g.AgreeWithClaimLevel(bottom))
}
......@@ -29,17 +29,13 @@ type Loader interface {
// loader pulls in fault dispute game claim data periodically and over subscriptions.
type loader struct {
log log.Logger
state Game
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)
......
......@@ -15,7 +15,7 @@ type Orchestrator struct {
claimLen, stepLen, step int
}
func NewOrchestrator(maxDepth uint64, traces []TraceProvider, names []string, root Claim) Orchestrator {
func NewOrchestrator(maxDepth uint64, traces []TraceProvider, names []string, agreeWithProposedOutput []bool, root Claim) Orchestrator {
o := Orchestrator{
agents: make([]Agent, len(traces)),
claims: []Claim{root},
......@@ -23,7 +23,7 @@ func NewOrchestrator(maxDepth uint64, traces []TraceProvider, names []string, ro
}
log.Info("Starting game", "root_letter", string(root.Value[31:]))
for i, trace := range traces {
o.agents[i] = NewAgent(&o, int(maxDepth), trace, &o, log.New("role", names[i]))
o.agents[i] = NewAgent(&o, int(maxDepth), trace, &o, agreeWithProposedOutput[i], log.New("role", names[i]))
}
return o
}
......
......@@ -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
}
......@@ -22,7 +21,11 @@ func NewSolver(gameDepth int, traceProvider TraceProvider) *Solver {
}
// NextMove returns the next move to make given the current state of the game.
func (s *Solver) NextMove(claim Claim) (*Claim, error) {
func (s *Solver) NextMove(claim Claim, agreeWithClaimLevel bool) (*Claim, error) {
if agreeWithClaimLevel {
return nil, nil
}
// Special case of the root claim
if claim.IsRoot() {
return s.handleRoot(claim)
......@@ -30,33 +33,14 @@ func (s *Solver) NextMove(claim Claim) (*Claim, error) {
return s.handleMiddle(claim)
}
type StepData struct {
LeafClaim Claim
IsAttack bool
}
// 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) (StepData, error) {
if claim.Depth() != s.gameDepth {
return StepData{}, errors.New("cannot step on non-leaf claims")
}
claimCorrect, err := s.agreeWithClaim(claim.ClaimData)
if err != nil {
return StepData{}, err
}
return StepData{
LeafClaim: claim,
IsAttack: claimCorrect,
}, nil
}
func (s *Solver) handleRoot(claim Claim) (*Claim, error) {
agree, err := s.agreeWithClaim(claim.ClaimData)
if err != nil {
return nil, err
}
// Attack the root claim if we do not agree with it
// Note: We always disagree with the claim level at this point,
// so if we agree with claim maybe we should also attack?
if !agree {
return s.attack(claim)
} else {
......@@ -65,10 +49,6 @@ func (s *Solver) handleRoot(claim Claim) (*Claim, error) {
}
func (s *Solver) handleMiddle(claim Claim) (*Claim, error) {
parentCorrect, err := s.agreeWithClaim(claim.Parent)
if err != nil {
return nil, err
}
claimCorrect, err := s.agreeWithClaim(claim.ClaimData)
if err != nil {
return nil, err
......@@ -76,25 +56,39 @@ func (s *Solver) handleMiddle(claim Claim) (*Claim, error) {
if claim.Depth() == s.gameDepth {
return nil, errors.New("game depth reached")
}
if parentCorrect && claimCorrect {
// We agree with the parent, but the claim is disagreeing with it.
// Since we agree with the claim, the difference must be to the right of the claim
if claimCorrect {
return s.defend(claim)
} else if parentCorrect && !claimCorrect {
// We agree with the parent, but the claim disagrees with it.
// Since we disagree with the claim, the difference must be to the left of the claim
return s.attack(claim)
} else if !parentCorrect && claimCorrect {
// Do nothing, we disagree with the parent, but this claim has correctly countered it
return nil, nil
} else if !parentCorrect && !claimCorrect {
// We disagree with the parent so want to counter it (which the claim is doing)
// but we also disagree with the claim so there must be a difference to the left of claim
// Note that we will create the correct counter-claim for parent when it is evaluated, no need to do it here
} else {
return s.attack(claim)
}
// This should not be reached
return nil, errors.New("no next move")
}
type StepData struct {
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) (StepData, error) {
if claim.Depth() != s.gameDepth {
return StepData{}, errors.New("cannot step on non-leaf claims")
}
claimCorrect, err := s.agreeWithClaim(claim.ClaimData)
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,
IsAttack: !claimCorrect,
PreStateTraceIndex: index,
}, nil
}
// attack returns a response that attacks the claim.
......@@ -107,6 +101,7 @@ func (s *Solver) attack(claim Claim) (*Claim, error) {
return &Claim{
ClaimData: ClaimData{Value: value, Position: position},
Parent: claim.ClaimData,
ParentContractIndex: claim.ContractIndex,
}, nil
}
......@@ -120,6 +115,7 @@ func (s *Solver) defend(claim Claim) (*Claim, error) {
return &Claim{
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,73 +23,87 @@ 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
}{
{
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),
},
},
}
for _, test := range indices {
res, err := solver.NextMove(test.claim)
res, err := solver.NextMove(test.claim, false)
require.NoError(t, err)
require.Equal(t, test.response, res.ClaimData)
}
}
func TestNoMoveAgainstOwnLevel(t *testing.T) {
maxDepth := 3
mallory := NewAlphabetProvider("abcdepqr", uint64(maxDepth))
solver := NewSolver(maxDepth, mallory)
claim := Claim{
ClaimData: ClaimData{
Value: alphabetClaim(7, "z"),
Position: NewPosition(0, 0),
},
// Root claim has no parent
}
move, err := solver.NextMove(claim, true)
require.Nil(t, move)
require.Nil(t, err)
}
func TestAttemptStep(t *testing.T) {
maxDepth := 3
canonicalProvider := NewAlphabetProvider("abcdefgh", uint64(maxDepth))
solver := NewSolver(maxDepth, canonicalProvider)
root, top, middle, bottom := createTestClaims()
g := NewGameState(root, testMaxDepth)
g := NewGameState(false, root, testMaxDepth)
require.NoError(t, g.Put(top))
require.NoError(t, g.Put(middle))
require.NoError(t, g.Put(bottom))
......
......@@ -33,6 +33,11 @@ var (
Usage: "Alphabet Trace (temporary)",
EnvVars: prefixEnvVars("ALPHABET"),
}
AgreeWithProposedOutputFlag = &cli.BoolFlag{
Name: "agree-with-proposed-output",
Usage: "Temporary hardcoded flag if we agree or disagree with the proposed output.",
EnvVars: prefixEnvVars("AGREE_WITH_PROPOSED_OUTPUT"),
}
// Optional Flags
)
......@@ -41,6 +46,7 @@ var requiredFlags = []cli.Flag{
L1EthRpcFlag,
DGFAddressFlag,
AlphabetFlag,
AgreeWithProposedOutputFlag,
}
// optionalFlags is a list of unchecked cli flags
......
#!/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 --agree-with-proposed-output=false
#!/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"
......@@ -27,7 +27,7 @@ type Frame struct {
ID ChannelID `json:"id"`
FrameNumber uint16 `json:"frame_number"`
Data []byte `json:"data"`
IsLast bool `'json:"is_last"`
IsLast bool `json:"is_last"`
}
// MarshalBinary writes the frame to `w`.
......
......@@ -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',
......
......@@ -331,6 +331,9 @@ OptimismPortal_Test:test_receive_succeeds() (gas: 127513)
OptimismPortal_Test:test_simple_isOutputFinalized_succeeds() (gas: 32971)
OptimismPortal_Test:test_unpause_onlyGuardian_reverts() (gas: 46098)
OptimismPortal_Test:test_unpause_succeeds() (gas: 31756)
PreimageOracle_Test:test_computePreimageKey_succeeds() (gas: 6267)
PreimageOracle_Test:test_loadKeccak256PreimagePart_outOfBoundsOffset_reverts() (gas: 9025)
PreimageOracle_Test:test_loadKeccak256PreimagePart_succeeds() (gas: 77552)
ProxyAdmin_Test:test_chugsplashChangeProxyAdmin_succeeds() (gas: 35586)
ProxyAdmin_Test:test_chugsplashGetProxyAdmin_succeeds() (gas: 15675)
ProxyAdmin_Test:test_chugsplashGetProxyImplementation_succeeds() (gas: 51084)
......
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
pragma solidity ^0.8.15;
/// @title PreimageOracle
/// @notice A contract for storing permissioned pre-images.
contract PreimageOracle {
/// @notice Mapping of pre-image keys to pre-image lengths.
mapping(bytes32 => uint256) public preimageLengths;
/// @notice Mapping of pre-image keys to pre-image parts.
mapping(bytes32 => mapping(uint256 => bytes32)) public preimageParts;
/// @notice Mapping of pre-image keys to pre-image part offsets.
mapping(bytes32 => mapping(uint256 => bool)) public preimagePartOk;
function readPreimage(bytes32 key, uint256 offset)
/// @notice Reads a pre-image from the oracle.
/// @param _key The key of the pre-image to read.
/// @param _offset The offset of the pre-image to read.
/// @return dat_ The pre-image data.
/// @return datLen_ The length of the pre-image data.
function readPreimage(bytes32 _key, uint256 _offset)
external
view
returns (bytes32 dat, uint256 datLen)
returns (bytes32 dat_, uint256 datLen_)
{
require(preimagePartOk[key][offset], "preimage must exist");
datLen = 32;
uint256 length = preimageLengths[key];
// add 8 for the length-prefix part
if (offset + 32 >= length + 8) {
datLen = length + 8 - offset;
require(preimagePartOk[_key][_offset], "pre-image must exist");
// Calculate the length of the pre-image data
// Add 8 for the length-prefix part
datLen_ = 32;
uint256 length = preimageLengths[_key];
if (_offset + 32 >= length + 8) {
datLen_ = length + 8 - _offset;
}
dat = preimageParts[key][offset];
// Retrieve the pre-image data
dat_ = preimageParts[_key][_offset];
}
// TODO(CLI-4104):
......@@ -37,17 +53,47 @@ contract PreimageOracle {
preimageLengths[key] = size;
}
// loadKeccak256PreimagePart prepares the pre-image to be read by keccak256 key,
// starting at the given offset, up to 32 bytes (clipped at preimage length, if out of data).
function loadKeccak256PreimagePart(uint256 partOffset, bytes calldata preimage) external {
/// @notice Computes and returns the key for a pre-image.
/// @param _preimage The pre-image.
/// @return key_ The pre-image key.
function computePreimageKey(bytes calldata _preimage) external pure returns (bytes32 key_) {
uint256 size;
assembly {
size := calldataload(0x24)
// Leave slots 0x40 and 0x60 untouched,
// and everything after as scratch-memory.
let ptr := 0x80
// Store size as a big-endian uint64 at the start of pre-image
mstore(ptr, shl(192, size))
ptr := add(ptr, 8)
// Copy preimage payload into memory so we can hash and read it.
calldatacopy(ptr, _preimage.offset, size)
// Compute the pre-image keccak256 hash (aka the pre-image key)
let h := keccak256(ptr, size)
// Mask out prefix byte, replace with type 2 byte
key_ := or(and(h, not(shl(248, 0xFF))), shl(248, 2))
}
}
/// @notice Prepares a pre-image to be read by keccak256 key, starting at
/// the given offset and up to 32 bytes (clipped at pre-image length, if out of data).
/// @param _partOffset The offset of the pre-image to read.
/// @param _preimage The preimage data.
function loadKeccak256PreimagePart(uint256 _partOffset, bytes calldata _preimage) external {
uint256 size;
bytes32 key;
bytes32 part;
assembly {
// len(sig) + len(partOffset) + len(preimage offset) = 4 + 32 + 32 = 0x44
size := calldataload(0x44)
// revert if part offset >= size+8 (i.e. parts must be within bounds)
if iszero(lt(partOffset, add(size, 8))) {
// revert if part offset > size+8 (i.e. parts must be within bounds)
if gt(_partOffset, add(size, 8)) {
revert(0, 0)
}
// we leave solidity slots 0x40 and 0x60 untouched,
......@@ -57,16 +103,16 @@ contract PreimageOracle {
mstore(ptr, shl(192, size))
ptr := add(ptr, 8)
// copy preimage payload into memory so we can hash and read it.
calldatacopy(ptr, preimage.offset, size)
calldatacopy(ptr, _preimage.offset, size)
// Note that it includes the 8-byte big-endian uint64 length prefix.
// this will be zero-padded at the end, since memory at end is clean.
part := mload(add(sub(ptr, 8), partOffset))
part := mload(add(sub(ptr, 8), _partOffset))
let h := keccak256(ptr, size) // compute preimage keccak256 hash
// mask out prefix byte, replace with type 2 byte
key := or(and(h, not(shl(248, 0xFF))), shl(248, 2))
}
preimagePartOk[key][partOffset] = true;
preimageParts[key][partOffset] = part;
preimagePartOk[key][_partOffset] = true;
preimageParts[key][_partOffset] = part;
preimageLengths[key] = size;
}
}
// SPDX-License-Identifier: MIT
pragma solidity 0.7.6;
/// @title IPreimageOracle
/// @notice Interface for a preimage oracle.
interface IPreimageOracle {
/// @notice Reads a preimage from the oracle.
/// @param _key The key of the preimage to read.
/// @param _offset The offset of the preimage to read.
/// @return dat_ The preimage data.
/// @return datLen_ The length of the preimage data.
function readPreimage(bytes32 _key, uint256 _offset)
external
view
returns (bytes32 dat_, uint256 datLen_);
/// @notice Computes and returns the key for a pre-image.
/// @param _preimage The pre-image.
/// @return key_ The pre-image key.
function computePreimageKey(bytes calldata _preimage) external pure returns (bytes32 key_);
/// @notice Prepares a preimage to be read by keccak256 key, starting at
/// the given offset and up to 32 bytes (clipped at preimage length, if out of data).
/// @param _partOffset The offset of the preimage to read.
/// @param _preimage The preimage data.
function loadKeccak256PreimagePart(uint256 _partOffset, bytes calldata _preimage) external;
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
import { Test } from "forge-std/Test.sol";
import { PreimageOracle } from "../cannon/PreimageOracle.sol";
contract PreimageOracle_Test is Test {
PreimageOracle oracle;
/// @notice Sets up the testing suite.
function setUp() public {
oracle = new PreimageOracle();
vm.label(address(oracle), "PreimageOracle");
}
/// @notice Test the pre-image key computation with a known pre-image.
function test_computePreimageKey_succeeds() public {
bytes memory preimage = hex"deadbeef";
bytes32 key = oracle.computePreimageKey(preimage);
bytes32 known = 0x02fd4e189132273036449fc9e11198c739161b4c0116a9a2dccdfa1c492006f1;
assertEq(key, known);
}
/// @notice Tests that a pre-image is correctly set.
function test_loadKeccak256PreimagePart_succeeds() public {
// Set the pre-image
bytes memory preimage = hex"deadbeef";
bytes32 key = oracle.computePreimageKey(preimage);
uint256 offset = 0;
oracle.loadKeccak256PreimagePart(offset, preimage);
// Validate the pre-image part
bytes32 part = oracle.preimageParts(key, offset);
bytes32 expectedPart = 0x0000000000000004deadbeef0000000000000000000000000000000000000000;
assertEq(part, expectedPart);
// Validate the pre-image length
uint256 length = oracle.preimageLengths(key);
assertEq(length, preimage.length);
// Validate that the pre-image part is set
bool ok = oracle.preimagePartOk(key, offset);
assertTrue(ok);
}
/// @notice Tests that a pre-image cannot be set with an out-of-bounds offset.
function test_loadKeccak256PreimagePart_outOfBoundsOffset_reverts() public {
bytes memory preimage = hex"deadbeef";
uint256 offset = preimage.length + 9;
vm.expectRevert();
oracle.loadKeccak256PreimagePart(offset, preimage);
}
/// @notice Reading a pre-image part that has not been set should revert.
function testFuzz_readPreimage_missingPreimage_reverts(bytes32 key, uint256 offset) public {
vm.expectRevert("pre-image must exist");
oracle.readPreimage(key, offset);
}
}
This diff is collapsed.
This source diff could not be displayed because it is too large. You can view the blob instead.
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