Commit a81de910 authored by Matthew Slipper's avatar Matthew Slipper Committed by GitHub

Merge pull request #11503 from ethereum-optimism/inphi/audit-fixes

Fault Proof Fixes
parents 2a995793 3a5e07f1
......@@ -153,7 +153,8 @@ func ExecuteMipsInstruction(insn, opcode, fun, rs, rt, mem uint32) uint32 {
case 0x06: // srlv
return rt >> (rs & 0x1F)
case 0x07: // srav
return SignExtend(rt>>rs, 32-rs)
shamt := rs & 0x1F
return SignExtend(rt>>shamt, 32-shamt)
// functs in range [0x8, 0x1b] are handled specially by other functions
case 0x08: // jr
return rs
......
......@@ -9,6 +9,7 @@ import (
"github.com/ethereum-optimism/optimism/cannon/mipsevm"
"github.com/ethereum-optimism/optimism/cannon/mipsevm/memory"
"github.com/ethereum-optimism/optimism/cannon/mipsevm/program"
)
// Syscall codes
......@@ -132,7 +133,6 @@ const (
// Other constants
const (
SchedQuantum = 100_000
BrkStart = 0x40000000
)
func GetSyscallArgs(registers *[32]uint32) (syscallNum, a0, a1, a2, a3 uint32) {
......@@ -158,6 +158,12 @@ func HandleSysMmap(a0, a1, heap uint32) (v0, v1, newHeap uint32) {
v0 = heap
//fmt.Printf("mmap heap 0x%x size 0x%x\n", v0, sz)
newHeap += sz
// Fail if new heap exceeds memory limit, newHeap overflows around to low memory, or sz overflows
if newHeap > program.HEAP_END || newHeap < heap || sz < a1 {
v0 = SysErrorSignal
v1 = MipsEINVAL
return v0, v1, heap
}
} else {
v0 = a0
//fmt.Printf("mmap hint 0x%x size 0x%x\n", v0, sz)
......
......@@ -10,6 +10,7 @@ import (
"github.com/ethereum-optimism/optimism/cannon/mipsevm"
"github.com/ethereum-optimism/optimism/cannon/mipsevm/exec"
"github.com/ethereum-optimism/optimism/cannon/mipsevm/program"
)
func (m *InstrumentedState) handleSyscall() error {
......@@ -26,7 +27,7 @@ func (m *InstrumentedState) handleSyscall() error {
v0, v1, newHeap = exec.HandleSysMmap(a0, a1, m.state.Heap)
m.state.Heap = newHeap
case exec.SysBrk:
v0 = exec.BrkStart
v0 = program.PROGRAM_BREAK
case exec.SysClone: // clone
// a0 = flag bitmask, a1 = stack pointer
if exec.ValidCloneFlags != a0 {
......
......@@ -9,7 +9,11 @@ import (
"github.com/ethereum-optimism/optimism/cannon/mipsevm"
)
const HEAP_START = 0x05000000
const (
HEAP_START = 0x05_00_00_00
HEAP_END = 0x60_00_00_00
PROGRAM_BREAK = 0x40_00_00_00
)
type CreateInitialFPVMState[T mipsevm.FPVMState] func(pc, heapStart uint32) T
......
......@@ -5,6 +5,7 @@ import (
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum-optimism/optimism/cannon/mipsevm/exec"
"github.com/ethereum-optimism/optimism/cannon/mipsevm/program"
)
func (m *InstrumentedState) handleSyscall() error {
......@@ -20,7 +21,7 @@ func (m *InstrumentedState) handleSyscall() error {
v0, v1, newHeap = exec.HandleSysMmap(a0, a1, m.state.Heap)
m.state.Heap = newHeap
case exec.SysBrk:
v0 = exec.BrkStart
v0 = program.PROGRAM_BREAK
case exec.SysClone: // clone (not supported)
v0 = 1
case exec.SysExitGroup:
......
......@@ -5,6 +5,7 @@ import (
"io"
"os"
"path"
"strings"
"testing"
"time"
......@@ -18,6 +19,7 @@ import (
"github.com/ethereum-optimism/optimism/cannon/mipsevm/exec"
"github.com/ethereum-optimism/optimism/cannon/mipsevm/memory"
"github.com/ethereum-optimism/optimism/cannon/mipsevm/multithreaded"
"github.com/ethereum-optimism/optimism/cannon/mipsevm/program"
"github.com/ethereum-optimism/optimism/cannon/mipsevm/singlethreaded"
"github.com/ethereum-optimism/optimism/cannon/mipsevm/testutil"
)
......@@ -48,6 +50,7 @@ func TestEVM(t *testing.T) {
oracle := testutil.SelectOracleFixture(t, f.Name())
// Short-circuit early for exit_group.bin
exitGroup := f.Name() == "exit_group.bin"
expectPanic := strings.HasSuffix(f.Name(), "panic.bin")
evm := testutil.NewMIPSEVM(contracts, addrs)
evm.SetTracer(tracer)
......@@ -66,6 +69,17 @@ func TestEVM(t *testing.T) {
goState := singlethreaded.NewInstrumentedState(state, oracle, os.Stdout, os.Stderr, nil)
// Catch panics and check if they are expected
defer func() {
if r := recover(); r != nil {
if expectPanic {
// Success
} else {
t.Errorf("unexpected panic: %v", r)
}
}
}()
for i := 0; i < 1000; i++ {
curStep := goState.GetState().GetStep()
if goState.GetState().GetPC() == testutil.EndAddr {
......@@ -90,6 +104,8 @@ func TestEVM(t *testing.T) {
require.NotEqual(t, uint32(testutil.EndAddr), goState.GetState().GetPC(), "must not reach end")
require.True(t, goState.GetState().GetExited(), "must set exited state")
require.Equal(t, uint8(1), goState.GetState().GetExitCode(), "must exit with 1")
} else if expectPanic {
require.NotEqual(t, uint32(testutil.EndAddr), goState.GetState().GetPC(), "must not reach end")
} else {
require.Equal(t, uint32(testutil.EndAddr), state.Cpu.PC, "must reach end")
// inspect test result
......@@ -193,6 +209,88 @@ func TestEVMSingleStep(t *testing.T) {
}
}
func TestEVM_MMap(t *testing.T) {
contracts, addrs := testContractsSetup(t)
var tracer *tracing.Hooks
cases := []struct {
name string
heap uint32
address uint32
size uint32
shouldFail bool
expectedHeap uint32
}{
{name: "Increment heap by max value", heap: program.HEAP_START, address: 0, size: ^uint32(0), shouldFail: true},
{name: "Increment heap to 0", heap: program.HEAP_START, address: 0, size: ^uint32(0) - program.HEAP_START + 1, shouldFail: true},
{name: "Increment heap to previous page", heap: program.HEAP_START, address: 0, size: ^uint32(0) - program.HEAP_START - memory.PageSize + 1, shouldFail: true},
{name: "Increment max page size", heap: program.HEAP_START, address: 0, size: ^uint32(0) & ^uint32(memory.PageAddrMask), shouldFail: true},
{name: "Increment max page size from 0", heap: 0, address: 0, size: ^uint32(0) & ^uint32(memory.PageAddrMask), shouldFail: true},
{name: "Increment heap at limit", heap: program.HEAP_END, address: 0, size: 1, shouldFail: true},
{name: "Increment heap to limit", heap: program.HEAP_END - memory.PageSize, address: 0, size: 1, shouldFail: false, expectedHeap: program.HEAP_END},
{name: "Increment heap within limit", heap: program.HEAP_END - 2*memory.PageSize, address: 0, size: 1, shouldFail: false, expectedHeap: program.HEAP_END - memory.PageSize},
{name: "Request specific address", heap: program.HEAP_START, address: 0x50_00_00_00, size: 0, shouldFail: false, expectedHeap: program.HEAP_START},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
state := singlethreaded.CreateEmptyState()
state.Heap = c.heap
state.Memory.SetMemory(state.GetPC(), syscallInsn)
state.Registers = testutil.RandomRegisters(77)
state.Registers[2] = exec.SysMmap
state.Registers[4] = c.address
state.Registers[5] = c.size
step := state.Step
expectedRegisters := state.Registers
expectedHeap := state.Heap
expectedMemoryRoot := state.Memory.MerkleRoot()
if c.shouldFail {
expectedRegisters[2] = exec.SysErrorSignal
expectedRegisters[7] = exec.MipsEINVAL
} else {
expectedHeap = c.expectedHeap
if c.address == 0 {
expectedRegisters[2] = state.Heap
expectedRegisters[7] = 0
} else {
expectedRegisters[2] = c.address
expectedRegisters[7] = 0
}
}
us := singlethreaded.NewInstrumentedState(state, nil, os.Stdout, os.Stderr, nil)
stepWitness, err := us.Step(true)
require.NoError(t, err)
// Check expectations
require.Equal(t, step+1, state.Step)
require.Equal(t, expectedHeap, state.Heap)
require.Equal(t, expectedRegisters, state.Registers)
require.Equal(t, expectedMemoryRoot, state.Memory.MerkleRoot())
require.Equal(t, common.Hash{}, state.PreimageKey)
require.Equal(t, uint32(0), state.PreimageOffset)
require.Equal(t, uint32(4), state.Cpu.PC)
require.Equal(t, uint32(8), state.Cpu.NextPC)
require.Equal(t, uint32(0), state.Cpu.HI)
require.Equal(t, uint32(0), state.Cpu.LO)
require.Equal(t, false, state.Exited)
require.Equal(t, uint8(0), state.ExitCode)
require.Equal(t, hexutil.Bytes(nil), state.LastHint)
evm := testutil.NewMIPSEVM(contracts, addrs)
evm.SetTracer(tracer)
testutil.LogStepFailureAtCleanup(t, evm)
evmPost := evm.Step(t, stepWitness, step, singlethreaded.GetStateHashFn())
goPost, _ := us.GetState().EncodeWitness()
require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(),
"mipsevm produced different state than EVM")
})
}
}
func TestEVMSysWriteHint(t *testing.T) {
contracts, addrs := testContractsSetup(t)
var tracer *tracing.Hooks
......
......@@ -14,6 +14,7 @@ import (
"github.com/ethereum-optimism/optimism/cannon/mipsevm"
"github.com/ethereum-optimism/optimism/cannon/mipsevm/exec"
"github.com/ethereum-optimism/optimism/cannon/mipsevm/memory"
"github.com/ethereum-optimism/optimism/cannon/mipsevm/program"
"github.com/ethereum-optimism/optimism/cannon/mipsevm/singlethreaded"
"github.com/ethereum-optimism/optimism/cannon/mipsevm/testutil"
preimage "github.com/ethereum-optimism/optimism/op-preimage"
......@@ -45,7 +46,7 @@ func FuzzStateSyscallBrk(f *testing.F) {
state.Memory.SetMemory(pc, syscallInsn)
preStateRoot := state.Memory.MerkleRoot()
expectedRegisters := state.Registers
expectedRegisters[2] = 0x4000_0000
expectedRegisters[2] = program.PROGRAM_BREAK
goState := singlethreaded.NewInstrumentedState(state, nil, os.Stdout, os.Stderr, nil)
stepWitness, err := goState.Step(true)
......@@ -127,7 +128,14 @@ func FuzzStateSyscallClone(f *testing.F) {
func FuzzStateSyscallMmap(f *testing.F) {
contracts, addrs := testContractsSetup(f)
step := uint64(0)
f.Fuzz(func(t *testing.T, addr uint32, siz uint32, heap uint32) {
// Add special cases for large memory allocation
f.Add(uint32(0), uint32(0x1000), uint32(program.HEAP_END), int64(1))
f.Add(uint32(0), uint32(1<<31), uint32(program.HEAP_START), int64(2))
// Check edge case - just within bounds
f.Add(uint32(0), uint32(0x1000), uint32(program.HEAP_END-4096), int64(3))
f.Fuzz(func(t *testing.T, addr uint32, siz uint32, heap uint32, seed int64) {
state := &singlethreaded.State{
Cpu: mipsevm.CpuScalars{
PC: 0,
......@@ -139,11 +147,14 @@ func FuzzStateSyscallMmap(f *testing.F) {
ExitCode: 0,
Exited: false,
Memory: memory.NewMemory(),
Registers: [32]uint32{2: exec.SysMmap, 4: addr, 5: siz},
Registers: testutil.RandomRegisters(seed),
Step: step,
PreimageOffset: 0,
}
state.Memory.SetMemory(0, syscallInsn)
state.Registers[2] = exec.SysMmap
state.Registers[4] = addr
state.Registers[5] = siz
preStateRoot := state.Memory.MerkleRoot()
preStateRegisters := state.Registers
......@@ -152,32 +163,42 @@ func FuzzStateSyscallMmap(f *testing.F) {
require.NoError(t, err)
require.False(t, stepWitness.HasPreimage())
require.Equal(t, uint32(4), state.Cpu.PC)
require.Equal(t, uint32(8), state.Cpu.NextPC)
require.Equal(t, uint32(0), state.Cpu.LO)
require.Equal(t, uint32(0), state.Cpu.HI)
require.Equal(t, uint8(0), state.ExitCode)
require.Equal(t, false, state.Exited)
require.Equal(t, preStateRoot, state.Memory.MerkleRoot())
require.Equal(t, uint64(1), state.Step)
require.Equal(t, common.Hash{}, state.PreimageKey)
require.Equal(t, uint32(0), state.PreimageOffset)
var expectedHeap uint32
expectedRegisters := preStateRegisters
if addr == 0 {
expectedRegisters := preStateRegisters
expectedRegisters[2] = heap
require.Equal(t, expectedRegisters, state.Registers)
sizAlign := siz
if sizAlign&memory.PageAddrMask != 0 { // adjust size to align with page size
sizAlign = siz + memory.PageSize - (siz & memory.PageAddrMask)
}
require.Equal(t, uint32(heap+sizAlign), state.Heap)
newHeap := heap + sizAlign
if newHeap > program.HEAP_END || newHeap < heap || sizAlign < siz {
expectedHeap = heap
expectedRegisters[2] = exec.SysErrorSignal
expectedRegisters[7] = exec.MipsEINVAL
} else {
expectedRegisters[2] = heap
expectedRegisters[7] = 0 // no error
expectedHeap = heap + sizAlign
}
} else {
expectedRegisters := preStateRegisters
expectedRegisters[2] = addr
require.Equal(t, expectedRegisters, state.Registers)
require.Equal(t, uint32(heap), state.Heap)
expectedRegisters[7] = 0 // no error
expectedHeap = heap
}
require.Equal(t, uint32(4), state.Cpu.PC)
require.Equal(t, uint32(8), state.Cpu.NextPC)
require.Equal(t, uint32(0), state.Cpu.LO)
require.Equal(t, uint32(0), state.Cpu.HI)
require.Equal(t, preStateRoot, state.Memory.MerkleRoot())
require.Equal(t, uint64(1), state.Step)
require.Equal(t, common.Hash{}, state.PreimageKey)
require.Equal(t, uint32(0), state.PreimageOffset)
require.Equal(t, expectedHeap, state.Heap)
require.Equal(t, uint8(0), state.ExitCode)
require.Equal(t, false, state.Exited)
require.Equal(t, expectedRegisters, state.Registers)
evm := testutil.NewMIPSEVM(contracts, addrs)
evmPost := evm.Step(t, stepWitness, step, singlethreaded.GetStateHashFn())
goPost, _ := goState.GetState().EncodeWitness()
......
......@@ -5,36 +5,37 @@
.ent test
# load hash at 0x30001000
# point evaluation precompile input - 01e798154708fe7789429634053cbf9f99b619f9f084048927333fce637f549b564c0a11a0f704f4fc3e8acfe0f8245f0ad1347b378fbf96e206da11a5d3630624d25032e67a7e6a4910df5834b8fe70e6bcfeeac0352434196bdf4b2485d5a18f59a8d2a1a625a17f3fea0fe5eb8c896db3764f3185481bc22f91b4aaffcca25f26936857bc3a7c2539ea8ec3a952b7873033e038326e87ed3e1276fd140253fa08e9fc25fb2d9a98527fc22a2c9612fbeafdad446cbc7bcdbdcd780af2c16a
# 0x0a44472c cb798bc5 954fc466 e6ee2c31 e1ca8a87 d000966c 629d679a 4a29921f = keccak(address(0xa) ++ precompile_input)
# 0x0644472c cb798bc5 954fc466 e6ee2c31 e1ca8a87 d000966c 629d679a 4a29921f = keccak(address(0xa) ++ precompile_input).key (precompile)
# requiredGas is 50_000
# point evaluation precompile input (requiredGas ++ precompileInput) - 000000000000c35001e798154708fe7789429634053cbf9f99b619f9f084048927333fce637f549b564c0a11a0f704f4fc3e8acfe0f8245f0ad1347b378fbf96e206da11a5d3630624d25032e67a7e6a4910df5834b8fe70e6bcfeeac0352434196bdf4b2485d5a18f59a8d2a1a625a17f3fea0fe5eb8c896db3764f3185481bc22f91b4aaffcca25f26936857bc3a7c2539ea8ec3a952b7873033e038326e87ed3e1276fd140253fa08e9fc25fb2d9a98527fc22a2c9612fbeafdad446cbc7bcdbdcd780af2c16a
# 0x3efd5c3c 1c555298 0c63aee5 4570c276 cbff7532 796b4d75 3132d51a 6bedf0c6 = keccak(address(0xa) ++ required_gas ++ precompile_input)
# 0x06fd5c3c 1c555298 0c63aee5 4570c276 cbff7532 796b4d75 3132d51a 6bedf0c6 = keccak(address(0xa) ++ required_gas ++ precompile_input).key (precompile)
test:
lui $s0, 0x3000
ori $s0, 0x1000
lui $t0, 0x0644
ori $t0, 0x472c
lui $t0, 0x06fd
ori $t0, 0x5c3c
sw $t0, 0($s0)
lui $t0, 0xcb79
ori $t0, 0x8bc5
lui $t0, 0x1c55
ori $t0, 0x5298
sw $t0, 4($s0)
lui $t0, 0x954f
ori $t0, 0xc466
lui $t0, 0x0c63
ori $t0, 0xaee5
sw $t0, 8($s0)
lui $t0, 0xe6ee
ori $t0, 0x2c31
lui $t0, 0x4570
ori $t0, 0xc276
sw $t0, 0xc($s0)
lui $t0, 0xe1ca
ori $t0, 0x8a87
lui $t0, 0xcbff
ori $t0, 0x7532
sw $t0, 0x10($s0)
lui $t0, 0xd000
ori $t0, 0x966c
lui $t0, 0x796b
ori $t0, 0x4d75
sw $t0, 0x14($s0)
lui $t0, 0x629d
ori $t0, 0x679a
lui $t0, 0x3132
ori $t0, 0xd51a
sw $t0, 0x18($s0)
lui $t0, 0x4a29
ori $t0, 0x921f
lui $t0, 0x6bed
ori $t0, 0xf0c6
sw $t0, 0x1c($s0)
# preimage request - write(fdPreimageWrite, preimageData, 32)
......
###############################################################################
# Description:
# Tests that the 'srav' instruction properly only utilizes the lower 5 bits
# of the rs register rather than using the entire 32 bits.
#
###############################################################################
.section .test, "x"
.balign 4
.set noreorder
.global test
.ent test
test:
lui $s0, 0xbfff # Load the base address 0xbffffff0
ori $s0, 0xfff0
ori $s1, $0, 1 # Prepare the 'done' status
#### Test code start ####
lui $t0, 0xdeaf # A = 0xdeafbeef
ori $t0, 0xbeef
ori $t1, $0, 0x2c
srav $t2, $t0, $t1 # B = 0xdeafbeef >> (0x2c & 0x1f) = 0xdeadbeef >> 12 = 0xfffdeafb
lui $t3, 0xfffd
ori $t3, 0xeafb
subu $t4, $t2, $t3
sltiu $v0, $t4, 1
#### Test code end ####
sw $v0, 8($s0) # Set the test result
sw $s1, 4($s0) # Set 'done'
$done:
jr $ra
nop
.end test
package testutil
import (
"encoding/binary"
"errors"
"fmt"
"math"
......@@ -121,11 +122,13 @@ func EncodePreimageOracleInput(t *testing.T, wit *mipsevm.StepWitness, localCont
}
preimage := localOracle.GetPreimage(preimage.Keccak256Key(wit.PreimageKey).PreimageKey())
precompile := common.BytesToAddress(preimage[:20])
callInput := preimage[20:]
requiredGas := binary.BigEndian.Uint64(preimage[20:28])
callInput := preimage[28:]
input, err := oracle.ABI.Pack(
"loadPrecompilePreimagePart",
new(big.Int).SetUint64(uint64(wit.PreimageOffset)),
precompile,
requiredGas,
callInput,
)
require.NoError(t, err)
......
......@@ -42,11 +42,13 @@ func StaticOracle(t *testing.T, preimageData []byte) *TestOracle {
}
}
func StaticPrecompileOracle(t *testing.T, precompile common.Address, input []byte, result []byte) *TestOracle {
func StaticPrecompileOracle(t *testing.T, precompile common.Address, requiredGas uint64, input []byte, result []byte) *TestOracle {
return &TestOracle{
hint: func(v []byte) {},
getPreimage: func(k [32]byte) []byte {
keyData := append(precompile.Bytes(), input...)
requiredGasB := binary.BigEndian.AppendUint64(nil, requiredGas)
keyData := append(precompile.Bytes(), requiredGasB...)
keyData = append(keyData, input...)
switch k[0] {
case byte(preimage.Keccak256KeyType):
if k != preimage.Keccak256Key(crypto.Keccak256Hash(keyData)).PreimageKey() {
......@@ -135,7 +137,8 @@ func SelectOracleFixture(t *testing.T, programName string) mipsevm.PreimageOracl
precompile := common.BytesToAddress([]byte{0xa})
input := common.FromHex("01e798154708fe7789429634053cbf9f99b619f9f084048927333fce637f549b564c0a11a0f704f4fc3e8acfe0f8245f0ad1347b378fbf96e206da11a5d3630624d25032e67a7e6a4910df5834b8fe70e6bcfeeac0352434196bdf4b2485d5a18f59a8d2a1a625a17f3fea0fe5eb8c896db3764f3185481bc22f91b4aaffcca25f26936857bc3a7c2539ea8ec3a952b7873033e038326e87ed3e1276fd140253fa08e9fc25fb2d9a98527fc22a2c9612fbeafdad446cbc7bcdbdcd780af2c16a")
blobPrecompileReturnValue := common.FromHex("000000000000000000000000000000000000000000000000000000000000100073eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001")
return StaticPrecompileOracle(t, precompile, input, append([]byte{0x1}, blobPrecompileReturnValue...))
requiredGas := uint64(50_000)
return StaticPrecompileOracle(t, precompile, requiredGas, input, append([]byte{0x1}, blobPrecompileReturnValue...))
} else if strings.HasPrefix(programName, "oracle") {
return StaticOracle(t, []byte("hello world"))
} else {
......
package testutil
import "math/rand"
func RandomRegisters(seed int64) [32]uint32 {
r := rand.New(rand.NewSource(seed))
var registers [32]uint32
for i := 0; i < 32; i++ {
registers[i] = r.Uint32()
}
return registers
}
......@@ -5,6 +5,7 @@ import (
"io"
"os"
"path"
"strings"
"testing"
"github.com/ethereum/go-ethereum/log"
......@@ -32,6 +33,7 @@ func RunVMTests_OpenMips[T mipsevm.FPVMState](t *testing.T, stateFactory StateFa
oracle := SelectOracleFixture(t, f.Name())
// Short-circuit early for exit_group.bin
exitGroup := f.Name() == "exit_group.bin"
expectPanic := strings.HasSuffix(f.Name(), "panic.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
......@@ -51,6 +53,17 @@ func RunVMTests_OpenMips[T mipsevm.FPVMState](t *testing.T, stateFactory StateFa
us := vmFactory(state, oracle, os.Stdout, os.Stderr, CreateLogger())
// Catch panics and check if they are expected
defer func() {
if r := recover(); r != nil {
if expectPanic {
// Success
} else {
t.Errorf("unexpected panic: %v", r)
}
}
}()
for i := 0; i < 1000; i++ {
if us.GetState().GetPC() == EndAddr {
break
......@@ -66,6 +79,8 @@ func RunVMTests_OpenMips[T mipsevm.FPVMState](t *testing.T, stateFactory StateFa
require.NotEqual(t, uint32(EndAddr), us.GetState().GetPC(), "must not reach end")
require.True(t, us.GetState().GetExited(), "must set exited state")
require.Equal(t, uint8(1), us.GetState().GetExitCode(), "must exit with 1")
} else if expectPanic {
require.NotEqual(t, uint32(EndAddr), us.GetState().GetPC(), "must not reach end")
} else {
require.Equal(t, uint32(EndAddr), us.GetState().GetPC(), "must reach end")
done, result := state.GetMemory().GetMemory(BaseAddrEnd+4), state.GetMemory().GetMemory(BaseAddrEnd+8)
......
......@@ -7,7 +7,6 @@ import (
"fmt"
"math"
"math/big"
"strings"
"time"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts/metrics"
......@@ -26,7 +25,6 @@ import (
var maxChildChecks = big.NewInt(512)
var (
methodVersion = "version"
methodMaxClockDuration = "maxClockDuration"
methodMaxGameDepth = "maxGameDepth"
methodAbsolutePrestate = "absolutePrestate"
......@@ -84,14 +82,8 @@ type outputRootProof struct {
func NewFaultDisputeGameContract(ctx context.Context, metrics metrics.ContractMetricer, addr common.Address, caller *batching.MultiCaller) (FaultDisputeGameContract, error) {
contractAbi := snapshots.LoadFaultDisputeGameABI()
result, err := caller.SingleCall(ctx, rpcblock.Latest, batching.NewContractCall(contractAbi, addr, methodVersion))
if err != nil {
return nil, fmt.Errorf("failed to retrieve version of dispute game %v: %w", addr, err)
}
version := result.GetString(0)
if strings.HasPrefix(version, "0.8.") {
// Detected an older version of contracts, use a compatibility shim.
var builder VersionedBuilder[FaultDisputeGameContract]
builder.AddVersion(0, 8, func() (FaultDisputeGameContract, error) {
legacyAbi := mustParseAbi(faultDisputeGameAbi020)
return &FaultDisputeGameContract080{
FaultDisputeGameContractLatest: FaultDisputeGameContractLatest{
......@@ -100,8 +92,8 @@ func NewFaultDisputeGameContract(ctx context.Context, metrics metrics.ContractMe
contract: batching.NewBoundContract(legacyAbi, addr),
},
}, nil
} else if strings.HasPrefix(version, "0.18.") || strings.HasPrefix(version, "1.0.") {
// Detected an older version of contracts, use a compatibility shim.
})
builder.AddVersion(0, 18, func() (FaultDisputeGameContract, error) {
legacyAbi := mustParseAbi(faultDisputeGameAbi0180)
return &FaultDisputeGameContract0180{
FaultDisputeGameContractLatest: FaultDisputeGameContractLatest{
......@@ -110,8 +102,18 @@ func NewFaultDisputeGameContract(ctx context.Context, metrics metrics.ContractMe
contract: batching.NewBoundContract(legacyAbi, addr),
},
}, nil
} else if strings.HasPrefix(version, "1.1.") {
// Detected an older version of contracts, use a compatibility shim.
})
builder.AddVersion(1, 0, func() (FaultDisputeGameContract, error) {
legacyAbi := mustParseAbi(faultDisputeGameAbi0180)
return &FaultDisputeGameContract0180{
FaultDisputeGameContractLatest: FaultDisputeGameContractLatest{
metrics: metrics,
multiCaller: caller,
contract: batching.NewBoundContract(legacyAbi, addr),
},
}, nil
})
builder.AddVersion(1, 1, func() (FaultDisputeGameContract, error) {
legacyAbi := mustParseAbi(faultDisputeGameAbi111)
return &FaultDisputeGameContract111{
FaultDisputeGameContractLatest: FaultDisputeGameContractLatest{
......@@ -120,13 +122,14 @@ func NewFaultDisputeGameContract(ctx context.Context, metrics metrics.ContractMe
contract: batching.NewBoundContract(legacyAbi, addr),
},
}, nil
} else {
})
return builder.Build(ctx, caller, contractAbi, addr, func() (FaultDisputeGameContract, error) {
return &FaultDisputeGameContractLatest{
metrics: metrics,
multiCaller: caller,
contract: batching.NewBoundContract(contractAbi, addr),
}, nil
}
})
}
func mustParseAbi(json []byte) *abi.ABI {
......@@ -364,7 +367,7 @@ func (f *FaultDisputeGameContractLatest) getDelayedWETH(ctx context.Context, blo
return NewDelayedWETHContract(f.metrics, result.GetAddress(0), f.multiCaller), nil
}
func (f *FaultDisputeGameContractLatest) GetOracle(ctx context.Context) (*PreimageOracleContract, error) {
func (f *FaultDisputeGameContractLatest) GetOracle(ctx context.Context) (PreimageOracleContract, error) {
defer f.metrics.StartContractRequest("GetOracle")()
vm, err := f.Vm(ctx)
if err != nil {
......@@ -616,7 +619,7 @@ type FaultDisputeGameContract interface {
GetRequiredBond(ctx context.Context, position types.Position) (*big.Int, error)
UpdateOracleTx(ctx context.Context, claimIdx uint64, data *types.PreimageOracleData) (txmgr.TxCandidate, error)
GetWithdrawals(ctx context.Context, block rpcblock.Block, recipients ...common.Address) ([]*WithdrawalRequest, error)
GetOracle(ctx context.Context) (*PreimageOracleContract, error)
GetOracle(ctx context.Context) (PreimageOracleContract, error)
GetMaxClockDuration(ctx context.Context) (time.Duration, error)
GetMaxGameDepth(ctx context.Context) (types.Depth, error)
GetAbsolutePrestateHash(ctx context.Context) (common.Hash, error)
......
......@@ -797,6 +797,7 @@ func setupFaultDisputeGameTest(t *testing.T, version contractVersion) (*batching
caller := batching.NewMultiCaller(stubRpc, batching.DefaultBatchSize)
stubRpc.SetResponse(fdgAddr, methodVersion, rpcblock.Latest, nil, []interface{}{version.version})
stubRpc.SetResponse(oracleAddr, methodVersion, rpcblock.Latest, nil, []interface{}{oracleLatest})
game, err := NewFaultDisputeGameContract(context.Background(), contractMetrics.NoopContractMetrics, fdgAddr, caller)
require.NoError(t, err)
return stubRpc, game
......
package contracts
import (
_ "embed"
"math/big"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
preimage "github.com/ethereum-optimism/optimism/op-preimage"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum/go-ethereum/common"
)
//go:embed abis/PreimageOracle-1.0.0.json
var preimageOracleAbi100 []byte
type PreimageOracleContract100 struct {
PreimageOracleContractLatest
}
func (c *PreimageOracleContract100) AddGlobalDataTx(data *types.PreimageOracleData) (txmgr.TxCandidate, error) {
if len(data.OracleKey) == 0 || preimage.KeyType(data.OracleKey[0]) != preimage.PrecompileKeyType {
return c.PreimageOracleContractLatest.AddGlobalDataTx(data)
}
inputs := data.GetPreimageWithoutSize()
call := c.contract.Call(methodLoadPrecompilePreimagePart,
new(big.Int).SetUint64(uint64(data.OracleOffset)),
common.BytesToAddress(inputs[0:20]),
inputs[20:])
return call.ToTxCandidate()
}
package contracts
import (
"context"
"fmt"
"strings"
"github.com/ethereum-optimism/optimism/op-service/sources/batching"
"github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
)
var (
methodVersion = "version"
)
type version[C any] struct {
prefixes string
factory func() (C, error)
}
type VersionedBuilder[C any] struct {
versions []version[C]
}
func (v *VersionedBuilder[C]) AddVersion(major int, minor int, factory func() (C, error)) {
v.versions = append(v.versions, version[C]{fmt.Sprintf("%d.%d.", major, minor), factory})
}
func (v *VersionedBuilder[C]) Build(ctx context.Context, caller *batching.MultiCaller, contractAbi *abi.ABI, addr common.Address, defaultVersion func() (C, error)) (C, error) {
var nilC C
result, err := caller.SingleCall(ctx, rpcblock.Latest, batching.NewContractCall(contractAbi, addr, methodVersion))
if err != nil {
return nilC, fmt.Errorf("failed to retrieve version of dispute game %v: %w", addr, err)
}
contractVersion := result.GetString(0)
for _, version := range v.versions {
if strings.HasPrefix(contractVersion, version.prefixes) {
return version.factory()
}
}
return defaultVersion()
}
package contracts
import (
"context"
"testing"
"github.com/ethereum-optimism/optimism/op-service/sources/batching"
"github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock"
batchingTest "github.com/ethereum-optimism/optimism/op-service/sources/batching/test"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
)
var versionABI = `[{
"inputs": [],
"name": "version",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
}]`
func TestVersionedBuilder(t *testing.T) {
var builder VersionedBuilder[string]
builder.AddVersion(1, 1, func() (string, error) { return "v1.1", nil })
builder.AddVersion(1, 2, func() (string, error) { return "v1.2", nil })
require.Equal(t, "v1.1", buildWithVersion(t, builder, "1.1.0"))
require.Equal(t, "v1.1", buildWithVersion(t, builder, "1.1.1"))
require.Equal(t, "v1.1", buildWithVersion(t, builder, "1.1.2"))
require.Equal(t, "default", buildWithVersion(t, builder, "1.10.0"))
}
func buildWithVersion(t *testing.T, builder VersionedBuilder[string], version string) string {
addr := common.Address{0xaa}
contractABI := mustParseAbi(([]byte)(versionABI))
stubRpc := batchingTest.NewAbiBasedRpc(t, addr, contractABI)
stubRpc.SetResponse(addr, methodVersion, rpcblock.Latest, nil, []interface{}{version})
actual, err := builder.Build(context.Background(), batching.NewMultiCaller(stubRpc, batching.DefaultBatchSize), contractABI, addr, func() (string, error) {
return "default", nil
})
require.NoError(t, err)
return actual
}
......@@ -33,10 +33,10 @@ func (c *VMContract) Addr() common.Address {
return c.contract.Addr()
}
func (c *VMContract) Oracle(ctx context.Context) (*PreimageOracleContract, error) {
func (c *VMContract) Oracle(ctx context.Context) (PreimageOracleContract, error) {
results, err := c.multiCaller.SingleCall(ctx, rpcblock.Latest, c.contract.Call(methodOracle))
if err != nil {
return nil, fmt.Errorf("failed to load oracle address: %w", err)
}
return NewPreimageOracleContract(results.GetAddress(0), c.multiCaller), nil
return NewPreimageOracleContract(ctx, results.GetAddress(0), c.multiCaller)
}
......@@ -21,6 +21,8 @@ func TestVMContract_Oracle(t *testing.T) {
vmContract := NewVMContract(vmAddr, batching.NewMultiCaller(stubRpc, batching.DefaultBatchSize))
stubRpc.SetResponse(vmAddr, methodOracle, rpcblock.Latest, nil, []interface{}{oracleAddr})
stubRpc.AddContract(oracleAddr, snapshots.LoadPreimageOracleABI())
stubRpc.SetResponse(oracleAddr, methodVersion, rpcblock.Latest, nil, []interface{}{oracleLatest})
oracleContract, err := vmContract.Oracle(context.Background())
require.NoError(t, err)
......
......@@ -60,7 +60,7 @@ type GameContract interface {
GetStatus(ctx context.Context) (gameTypes.GameStatus, error)
GetMaxGameDepth(ctx context.Context) (types.Depth, error)
GetMaxClockDuration(ctx context.Context) (time.Duration, error)
GetOracle(ctx context.Context) (*contracts.PreimageOracleContract, error)
GetOracle(ctx context.Context) (contracts.PreimageOracleContract, error)
GetL1Head(ctx context.Context) (common.Hash, error)
}
......
......@@ -2,6 +2,7 @@ package types
import (
"context"
"encoding/binary"
"errors"
"fmt"
"math"
......@@ -149,8 +150,12 @@ func (p *PreimageOracleData) GetPrecompileAddress() common.Address {
return common.BytesToAddress(p.oracleData[8:28])
}
func (p *PreimageOracleData) GetPrecompileRequiredGas() uint64 {
return binary.BigEndian.Uint64(p.oracleData[28:36])
}
func (p *PreimageOracleData) GetPrecompileInput() []byte {
return p.oracleData[28:]
return p.oracleData[36:]
}
// NewPreimageOracleData creates a new [PreimageOracleData] instance.
......
This diff is collapsed.
......@@ -47,23 +47,49 @@ func (g *OutputCannonGameHelper) StartChallenger(ctx context.Context, name strin
return c
}
func (g *OutputCannonGameHelper) CreateHonestActor(ctx context.Context, l2Node string, options ...challenger.Option) *OutputHonestHelper {
opts := g.defaultChallengerOptions()
opts = append(opts, options...)
cfg := challenger.NewChallengerConfig(g.T, g.System, l2Node, opts...)
type honestActorConfig struct {
prestateBlock uint64
poststateBlock uint64
challengerOpts []challenger.Option
}
type HonestActorOpt func(cfg *honestActorConfig)
func WithClaimedL2BlockNumber(num uint64) HonestActorOpt {
return func(cfg *honestActorConfig) {
cfg.poststateBlock = num
}
}
func WithPrivKey(privKey *ecdsa.PrivateKey) HonestActorOpt {
return func(cfg *honestActorConfig) {
cfg.challengerOpts = append(cfg.challengerOpts, challenger.WithPrivKey(privKey))
}
}
func (g *OutputCannonGameHelper) CreateHonestActor(ctx context.Context, l2Node string, options ...HonestActorOpt) *OutputHonestHelper {
logger := testlog.Logger(g.T, log.LevelInfo).New("role", "HonestHelper", "game", g.Addr)
l2Client := g.System.NodeClient(l2Node)
prestateBlock, poststateBlock, err := g.Game.GetBlockRange(ctx)
realPrestateBlock, realPostStateBlock, err := g.Game.GetBlockRange(ctx)
g.Require.NoError(err, "Failed to load block range")
dir := filepath.Join(cfg.Datadir, "honest")
splitDepth := g.SplitDepth(ctx)
rollupClient := g.System.RollupClient(l2Node)
prestateProvider := outputs.NewPrestateProvider(rollupClient, prestateBlock)
actorCfg := &honestActorConfig{
prestateBlock: realPrestateBlock,
poststateBlock: realPostStateBlock,
challengerOpts: g.defaultChallengerOptions(),
}
for _, option := range options {
option(actorCfg)
}
cfg := challenger.NewChallengerConfig(g.T, g.System, l2Node, actorCfg.challengerOpts...)
dir := filepath.Join(cfg.Datadir, "honest")
prestateProvider := outputs.NewPrestateProvider(rollupClient, actorCfg.prestateBlock)
l1Head := g.GetL1Head(ctx)
accessor, err := outputs.NewOutputCannonTraceAccessor(
logger, metrics.NoopMetrics, cfg.Cannon, vm.NewOpProgramServerExecutor(), l2Client, prestateProvider, cfg.CannonAbsolutePreState, rollupClient, dir, l1Head, splitDepth, prestateBlock, poststateBlock)
logger, metrics.NoopMetrics, cfg.Cannon, vm.NewOpProgramServerExecutor(), l2Client, prestateProvider, cfg.CannonAbsolutePreState, rollupClient, dir, l1Head, splitDepth, actorCfg.prestateBlock, actorCfg.poststateBlock)
g.Require.NoError(err, "Failed to create output cannon trace accessor")
return NewOutputHonestHelper(g.T, g.Require, &g.OutputGameHelper, g.Game, accessor)
}
......@@ -128,7 +154,7 @@ func (g *OutputCannonGameHelper) ChallengeToPreimageLoad(ctx context.Context, ou
if preloadPreimage {
_, _, preimageData, err := provider.GetStepData(ctx, types.NewPosition(execDepth, big.NewInt(int64(targetTraceIndex))))
g.Require.NoError(err)
g.UploadPreimage(ctx, preimageData, challengerKey)
g.UploadPreimage(ctx, preimageData)
g.WaitForPreimageInOracle(ctx, preimageData)
}
......
......@@ -5,6 +5,7 @@ import (
"crypto/ecdsa"
"fmt"
"math/big"
"strings"
"testing"
"time"
......@@ -14,10 +15,8 @@ import (
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
keccakTypes "github.com/ethereum-optimism/optimism/op-challenger/game/keccak/types"
gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum-optimism/optimism/op-e2e/bindings"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/transactions"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait"
preimage "github.com/ethereum-optimism/optimism/op-preimage"
"github.com/ethereum-optimism/optimism/op-service/errutil"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock"
......@@ -586,7 +585,11 @@ func (g *OutputGameHelper) StepFails(ctx context.Context, claimIdx int64, isAtta
_, _, err = transactions.SendTx(ctx, g.Client, candidate, g.PrivKey, transactions.WithReceiptFail())
err = errutil.TryAddRevertReason(err)
g.Require.Error(err, "Transaction should fail")
g.Require.Contains(err.Error(), "0xfb4e40dd", "Revert reason should be abi encoded ValidStep()")
validStepErr := "0xfb4e40dd"
invalidPrestateErr := "0x696550ff"
if !strings.Contains(err.Error(), validStepErr) && !strings.Contains(err.Error(), invalidPrestateErr) {
g.Require.Failf("Revert reason should be abi encoded ValidStep() or InvalidPrestate() but was: %v", err.Error())
}
}
// ResolveClaim resolves a single subgame
......@@ -649,28 +652,14 @@ func (g *OutputGameHelper) WaitForPreimageInOracle(ctx context.Context, data *ty
g.Require.NoErrorf(err, "Did not find preimage (%v) in oracle", common.Bytes2Hex(data.OracleKey))
}
func (g *OutputGameHelper) UploadPreimage(ctx context.Context, data *types.PreimageOracleData, privateKey *ecdsa.PrivateKey) {
func (g *OutputGameHelper) UploadPreimage(ctx context.Context, data *types.PreimageOracleData) {
oracle := g.oracle(ctx)
boundOracle, err := bindings.NewPreimageOracle(oracle.Addr(), g.Client)
g.Require.NoError(err)
var tx *gethtypes.Transaction
switch data.OracleKey[0] {
case byte(preimage.PrecompileKeyType):
tx, err = boundOracle.LoadPrecompilePreimagePart(
g.Opts,
new(big.Int).SetUint64(uint64(data.OracleOffset)),
data.GetPrecompileAddress(),
data.GetPrecompileInput(),
)
default:
tx, err = boundOracle.LoadKeccak256PreimagePart(g.Opts, new(big.Int).SetUint64(uint64(data.OracleOffset)), data.GetPreimageWithoutSize())
}
g.Require.NoError(err, "Failed to load preimage part")
_, err = wait.ForReceiptOK(ctx, g.Client, tx.Hash())
g.Require.NoError(err)
tx, err := oracle.AddGlobalDataTx(data)
g.Require.NoError(err, "Failed to create preimage upload tx")
transactions.RequireSendTx(g.T, ctx, g.Client, tx, g.PrivKey)
}
func (g *OutputGameHelper) oracle(ctx context.Context) *contracts.PreimageOracleContract {
func (g *OutputGameHelper) oracle(ctx context.Context) contracts.PreimageOracleContract {
oracle, err := g.Game.GetOracle(ctx)
g.Require.NoError(err, "Failed to create oracle contract")
return oracle
......
......@@ -7,6 +7,7 @@ import (
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/transactions"
"github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock"
"github.com/stretchr/testify/require"
)
......@@ -99,8 +100,13 @@ func (h *OutputHonestHelper) StepFails(ctx context.Context, claimIdx int64, isAt
// If we're defending, then the step will be from the trace to the next one
pos = pos.MoveRight()
}
prestate, proofData, _, err := h.correctTrace.GetStepData(ctx, game, claim, pos)
prestate, proofData, preimage, err := h.correctTrace.GetStepData(ctx, game, claim, pos)
h.require.NoError(err, "Get step data")
if preimage != nil {
tx, err := h.game.Game.UpdateOracleTx(ctx, uint64(claimIdx), preimage)
h.require.NoError(err)
transactions.RequireSendTx(h.t, ctx, h.game.Client, tx, h.game.PrivKey)
}
h.game.StepFails(ctx, claimIdx, isAttack, prestate, proofData)
}
......
......@@ -33,11 +33,11 @@ type Helper struct {
require *require.Assertions
client *ethclient.Client
privKey *ecdsa.PrivateKey
oracle *contracts.PreimageOracleContract
oracle contracts.PreimageOracleContract
uuidProvider atomic.Int64
}
func NewHelper(t *testing.T, privKey *ecdsa.PrivateKey, client *ethclient.Client, oracle *contracts.PreimageOracleContract) *Helper {
func NewHelper(t *testing.T, privKey *ecdsa.PrivateKey, client *ethclient.Client, oracle contracts.PreimageOracleContract) *Helper {
return &Helper{
t: t,
require: require.New(t),
......
......@@ -194,7 +194,7 @@ func TestOutputCannonDefendStep(t *testing.T) {
game.StartChallenger(ctx, "Challenger", challenger.WithPrivKey(sys.Cfg.Secrets.Alice))
correctTrace := game.CreateHonestActor(ctx, "sequencer", challenger.WithPrivKey(sys.Cfg.Secrets.Mallory))
correctTrace := game.CreateHonestActor(ctx, "sequencer", disputegame.WithPrivKey(sys.Cfg.Secrets.Mallory))
maxDepth := game.MaxDepth(ctx)
game.DefendClaim(ctx, outputRootClaim, func(claim *disputegame.ClaimHelper) *disputegame.ClaimHelper {
......@@ -410,7 +410,7 @@ func TestOutputCannonProposedOutputRootValid(t *testing.T) {
disputeGameFactory := disputegame.NewFactoryHelper(t, ctx, sys)
game := disputeGameFactory.StartOutputCannonGameWithCorrectRoot(ctx, "sequencer", 1)
correctTrace := game.CreateHonestActor(ctx, "sequencer", challenger.WithPrivKey(sys.Cfg.Secrets.Mallory))
correctTrace := game.CreateHonestActor(ctx, "sequencer", disputegame.WithPrivKey(sys.Cfg.Secrets.Mallory))
game.StartChallenger(ctx, "Challenger", challenger.WithPrivKey(sys.Cfg.Secrets.Alice))
......@@ -445,7 +445,7 @@ func TestOutputCannonPoisonedPostState(t *testing.T) {
disputeGameFactory := disputegame.NewFactoryHelper(t, ctx, sys)
// Root claim is dishonest
game := disputeGameFactory.StartOutputCannonGame(ctx, "sequencer", 1, common.Hash{0xaa})
correctTrace := game.CreateHonestActor(ctx, "sequencer", challenger.WithPrivKey(sys.Cfg.Secrets.Alice))
correctTrace := game.CreateHonestActor(ctx, "sequencer", disputegame.WithPrivKey(sys.Cfg.Secrets.Alice))
// Honest first attack at "honest" level
claim := correctTrace.AttackClaim(ctx, game.RootClaim(ctx))
......@@ -509,7 +509,7 @@ func TestDisputeOutputRootBeyondProposedBlock_ValidOutputRoot(t *testing.T) {
disputeGameFactory := disputegame.NewFactoryHelper(t, ctx, sys)
// Root claim is dishonest
game := disputeGameFactory.StartOutputCannonGameWithCorrectRoot(ctx, "sequencer", 1)
correctTrace := game.CreateHonestActor(ctx, "sequencer", challenger.WithPrivKey(sys.Cfg.Secrets.Alice))
correctTrace := game.CreateHonestActor(ctx, "sequencer", disputegame.WithPrivKey(sys.Cfg.Secrets.Alice))
// Start the honest challenger
game.StartChallenger(ctx, "Honest", challenger.WithPrivKey(sys.Cfg.Secrets.Bob))
......@@ -559,7 +559,7 @@ func TestDisputeOutputRootBeyondProposedBlock_InvalidOutputRoot(t *testing.T) {
disputeGameFactory := disputegame.NewFactoryHelper(t, ctx, sys)
// Root claim is dishonest
game := disputeGameFactory.StartOutputCannonGame(ctx, "sequencer", 1, common.Hash{0xaa})
correctTrace := game.CreateHonestActor(ctx, "sequencer", challenger.WithPrivKey(sys.Cfg.Secrets.Alice))
correctTrace := game.CreateHonestActor(ctx, "sequencer", disputegame.WithPrivKey(sys.Cfg.Secrets.Alice))
// Start the honest challenger
game.StartChallenger(ctx, "Honest", challenger.WithPrivKey(sys.Cfg.Secrets.Bob))
......@@ -610,7 +610,7 @@ func TestDisputeOutputRoot_ChangeClaimedOutputRoot(t *testing.T) {
disputeGameFactory := disputegame.NewFactoryHelper(t, ctx, sys)
// Root claim is dishonest
game := disputeGameFactory.StartOutputCannonGame(ctx, "sequencer", 1, common.Hash{0xaa})
correctTrace := game.CreateHonestActor(ctx, "sequencer", challenger.WithPrivKey(sys.Cfg.Secrets.Alice))
correctTrace := game.CreateHonestActor(ctx, "sequencer", disputegame.WithPrivKey(sys.Cfg.Secrets.Alice))
// Start the honest challenger
game.StartChallenger(ctx, "Honest", challenger.WithPrivKey(sys.Cfg.Secrets.Bob))
......@@ -700,7 +700,7 @@ func TestInvalidateUnsafeProposal(t *testing.T) {
// Root claim is _dishonest_ because the required data is not available on L1
game := disputeGameFactory.StartOutputCannonGameWithCorrectRoot(ctx, "sequencer", blockNum, disputegame.WithUnsafeProposal())
correctTrace := game.CreateHonestActor(ctx, "sequencer", challenger.WithPrivKey(sys.Cfg.Secrets.Alice))
correctTrace := game.CreateHonestActor(ctx, "sequencer", disputegame.WithPrivKey(sys.Cfg.Secrets.Alice))
// Start the honest challenger
game.StartChallenger(ctx, "Challenger", challenger.WithPrivKey(sys.Cfg.Secrets.Bob))
......@@ -762,7 +762,7 @@ func TestInvalidateProposalForFutureBlock(t *testing.T) {
// Root claim is _dishonest_ because the required data is not available on L1
game := disputeGameFactory.StartOutputCannonGame(ctx, "sequencer", farFutureBlockNum, common.Hash{0xaa}, disputegame.WithFutureProposal())
correctTrace := game.CreateHonestActor(ctx, "sequencer", challenger.WithPrivKey(sys.Cfg.Secrets.Alice))
correctTrace := game.CreateHonestActor(ctx, "sequencer", disputegame.WithPrivKey(sys.Cfg.Secrets.Alice))
// Start the honest challenger
game.StartChallenger(ctx, "Honest", challenger.WithPrivKey(sys.Cfg.Secrets.Bob))
......@@ -815,3 +815,98 @@ func TestInvalidateCorrectProposalFutureBlock(t *testing.T) {
game.WaitForGameStatus(ctx, gameTypes.GameStatusChallengerWon)
game.LogGameData(ctx)
}
func TestOutputCannonHonestSafeTraceExtension_ValidRoot(t *testing.T) {
op_e2e.InitParallel(t, op_e2e.UsesCannon)
ctx := context.Background()
sys, l1Client := StartFaultDisputeSystem(t)
t.Cleanup(sys.Close)
// Wait for there to be there are safe L2 blocks past the claimed safe head that have data available on L1 within
// the commitment stored in the dispute game.
safeHeadNum := uint64(3)
require.NoError(t, wait.ForSafeBlock(ctx, sys.RollupClient("sequencer"), safeHeadNum))
// Create a dispute game with an honest claim
disputeGameFactory := disputegame.NewFactoryHelper(t, ctx, sys)
game := disputeGameFactory.StartOutputCannonGameWithCorrectRoot(ctx, "sequencer", safeHeadNum-1)
require.NotNil(t, game)
// Create a correct trace actor with an honest trace extending to L2 block #4
correctTrace := game.CreateHonestActor(ctx, "sequencer", disputegame.WithPrivKey(sys.Cfg.Secrets.Mallory))
// Create a correct trace actor with an honest trace extending to L2 block #5
// Notably, L2 block #5 is a valid block within the safe chain, and the data required to reproduce it
// will be committed to within the L1 head of the dispute game.
correctTracePlus1 := game.CreateHonestActor(ctx, "sequencer",
disputegame.WithPrivKey(sys.Cfg.Secrets.Mallory),
disputegame.WithClaimedL2BlockNumber(safeHeadNum))
// Start the honest challenger. They will defend the root claim.
game.StartChallenger(ctx, "Challenger", challenger.WithPrivKey(sys.Cfg.Secrets.Alice))
claim := game.RootClaim(ctx)
game.ChallengeClaim(ctx, claim, func(parent *disputegame.ClaimHelper) *disputegame.ClaimHelper {
// Have to disagree with the root claim - we're trying to invalidate a valid output root
if parent.IsRootClaim() {
return parent.Attack(ctx, common.Hash{0xdd})
}
return correctTracePlus1.CounterClaim(ctx, parent)
}, func(parentClaimIdx int64) {
correctTrace.StepFails(ctx, parentClaimIdx, true)
correctTrace.StepFails(ctx, parentClaimIdx, false)
correctTracePlus1.StepFails(ctx, parentClaimIdx, true)
correctTracePlus1.StepFails(ctx, parentClaimIdx, false)
})
game.LogGameData(ctx)
// Time travel past when the game will be resolvable.
sys.TimeTravelClock.AdvanceTime(game.MaxClockDuration(ctx))
require.NoError(t, wait.ForNextBlock(ctx, l1Client))
game.WaitForInactivity(ctx, 10, true)
game.LogGameData(ctx)
require.EqualValues(t, gameTypes.GameStatusDefenderWon, game.Status(ctx))
}
func TestOutputCannonHonestSafeTraceExtension_InvalidRoot(t *testing.T) {
op_e2e.InitParallel(t, op_e2e.UsesCannon)
ctx := context.Background()
sys, l1Client := StartFaultDisputeSystem(t)
t.Cleanup(sys.Close)
// Wait for there to be there are safe L2 blocks past the claimed safe head that have data available on L1 within
// the commitment stored in the dispute game.
safeHeadNum := uint64(2)
require.NoError(t, wait.ForSafeBlock(ctx, sys.RollupClient("sequencer"), safeHeadNum))
// Create a dispute game with a dishonest claim @ L2 block #4
disputeGameFactory := disputegame.NewFactoryHelper(t, ctx, sys)
game := disputeGameFactory.StartOutputCannonGame(ctx, "sequencer", safeHeadNum-1, common.Hash{0xCA, 0xFE})
require.NotNil(t, game)
// Create a correct trace actor with an honest trace extending to L2 block #5
// Notably, L2 block #5 is a valid block within the safe chain, and the data required to reproduce it
// will be committed to within the L1 head of the dispute game.
correctTracePlus1 := game.CreateHonestActor(ctx, "sequencer",
disputegame.WithPrivKey(sys.Cfg.Secrets.Mallory),
disputegame.WithClaimedL2BlockNumber(safeHeadNum))
// Start the honest challenger. They will challenge the root claim.
game.StartChallenger(ctx, "Challenger", challenger.WithPrivKey(sys.Cfg.Secrets.Alice))
claim := game.RootClaim(ctx)
game.DefendClaim(ctx, claim, func(parent *disputegame.ClaimHelper) *disputegame.ClaimHelper {
return correctTracePlus1.CounterClaim(ctx, parent)
})
// Time travel past when the game will be resolvable.
sys.TimeTravelClock.AdvanceTime(game.MaxClockDuration(ctx))
require.NoError(t, wait.ForNextBlock(ctx, l1Client))
game.WaitForInactivity(ctx, 10, true)
game.LogGameData(ctx)
require.EqualValues(t, gameTypes.GameStatusChallengerWon, game.Status(ctx))
}
......@@ -24,6 +24,7 @@ import (
"github.com/ethereum-optimism/optimism/op-challenger/metrics"
op_e2e "github.com/ethereum-optimism/optimism/op-e2e"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/challenger"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/disputegame"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait"
"github.com/ethereum-optimism/optimism/op-service/client"
"github.com/ethereum-optimism/optimism/op-service/ioutil"
......@@ -35,14 +36,16 @@ func TestPrecompiles(t *testing.T) {
op_e2e.InitParallel(t, op_e2e.UsesCannon)
// precompile test vectors copied from go-ethereum
tests := []struct {
name string
address common.Address
input []byte
name string
address common.Address
input []byte
accelerated bool
}{
{
name: "ecrecover",
address: common.BytesToAddress([]byte{0x01}),
input: common.FromHex("18c547e4f7b0f325ad1e56f57e26c745b09a3e503d86e00e5255ff7f715d3d1c000000000000000000000000000000000000000000000000000000000000001c73b1693892219d736caba55bdb67216e485557ea6b6af75f37096c9aa6a5a75feeb940b1d03b21e36b0e47e79769f095fe2ab855bd91e3a38756b7d75a9c4549"),
name: "ecrecover",
address: common.BytesToAddress([]byte{0x01}),
input: common.FromHex("18c547e4f7b0f325ad1e56f57e26c745b09a3e503d86e00e5255ff7f715d3d1c000000000000000000000000000000000000000000000000000000000000001c73b1693892219d736caba55bdb67216e485557ea6b6af75f37096c9aa6a5a75feeb940b1d03b21e36b0e47e79769f095fe2ab855bd91e3a38756b7d75a9c4549"),
accelerated: true,
},
{
name: "sha256",
......@@ -55,9 +58,10 @@ func TestPrecompiles(t *testing.T) {
input: common.FromHex("68656c6c6f20776f726c64"),
},
{
name: "bn256Pairing",
address: common.BytesToAddress([]byte{0x08}),
input: common.FromHex("1c76476f4def4bb94541d57ebba1193381ffa7aa76ada664dd31c16024c43f593034dd2920f673e204fee2811c678745fc819b55d3e9d294e45c9b03a76aef41209dd15ebff5d46c4bd888e51a93cf99a7329636c63514396b4a452003a35bf704bf11ca01483bfa8b34b43561848d28905960114c8ac04049af4b6315a416782bb8324af6cfc93537a2ad1a445cfd0ca2a71acd7ac41fadbf933c2a51be344d120a2a4cf30c1bf9845f20c6fe39e07ea2cce61f0c9bb048165fe5e4de877550111e129f1cf1097710d41c4ac70fcdfa5ba2023c6ff1cbeac322de49d1b6df7c2032c61a830e3c17286de9462bf242fca2883585b93870a73853face6a6bf411198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c21800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa"),
name: "bn256Pairing",
address: common.BytesToAddress([]byte{0x08}),
input: common.FromHex("1c76476f4def4bb94541d57ebba1193381ffa7aa76ada664dd31c16024c43f593034dd2920f673e204fee2811c678745fc819b55d3e9d294e45c9b03a76aef41209dd15ebff5d46c4bd888e51a93cf99a7329636c63514396b4a452003a35bf704bf11ca01483bfa8b34b43561848d28905960114c8ac04049af4b6315a416782bb8324af6cfc93537a2ad1a445cfd0ca2a71acd7ac41fadbf933c2a51be344d120a2a4cf30c1bf9845f20c6fe39e07ea2cce61f0c9bb048165fe5e4de877550111e129f1cf1097710d41c4ac70fcdfa5ba2023c6ff1cbeac322de49d1b6df7c2032c61a830e3c17286de9462bf242fca2883585b93870a73853face6a6bf411198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c21800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa"),
accelerated: true,
},
{
name: "blake2F",
......@@ -65,9 +69,10 @@ func TestPrecompiles(t *testing.T) {
input: common.FromHex("0000000048c9bdf267e6096a3ba7ca8485ae67bb2bf894fe72f36e3cf1361d5f3af54fa5d182e6ad7f520e511f6c3e2b8c68059b6bbd41fbabd9831f79217e1319cde05b61626300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000001"),
},
{
name: "kzgPointEvaluation",
address: common.BytesToAddress([]byte{0x0a}),
input: common.FromHex("01e798154708fe7789429634053cbf9f99b619f9f084048927333fce637f549b564c0a11a0f704f4fc3e8acfe0f8245f0ad1347b378fbf96e206da11a5d3630624d25032e67a7e6a4910df5834b8fe70e6bcfeeac0352434196bdf4b2485d5a18f59a8d2a1a625a17f3fea0fe5eb8c896db3764f3185481bc22f91b4aaffcca25f26936857bc3a7c2539ea8ec3a952b7873033e038326e87ed3e1276fd140253fa08e9fc25fb2d9a98527fc22a2c9612fbeafdad446cbc7bcdbdcd780af2c16a"),
name: "kzgPointEvaluation",
address: common.BytesToAddress([]byte{0x0a}),
input: common.FromHex("01e798154708fe7789429634053cbf9f99b619f9f084048927333fce637f549b564c0a11a0f704f4fc3e8acfe0f8245f0ad1347b378fbf96e206da11a5d3630624d25032e67a7e6a4910df5834b8fe70e6bcfeeac0352434196bdf4b2485d5a18f59a8d2a1a625a17f3fea0fe5eb8c896db3764f3185481bc22f91b4aaffcca25f26936857bc3a7c2539ea8ec3a952b7873033e038326e87ed3e1276fd140253fa08e9fc25fb2d9a98527fc22a2c9612fbeafdad446cbc7bcdbdcd780af2c16a"),
accelerated: true,
},
}
for _, test := range tests {
......@@ -134,6 +139,45 @@ func TestPrecompiles(t *testing.T) {
}
runCannon(t, ctx, sys, inputs, "sequencer")
})
t.Run("DisputePrecompile-"+test.name, func(t *testing.T) {
op_e2e.InitParallel(t, op_e2e.UsesCannon)
if !test.accelerated {
t.Skipf("%v is not accelerated so no preimgae to upload", test.name)
}
ctx := context.Background()
sys, _ := StartFaultDisputeSystem(t, WithBlobBatches())
defer sys.Close()
l2Seq := sys.Clients["sequencer"]
aliceKey := sys.Cfg.Secrets.Alice
receipt := op_e2e.SendL2Tx(t, sys.Cfg, l2Seq, aliceKey, func(opts *op_e2e.TxOpts) {
opts.Gas = 1_000_000
opts.ToAddr = &test.address
opts.Nonce = 0
opts.Data = test.input
})
disputeGameFactory := disputegame.NewFactoryHelper(t, ctx, sys)
game := disputeGameFactory.StartOutputCannonGame(ctx, "sequencer", receipt.BlockNumber.Uint64(), common.Hash{0x01, 0xaa})
require.NotNil(t, game)
outputRootClaim := game.DisputeLastBlock(ctx)
game.LogGameData(ctx)
honestChallenger := game.StartChallenger(ctx, "HonestActor", challenger.WithPrivKey(sys.Cfg.Secrets.Alice))
// Wait for the honest challenger to dispute the outputRootClaim. This creates a root of an execution game that we challenge by coercing
// a step at a preimage trace index.
outputRootClaim = outputRootClaim.WaitForCounterClaim(ctx)
// Now the honest challenger is positioned as the defender of the execution game
// We then move to challenge it to induce a preimage load
preimageLoadCheck := game.CreateStepPreimageLoadCheck(ctx)
game.ChallengeToPreimageLoad(ctx, outputRootClaim, sys.Cfg.Secrets.Alice, utils.FirstPreimageLoadOfType("precompile"), preimageLoadCheck, false)
// The above method already verified the image was uploaded and step called successfully
// So we don't waste time resolving the game - that's tested elsewhere.
require.NoError(t, honestChallenger.Close())
})
}
}
......
......@@ -95,12 +95,12 @@ func (o *CachingOracle) GetBlob(ref eth.L1BlockRef, blobHash eth.IndexedBlobHash
return blob
}
func (o *CachingOracle) Precompile(address common.Address, input []byte) ([]byte, bool) {
func (o *CachingOracle) Precompile(address common.Address, input []byte, requiredGas uint64) ([]byte, bool) {
cacheKey := crypto.Keccak256Hash(append(address.Bytes(), input...))
if val, ok := o.pcmps.Get(cacheKey); ok {
return val.result, val.ok
}
res, ok := o.oracle.Precompile(address, input)
res, ok := o.oracle.Precompile(address, input, requiredGas)
o.pcmps.Add(cacheKey, precompileResult{res, ok})
return res, ok
}
package l1
import (
"encoding/binary"
"math/rand"
"testing"
......@@ -99,18 +100,21 @@ func TestCachingOracle_Precompile(t *testing.T) {
oracle := NewCachingOracle(stub)
input := []byte{0x01, 0x02, 0x03, 0x04}
requiredGas := uint64(100)
output := []byte{0x0a, 0x0b, 0x0c, 0x0d}
addr := common.Address{0x1}
key := crypto.Keccak256Hash(append(append(addr.Bytes(), binary.BigEndian.AppendUint64(nil, requiredGas)...), input...))
// Initial call retrieves from the stub
stub.PcmpResults[crypto.Keccak256Hash(append(addr.Bytes(), input...))] = output
actualResult, actualStatus := oracle.Precompile(addr, input)
stub.PcmpResults[key] = output
actualResult, actualStatus := oracle.Precompile(addr, input, requiredGas)
require.True(t, actualStatus)
require.EqualValues(t, output, actualResult)
// Later calls should retrieve from cache
delete(stub.PcmpResults, crypto.Keccak256Hash(append(addr.Bytes(), input...)))
actualResult, actualStatus = oracle.Precompile(addr, input)
delete(stub.PcmpResults, key)
actualResult, actualStatus = oracle.Precompile(addr, input, requiredGas)
require.True(t, actualStatus)
require.EqualValues(t, output, actualResult)
}
......@@ -13,6 +13,7 @@ const (
HintL1Receipts = "l1-receipts"
HintL1Blob = "l1-blob"
HintL1Precompile = "l1-precompile"
HintL1PrecompileV2 = "l1-precompile-v2"
)
type BlockHeaderHint common.Hash
......@@ -54,3 +55,11 @@ var _ preimage.Hint = PrecompileHint{}
func (l PrecompileHint) Hint() string {
return HintL1Precompile + " " + hexutil.Encode(l)
}
type PrecompileHintV2 []byte
var _ preimage.Hint = PrecompileHintV2{}
func (l PrecompileHintV2) Hint() string {
return HintL1PrecompileV2 + " " + hexutil.Encode(l)
}
......@@ -29,7 +29,7 @@ type Oracle interface {
GetBlob(ref eth.L1BlockRef, blobHash eth.IndexedBlobHash) *eth.Blob
// Precompile retrieves the result and success indicator of a precompile call for the given input.
Precompile(precompileAddress common.Address, input []byte) ([]byte, bool)
Precompile(precompileAddress common.Address, input []byte, requiredGas uint64) ([]byte, bool)
}
// PreimageOracle implements Oracle using by interfacing with the pure preimage.Oracle
......@@ -119,9 +119,10 @@ func (p *PreimageOracle) GetBlob(ref eth.L1BlockRef, blobHash eth.IndexedBlobHas
return &blob
}
func (p *PreimageOracle) Precompile(address common.Address, input []byte) ([]byte, bool) {
hintBytes := append(address.Bytes(), input...)
p.hint.Hint(PrecompileHint(hintBytes))
func (p *PreimageOracle) Precompile(address common.Address, input []byte, requiredGas uint64) ([]byte, bool) {
hintBytes := append(address.Bytes(), binary.BigEndian.AppendUint64(nil, requiredGas)...)
hintBytes = append(hintBytes, input...)
p.hint.Hint(PrecompileHintV2(hintBytes))
key := preimage.PrecompileKey(crypto.Keccak256Hash(hintBytes))
result := p.oracle.Get(key)
if len(result) == 0 { // must contain at least the status code
......
package test
import (
"encoding/binary"
"testing"
"github.com/ethereum-optimism/optimism/op-service/eth"
......@@ -75,8 +76,10 @@ func (o StubOracle) GetBlob(ref eth.L1BlockRef, blobHash eth.IndexedBlobHash) *e
return blob
}
func (o StubOracle) Precompile(addr common.Address, input []byte) ([]byte, bool) {
result, ok := o.PcmpResults[crypto.Keccak256Hash(append(addr.Bytes(), input...))]
func (o StubOracle) Precompile(addr common.Address, input []byte, requiredGas uint64) ([]byte, bool) {
arg := append(addr.Bytes(), binary.BigEndian.AppendUint64(nil, requiredGas)...)
arg = append(arg, input...)
result, ok := o.PcmpResults[crypto.Keccak256Hash(arg)]
if !ok {
o.t.Fatalf("unknown kzg point evaluation %x", input)
}
......
package l2
import (
"encoding/binary"
"math/big"
"testing"
......@@ -40,6 +41,12 @@ var (
blobPrecompileReturnValue = common.FromHex("000000000000000000000000000000000000000000000000000000000000100073eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001")
)
var (
ecRecoverRequiredGas uint64 = 3000
bn256PairingRequiredGas uint64 = 113000
kzgRequiredGas uint64 = 50_000
)
func TestInitialState(t *testing.T) {
blocks, chain := setupOracleBackedChain(t, 5)
head := blocks[5]
......@@ -200,28 +207,32 @@ func TestGetHeaderByNumber(t *testing.T) {
func TestPrecompileOracle(t *testing.T) {
tests := []struct {
name string
input []byte
target common.Address
result []byte
name string
input []byte
target common.Address
requiredGas uint64
result []byte
}{
{
name: "EcRecover",
input: ecRecoverInputData,
target: common.BytesToAddress([]byte{0x1}),
result: ecRecoverReturnValue,
name: "EcRecover",
input: ecRecoverInputData,
target: common.BytesToAddress([]byte{0x1}),
requiredGas: ecRecoverRequiredGas,
result: ecRecoverReturnValue,
},
{
name: "Bn256Pairing",
input: bn256PairingInputData,
target: common.BytesToAddress([]byte{0x8}),
result: bn256PairingReturnValue,
name: "Bn256Pairing",
input: bn256PairingInputData,
target: common.BytesToAddress([]byte{0x8}),
requiredGas: bn256PairingRequiredGas,
result: bn256PairingReturnValue,
},
{
name: "KZGPointEvaluation",
input: kzgInputData,
target: common.BytesToAddress([]byte{0xa}),
result: blobPrecompileReturnValue,
name: "KZGPointEvaluation",
input: kzgInputData,
target: common.BytesToAddress([]byte{0xa}),
requiredGas: kzgRequiredGas,
result: blobPrecompileReturnValue,
},
}
......@@ -234,9 +245,11 @@ func TestPrecompileOracle(t *testing.T) {
chainCfg, blocks, oracle := setupOracle(t, blockCount, headBlockNumber, true)
head := blocks[headBlockNumber].Hash()
stubOutput := eth.OutputV0{BlockHash: head}
precompileOracle := new(l2test.StubPrecompileOracle)
precompileOracle := l2test.NewStubPrecompileOracle(t)
arg := append(test.target.Bytes(), binary.BigEndian.AppendUint64(nil, test.requiredGas)...)
arg = append(arg, test.input...)
precompileOracle.Results = map[common.Hash]l2test.PrecompileResult{
crypto.Keccak256Hash(append(test.target.Bytes(), test.input...)): {Result: test.result, Ok: true},
crypto.Keccak256Hash(arg): {Result: test.result, Ok: true},
}
chain, err := NewOracleBackedL2Chain(logger, oracle, precompileOracle, chainCfg, common.Hash(eth.OutputRoot(&stubOutput)))
require.NoError(t, err)
......@@ -265,7 +278,7 @@ func setupOracleBackedChainWithLowerHead(t *testing.T, blockCount int, headBlock
chainCfg, blocks, oracle := setupOracle(t, blockCount, headBlockNumber, false)
head := blocks[headBlockNumber].Hash()
stubOutput := eth.OutputV0{BlockHash: head}
precompileOracle := new(l2test.StubPrecompileOracle)
precompileOracle := l2test.NewStubPrecompileOracle(t)
chain, err := NewOracleBackedL2Chain(logger, oracle, precompileOracle, chainCfg, common.Hash(eth.OutputRoot(&stubOutput)))
require.NoError(t, err)
return blocks, chain
......
......@@ -46,7 +46,7 @@ var (
// PrecompileOracle defines the high-level API used to retrieve the result of a precompile call
// The caller is expected to validate the input to the precompile call
type PrecompileOracle interface {
Precompile(address common.Address, input []byte) ([]byte, bool)
Precompile(address common.Address, input []byte, requiredGas uint64) ([]byte, bool)
}
func CreatePrecompileOverrides(precompileOracle PrecompileOracle) vm.PrecompileOverrides {
......@@ -104,7 +104,7 @@ func (c *ecrecoverOracle) Run(input []byte) ([]byte, error) {
// v needs to be at the end for libsecp256k1
// Modification note: below replaces the crypto.Ecrecover call
result, ok := c.Oracle.Precompile(ecrecoverPrecompileAddress, input)
result, ok := c.Oracle.Precompile(ecrecoverPrecompileAddress, input, c.RequiredGas(input))
if !ok {
return nil, errors.New("invalid ecrecover input")
}
......@@ -147,7 +147,7 @@ func (b *bn256PairingOracle) Run(input []byte) ([]byte, error) {
}
// Modification note: below replaces point verification and pairing checks
// Assumes both L2 and the L1 oracle have an identical range of valid points
result, ok := b.Oracle.Precompile(bn256PairingPrecompileAddress, input)
result, ok := b.Oracle.Precompile(bn256PairingPrecompileAddress, input, b.RequiredGas(input))
if !ok {
return nil, errors.New("invalid bn256Pairing check")
}
......@@ -214,7 +214,7 @@ func (b *kzgPointEvaluationOracle) Run(input []byte) ([]byte, error) {
copy(proof[:], input[144:])
// Modification note: below replaces the kzg4844.VerifyProof call
result, ok := b.Oracle.Precompile(kzgPointEvaluationPrecompileAddress, input)
result, ok := b.Oracle.Precompile(kzgPointEvaluationPrecompileAddress, input, b.RequiredGas(input))
if !ok {
return nil, fmt.Errorf("%w: invalid KZG point evaluation", errBlobVerifyKZGProof)
}
......
package test
import (
"encoding/binary"
"testing"
"github.com/ethereum-optimism/optimism/op-service/eth"
......@@ -130,15 +131,21 @@ type StubPrecompileOracle struct {
Calls int
}
func NewStubPrecompileOracle(t *testing.T) *StubPrecompileOracle {
return &StubPrecompileOracle{t: t, Results: make(map[common.Hash]PrecompileResult)}
}
type PrecompileResult struct {
Result []byte
Ok bool
}
func (o *StubPrecompileOracle) Precompile(address common.Address, input []byte) ([]byte, bool) {
result, ok := o.Results[crypto.Keccak256Hash(append(address.Bytes(), input...))]
func (o *StubPrecompileOracle) Precompile(address common.Address, input []byte, requiredGas uint64) ([]byte, bool) {
arg := append(address.Bytes(), binary.BigEndian.AppendUint64(nil, requiredGas)...)
arg = append(arg, input...)
result, ok := o.Results[crypto.Keccak256Hash(arg)]
if !ok {
o.t.Fatalf("no value for point evaluation %v", input)
o.t.Fatalf("no value for point evaluation %x required gas %v", input, requiredGas)
}
o.Calls++
return result.Result, result.Ok
......
......@@ -205,6 +205,36 @@ func (p *Prefetcher) prefetch(ctx context.Context, hint string) error {
return err
}
return p.kvStore.Put(preimage.PrecompileKey(inputHash).PreimageKey(), result)
case l1.HintL1PrecompileV2:
if len(hintBytes) < 28 {
return fmt.Errorf("invalid precompile hint: %x", hint)
}
precompileAddress := common.BytesToAddress(hintBytes[:20])
// requiredGas := hintBytes[20:28] - unused by the host. Since the client already validates gas requirements.
// The requiredGas is only used by the L1 PreimageOracle to enforce complete precompile execution.
// For extra safety, avoid accelerating unexpected precompiles
if !slices.Contains(acceleratedPrecompiles, precompileAddress) {
return fmt.Errorf("unsupported precompile address: %s", precompileAddress)
}
// NOTE: We use the precompiled contracts from Cancun because it's the only set that contains the addresses of all accelerated precompiles
// We assume the precompile Run function behavior does not change across EVM upgrades.
// As such, we must not rely on upgrade-specific behavior such as precompile.RequiredGas.
precompile := getPrecompiledContract(precompileAddress)
// KZG Point Evaluation precompile also verifies its input
result, err := precompile.Run(hintBytes[28:])
if err == nil {
result = append(precompileSuccess[:], result...)
} else {
result = append(precompileFailure[:], result...)
}
inputHash := crypto.Keccak256Hash(hintBytes)
// Put the input preimage so it can be loaded later
if err := p.kvStore.Put(preimage.Keccak256Key(inputHash).PreimageKey(), hintBytes); err != nil {
return err
}
return p.kvStore.Put(preimage.PrecompileKey(inputHash).PreimageKey(), result)
case l2.HintL2BlockHeader, l2.HintL2Transactions:
if len(hintBytes) != 32 {
return fmt.Errorf("invalid L2 header/tx hint: %x", hint)
......
......@@ -4,6 +4,7 @@ import (
"context"
"crypto/sha256"
"encoding/binary"
"fmt"
"math/rand"
"testing"
......@@ -25,6 +26,11 @@ import (
"github.com/ethereum-optimism/optimism/op-service/testutils"
)
var (
ecRecoverInput = common.FromHex("18c547e4f7b0f325ad1e56f57e26c745b09a3e503d86e00e5255ff7f715d3d1c000000000000000000000000000000000000000000000000000000000000001c73b1693892219d736caba55bdb67216e485557ea6b6af75f37096c9aa6a5a75feeb940b1d03b21e36b0e47e79769f095fe2ab855bd91e3a38756b7d75a9c4549")
kzgPointEvalInput = common.FromHex("01e798154708fe7789429634053cbf9f99b619f9f084048927333fce637f549b564c0a11a0f704f4fc3e8acfe0f8245f0ad1347b378fbf96e206da11a5d3630624d25032e67a7e6a4910df5834b8fe70e6bcfeeac0352434196bdf4b2485d5a18f59a8d2a1a625a17f3fea0fe5eb8c896db3764f3185481bc22f91b4aaffcca25f26936857bc3a7c2539ea8ec3a952b7873033e038326e87ed3e1276fd140253fa08e9fc25fb2d9a98527fc22a2c9612fbeafdad446cbc7bcdbdcd780af2c16a")
)
func TestNoHint(t *testing.T) {
t.Run("NotFound", func(t *testing.T) {
prefetcher, _, _, _, _ := createPrefetcher(t)
......@@ -221,9 +227,6 @@ func TestFetchL1Blob(t *testing.T) {
}
func TestFetchPrecompileResult(t *testing.T) {
ecRecoverInput := common.FromHex("18c547e4f7b0f325ad1e56f57e26c745b09a3e503d86e00e5255ff7f715d3d1c000000000000000000000000000000000000000000000000000000000000001c73b1693892219d736caba55bdb67216e485557ea6b6af75f37096c9aa6a5a75feeb940b1d03b21e36b0e47e79769f095fe2ab855bd91e3a38756b7d75a9c4549")
kzgPointEvalInput := common.FromHex("01e798154708fe7789429634053cbf9f99b619f9f084048927333fce637f549b564c0a11a0f704f4fc3e8acfe0f8245f0ad1347b378fbf96e206da11a5d3630624d25032e67a7e6a4910df5834b8fe70e6bcfeeac0352434196bdf4b2485d5a18f59a8d2a1a625a17f3fea0fe5eb8c896db3764f3185481bc22f91b4aaffcca25f26936857bc3a7c2539ea8ec3a952b7873033e038326e87ed3e1276fd140253fa08e9fc25fb2d9a98527fc22a2c9612fbeafdad446cbc7bcdbdcd780af2c16a")
failure := []byte{0}
success := []byte{1}
......@@ -239,6 +242,18 @@ func TestFetchPrecompileResult(t *testing.T) {
input: ecRecoverInput,
result: append(success, common.FromHex("000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b")...),
},
{
name: "KzgPointEvaluation-Valid",
addr: common.BytesToAddress([]byte{0xa}),
input: kzgPointEvalInput,
result: append(success, common.FromHex("000000000000000000000000000000000000000000000000000000000000100073eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001")...),
},
{
name: "KzgPointEvaluation-Invalid",
addr: common.BytesToAddress([]byte{0xa}),
input: []byte{0x0},
result: failure,
},
{
name: "Bn256Pairing-Valid",
addr: common.BytesToAddress([]byte{0x8}),
......@@ -251,17 +266,88 @@ func TestFetchPrecompileResult(t *testing.T) {
input: []byte{0x1},
result: failure,
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
prefetcher, _, _, _, _ := createPrefetcher(t)
oracle := newLegacyPrecompileOracle(asOracleFn(t, prefetcher), asHinter(t, prefetcher))
result, ok := oracle.Precompile(test.addr, test.input)
require.Equal(t, test.result[0] == 1, ok)
require.EqualValues(t, test.result[1:], result)
key := crypto.Keccak256Hash(append(test.addr.Bytes(), test.input...))
val, err := prefetcher.kvStore.Get(preimage.Keccak256Key(key).PreimageKey())
require.NoError(t, err)
require.NotEmpty(t, val)
val, err = prefetcher.kvStore.Get(preimage.PrecompileKey(key).PreimageKey())
require.NoError(t, err)
require.EqualValues(t, test.result, val)
})
}
t.Run("Already Known", func(t *testing.T) {
input := []byte("test input")
addr := common.BytesToAddress([]byte{0x1})
result := []byte{0x1}
prefetcher, _, _, _, kv := createPrefetcher(t)
err := kv.Put(preimage.PrecompileKey(crypto.Keccak256Hash(append(addr.Bytes(), input...))).PreimageKey(), append([]byte{1}, result...))
require.NoError(t, err)
oracle := newLegacyPrecompileOracle(asOracleFn(t, prefetcher), asHinter(t, prefetcher))
actualResult, status := oracle.Precompile(addr, input)
require.EqualValues(t, result, actualResult)
require.True(t, status)
})
}
func TestFetchPrecompileResultV2(t *testing.T) {
failure := []byte{0}
success := []byte{1}
tests := []struct {
name string
addr common.Address
input []byte
requiredGas uint64
result []byte
}{
{
name: "EcRecover-Valid",
addr: common.BytesToAddress([]byte{0x1}),
input: ecRecoverInput,
requiredGas: 3000,
result: append(success, common.FromHex("000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b")...),
},
{
name: "KzgPointEvaluation-Valid",
addr: common.BytesToAddress([]byte{0xa}),
input: kzgPointEvalInput,
result: append(success, common.FromHex("000000000000000000000000000000000000000000000000000000000000100073eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001")...),
name: "Bn256Pairing-Valid",
addr: common.BytesToAddress([]byte{0x8}),
input: []byte{}, // empty is valid
requiredGas: 6000,
result: append(success, common.FromHex("0000000000000000000000000000000000000000000000000000000000000001")...),
},
{
name: "KzgPointEvaluation-Invalid",
addr: common.BytesToAddress([]byte{0xa}),
input: []byte{0x0},
result: failure,
name: "Bn256Pairing-Invalid",
addr: common.BytesToAddress([]byte{0x8}),
input: []byte{0x1},
requiredGas: 6000,
result: failure,
},
{
name: "KzgPointEvaluation-Valid",
addr: common.BytesToAddress([]byte{0xa}),
input: kzgPointEvalInput,
requiredGas: 50_000,
result: append(success, common.FromHex("000000000000000000000000000000000000000000000000000000000000100073eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001")...),
},
{
name: "KzgPointEvaluation-Invalid",
addr: common.BytesToAddress([]byte{0xa}),
input: []byte{0x0},
requiredGas: 50_000,
result: failure,
},
}
for _, test := range tests {
......@@ -270,11 +356,11 @@ func TestFetchPrecompileResult(t *testing.T) {
prefetcher, _, _, _, _ := createPrefetcher(t)
oracle := l1.NewPreimageOracle(asOracleFn(t, prefetcher), asHinter(t, prefetcher))
result, ok := oracle.Precompile(test.addr, test.input)
result, ok := oracle.Precompile(test.addr, test.input, test.requiredGas)
require.Equal(t, test.result[0] == 1, ok)
require.EqualValues(t, test.result[1:], result)
key := crypto.Keccak256Hash(append(test.addr.Bytes(), test.input...))
key := crypto.Keccak256Hash(append(append(test.addr.Bytes(), binary.BigEndian.AppendUint64(nil, test.requiredGas)...), test.input...))
val, err := prefetcher.kvStore.Get(preimage.Keccak256Key(key).PreimageKey())
require.NoError(t, err)
require.NotEmpty(t, val)
......@@ -287,19 +373,35 @@ func TestFetchPrecompileResult(t *testing.T) {
t.Run("Already Known", func(t *testing.T) {
input := []byte("test input")
requiredGas := uint64(3000)
addr := common.BytesToAddress([]byte{0x1})
result := []byte{0x1}
prefetcher, _, _, _, kv := createPrefetcher(t)
err := kv.Put(preimage.PrecompileKey(crypto.Keccak256Hash(append(addr.Bytes(), input...))).PreimageKey(), append([]byte{1}, result...))
keyArg := append(addr.Bytes(), binary.BigEndian.AppendUint64(nil, requiredGas)...)
keyArg = append(keyArg, input...)
err := kv.Put(preimage.PrecompileKey(crypto.Keccak256Hash(keyArg)).PreimageKey(), append([]byte{1}, result...))
require.NoError(t, err)
oracle := l1.NewPreimageOracle(asOracleFn(t, prefetcher), asHinter(t, prefetcher))
actualResult, status := oracle.Precompile(addr, input)
actualResult, status := oracle.Precompile(addr, input, requiredGas)
require.EqualValues(t, actualResult, result)
require.True(t, status)
})
}
func TestUnsupportedPrecompile(t *testing.T) {
prefetcher, _, _, _, _ := createPrefetcher(t)
oracleFn := func(t *testing.T, prefetcher *Prefetcher) preimage.OracleFn {
return func(key preimage.Key) []byte {
_, err := prefetcher.GetPreimage(context.Background(), key.PreimageKey())
require.ErrorContains(t, err, "unsupported precompile address")
return []byte{1}
}
}
oracle := newLegacyPrecompileOracle(oracleFn(t, prefetcher), asHinter(t, prefetcher))
oracle.Precompile(common.HexToAddress("0xdead"), nil)
}
func TestRestrictedPrecompileContracts(t *testing.T) {
for _, addr := range acceleratedPrecompiles {
require.NotNil(t, getPrecompiledContract(addr))
......@@ -596,3 +698,28 @@ func assertReceiptsEqual(t *testing.T, expectedRcpt types.Receipts, actualRcpt t
require.Equal(t, expected, actual)
}
}
// legacyOracleImpl is a wrapper around the new preimage.Oracle interface that uses the legacy preimage hint API.
// It's used to test backwards-compatibility with clients using legacy preimage hints.
type legacyPrecompileOracle struct {
oracle preimage.Oracle
hint preimage.Hinter
}
func newLegacyPrecompileOracle(raw preimage.Oracle, hint preimage.Hinter) *legacyPrecompileOracle {
return &legacyPrecompileOracle{
oracle: raw,
hint: hint,
}
}
func (o *legacyPrecompileOracle) Precompile(address common.Address, input []byte) ([]byte, bool) {
hintBytes := append(address.Bytes(), input...)
o.hint.Hint(l1.PrecompileHint(hintBytes))
key := preimage.PrecompileKey(crypto.Keccak256Hash(hintBytes))
result := o.oracle.Get(key)
if len(result) == 0 { // must contain at least the status code
panic(fmt.Errorf("unexpected precompile oracle behavior, got result: %x", result))
}
return result[1:], result[0] == 1
}
......@@ -5,7 +5,7 @@ SCRIPTS_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
COMPAT_DIR="${SCRIPTS_DIR}/../temp/compat"
TESTNAME="${1?Must specify compat file to run}"
BASEURL="${2:-https://github.com/ethereum-optimism/chain-test-data/releases/download/2024-03-14.3}"
BASEURL="${2:-https://github.com/ethereum-optimism/chain-test-data/releases/download/2024-08-02}"
URL="${BASEURL}/${TESTNAME}.tar.bz"
......
......@@ -56,7 +56,7 @@ func (c *expectedCall) Matches(rpcMethod string, args ...interface{}) error {
return fmt.Errorf("expected input to have at least 4 bytes but was %v", len(data))
}
if !slices.Equal(c.abiMethod.ID, data[:4]) {
return fmt.Errorf("expected abi method ID %x but was %x", c.abiMethod.ID, data[:4])
return fmt.Errorf("expected abi method ID %x but was %v", c.abiMethod.ID, data[:4])
}
if !slices.Equal(c.packedArgs, data[4:]) {
return fmt.Errorf("expected args %x but was %x", c.packedArgs, data[4:])
......
......@@ -59,7 +59,7 @@ func (r *RpcStub) findExpectedCall(rpcMethod string, args ...interface{}) Expect
if err := call.Matches(rpcMethod, args...); err == nil {
return call
} else {
matchResults += fmt.Sprintf("%v: %v", call, err)
matchResults += fmt.Sprintf("%v: %v\n", call, err)
}
}
require.Failf(r.t, "No matching expected calls.", matchResults)
......
......@@ -41,7 +41,7 @@
"requiredProtocolVersion": "0x0000000000000000000000000000000000000004000000000000000000000001",
"recommendedProtocolVersion": "0x0000000000000000000000000000000000000004000000000000000000000001",
"fundDevAccounts": false,
"faultGameAbsolutePrestate": "0x0385c3f8ee78491001d92b90b07d0cf387b7b52ab9b83b4d87c994e92cf823ba",
"faultGameAbsolutePrestate": "0x030de10d9da911a2b180ecfae2aeaba8758961fc28262ce989458c6f9a547922",
"faultGameMaxDepth": 73,
"faultGameClockExtension": 10800,
"faultGameMaxClockDuration": 302400,
......
......@@ -208,6 +208,32 @@ library ChainAssertions {
}
}
/// @notice Asserts that the permissioned DelayedWETH is setup correctly
function checkPermissionedDelayedWETH(
Types.ContractSet memory _contracts,
DeployConfig _cfg,
bool _isProxy,
address _expectedOwner
)
internal
view
{
console.log("Running chain assertions on the permissioned DelayedWETH");
DelayedWETH weth = DelayedWETH(payable(_contracts.PermissionedDelayedWETH));
// Check that the contract is initialized
assertSlotValueIsOne({ _contractAddress: address(weth), _slot: 0, _offset: 0 });
if (_isProxy) {
require(weth.owner() == _expectedOwner);
require(weth.delay() == _cfg.faultGameWithdrawalDelay());
require(weth.config() == SuperchainConfig(_contracts.SuperchainConfig));
} else {
require(weth.owner() == _expectedOwner);
require(weth.delay() == _cfg.faultGameWithdrawalDelay());
}
}
/// @notice Asserts that the L2OutputOracle is setup correctly
function checkL2OutputOracle(
Types.ContractSet memory _contracts,
......
......@@ -92,6 +92,8 @@ contract FPACOPS is Deploy, StdAssertions {
function initializeAnchorStateRegistryProxy() internal broadcast {
console.log("Initializing AnchorStateRegistryProxy with AnchorStateRegistry.");
address superchainConfigProxy = mustGetAddress("SuperchainConfigProxy");
SuperchainConfig superchainConfig = SuperchainConfig(superchainConfigProxy);
AnchorStateRegistry.StartingAnchorRoot[] memory roots = new AnchorStateRegistry.StartingAnchorRoot[](2);
roots[0] = AnchorStateRegistry.StartingAnchorRoot({
......@@ -111,7 +113,8 @@ contract FPACOPS is Deploy, StdAssertions {
address asrProxy = mustGetAddress("AnchorStateRegistryProxy");
Proxy(payable(asrProxy)).upgradeToAndCall(
mustGetAddress("AnchorStateRegistry"), abi.encodeCall(AnchorStateRegistry.initialize, (roots))
mustGetAddress("AnchorStateRegistry"),
abi.encodeCall(AnchorStateRegistry.initialize, (roots, superchainConfig))
);
}
......
This diff is collapsed.
......@@ -23,4 +23,9 @@ cannon-prestate: # Generate the cannon prestate, and tar the `op-program` + `can
.PHONY: deploy-fresh
deploy-fresh: cannon-prestate # Deploy a fresh version of the FPAC contracts. Pass `--broadcast` to send to the network.
forge script FPACOPS.sol --sig "deployFPAC(address,address)" $(proxy-admin) $(system-owner-safe) --chain $(chain) -vvv $(args)
forge script FPACOPS.s.sol --sig "deployFPAC(address,address,address)" $(proxy-admin) $(system-owner-safe) $(superchain-config-proxy) --chain $(chain) -vvv $(args)
# TODO: Convert this whole file to a justfile
.PHONY: deploy-upgrade
deploy-upgrade: cannon-prestate # Deploy upgraded FP contracts. Pass `--broadcast` to send to the network.
forge script FPACOPS2.s.sol --sig "deployFPAC2(address,address,address,address,address)" $(proxy-admin) $(system-owner-safe) $(superchain-config-proxy) $(dispute-game-factory-proxy) $(anchor-state-registry-proxy) --chain $(chain) -vvv $(args)
......@@ -9,6 +9,7 @@ library Types {
address L2OutputOracle;
address DisputeGameFactory;
address DelayedWETH;
address PermissionedDelayedWETH;
address AnchorStateRegistry;
address OptimismMintableERC20Factory;
address OptimismPortal;
......
This diff is collapsed.
......@@ -71,6 +71,24 @@
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "contract AnchorStateRegistry",
"name": "_registry",
"type": "address"
},
{
"internalType": "contract IFaultDisputeGame",
"name": "_game",
"type": "address"
}
],
"name": "setAnchorState",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
......
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