Commit ff823e43 authored by protolambda's avatar protolambda

migration: drop legacy extras / CI

parent 086aa0f0
on: [push, pull_request]
name: Cannon Test
permissions:
contents: read
jobs:
cannon-test:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v3
with:
go-version: 1.20.x
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
- uses: actions/checkout@v3
with:
submodules: true
- name: unicorn commit hash
working-directory: ./diffmips/unicorn
run: |
git rev-parse HEAD > /tmp/unicorn-commit-hash.txt
- name: cached libunicorn
uses: actions/cache@v3
with:
path: |
./diffmips/unicorn/build
key:
unicorn-build-{{ hashFiles('/tmp/unicorn-commit-hash.txt') }}
restore-keys: |
unicorn-build-{{ hashFiles('/tmp/unicorn-commit-hash.txt') }}
- name: install libunicorn
working-directory: ./diffmips
run: make libunicorn
- uses: actions/cache@v3
with:
# In order:
# * Module download cache
# * Build cache (Linux)
# * Build cache (Mac)
# * Build cache (Windows)
path: |
~/go/pkg/mod
~/.cache/go-build
~/Library/Caches/go-build
~\AppData\Local\go-build
key: ${{ runner.os }}-go-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-${{ matrix.go-version }}-
- name: main golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: v1.53.3
working-directory: ./
skip-cache: true # we already have go caching
args: --timeout=3m
- name: unicorntest golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: v1.53.3
working-directory: ./diffmips/unicorntest
skip-cache: true # we already have go caching
args: --timeout=3m
- name: Build examples
working-directory: ./example
run: make elf
- name: build contracts
working-directory: ./contracts
run: forge build
- name: mipsevm tests
working-directory: ./mipsevm
run: go test ./...
- name: diffmips tests
working-directory: ./diffmips/unicorntest
run: go test ./...
[submodule "unicorn"]
path = diffmips/unicorn
url = https://github.com/unicorn-engine/unicorn.git
SHELL := /bin/bash
build: submodules libunicorn
.PHONY: build
submodules:
# CI will checkout submodules on its own (and fails on these commands)
if [[ -z "$$GITHUB_ENV" ]]; then \
git submodule init; \
git submodule update; \
fi
.PHONY: submodules
# Approximation, use `make libunicorn_rebuild` to force.
unicorn/build: unicorn/CMakeLists.txt
mkdir -p unicorn/build
cd unicorn/build && cmake .. -DUNICORN_ARCH=mips -DCMAKE_BUILD_TYPE=Release
# Not sure why, but the second invocation is needed for fresh installs on MacOS.
if [ "$(shell uname)" == "Darwin" ]; then \
cd unicorn/build && cmake .. -DUNICORN_ARCH=mips -DCMAKE_BUILD_TYPE=Release; \
fi
# Rebuild whenever anything in the unicorn/ directory changes.
unicorn/build/libunicorn.so: unicorn/build unicorn
cd unicorn/build && make -j8
# The Go linker / runtime expects dynamic libraries in the unicorn/ dir.
find ./unicorn/build -name "libunicorn.*" | xargs -L 1 -I {} cp {} ./unicorn/
# Update timestamp on libunicorn.so to make it more recent than the build/ dir.
# On Mac this will create a new empty file (dyn libraries are .dylib), but works
# fine for the purpose of avoiding recompilation.
touch unicorn/build/libunicorn.so
libunicorn: unicorn/build/libunicorn.so
.PHONY: libunicorn
libunicorn_rebuild:
touch unicorn/CMakeLists.txt
make libunicorn
.PHONY: libunicorn_rebuild
clean:
rm -f unicorn/libunicorn.*
.PHONY: clean
test:
cd unicorntest && go test -v ./...
.PHONY: test
# diffmips
This is a collection of MIPS testing tools.
The Unicorn emulator first backed Cannon directly, but has been separated as a testing-only tool,
and is replaced with a Go implementation of the minimal MIPS functionality.
Directory layout
```
unicorn -- Sub-module, used by mipsevm for offchain MIPS emulation.
unicorntest -- Go module with Go tests, diffed against the Cannon state. [Work in Progress]
```
### `unicorn`
To build unicorn from source (git sub-module), run:
```
make libunicorn
```
### `unicorntest`
This requires `unicorn` to be built, as well as the `contracts` for testing.
To test:
```
make test
```
Subproject commit 7b8c63dfe650b5d4d2bf684526161971925e6350
package unicorntest
import (
"bytes"
"os"
"path"
"testing"
"github.com/stretchr/testify/require"
"github.com/ethereum-optimism/cannon/mipsevm"
)
// baseAddrStart - baseAddrEnd is used in tests to write the results to
const baseAddrEnd = 0xbf_ff_ff_f0
const baseAddrStart = 0xbf_c0_00_00
// endAddr is used as return-address for tests
const endAddr = 0xa7ef00d0
func TestState(t *testing.T) {
testFiles, err := os.ReadDir("../../mipsevm/open_mips_tests/test/bin")
require.NoError(t, err)
for _, f := range testFiles {
t.Run(f.Name(), func(t *testing.T) {
if f.Name() == "oracle.bin" {
t.Skip("oracle test needs to be updated to use syscall pre-image oracle")
}
fn := path.Join("../../mipsevm/open_mips_tests/test/bin", f.Name())
programMem, err := os.ReadFile(fn)
require.NoError(t, err)
state := &mipsevm.State{PC: 0, NextPC: 4, Memory: mipsevm.NewMemory()}
err = state.Memory.SetMemoryRange(0, bytes.NewReader(programMem))
require.NoError(t, err, "load program into state")
// set the return address ($ra) to jump into when test completes
state.Registers[31] = endAddr
mu, err := NewUnicorn()
require.NoError(t, err, "load unicorn")
defer mu.Close()
require.NoError(t, mu.MemMap(baseAddrStart, ((baseAddrEnd-baseAddrStart)&^mipsevm.PageAddrMask)+mipsevm.PageSize))
require.NoError(t, mu.MemMap(endAddr&^mipsevm.PageAddrMask, mipsevm.PageSize))
err = LoadUnicorn(state, mu)
require.NoError(t, err, "load state into unicorn")
us, err := NewUnicornState(mu, state, nil, os.Stdout, os.Stderr)
require.NoError(t, err, "hook unicorn to state")
for i := 0; i < 1000; i++ {
if us.state.PC == endAddr {
break
}
_, err := us.Step(false)
require.NoError(t, err)
}
require.Equal(t, uint32(endAddr), us.state.PC, "must reach end")
// inspect test result
done, result := state.Memory.GetMemory(baseAddrEnd+4), state.Memory.GetMemory(baseAddrEnd+8)
require.Equal(t, done, uint32(1), "must be done")
require.Equal(t, result, uint32(1), "must have success result")
})
}
}
module unicorntest
go 1.20
require (
github.com/ethereum-optimism/cannon v0.0.0
github.com/stretchr/testify v1.8.2
github.com/unicorn-engine/unicorn v0.0.0-20230207094436-7b8c63dfe650
)
require (
github.com/DataDog/zstd v1.5.2 // indirect
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 // indirect
github.com/VictoriaMetrics/fastcache v1.6.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cockroachdb/errors v1.9.1 // indirect
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect
github.com/cockroachdb/pebble v0.0.0-20230209160836-829675f94811 // indirect
github.com/cockroachdb/redact v1.1.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/deckarep/golang-set/v2 v2.1.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
github.com/edsrzf/mmap-go v1.0.0 // indirect
github.com/ethereum-optimism/cannon/preimage v0.0.0 // indirect
github.com/ethereum/go-ethereum v1.11.5 // indirect
github.com/getsentry/sentry-go v0.18.0 // indirect
github.com/go-ole/go-ole v1.2.1 // indirect
github.com/go-stack/stack v1.8.1 // indirect
github.com/gofrs/flock v0.8.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/holiman/bloomfilter/v2 v2.0.3 // indirect
github.com/holiman/uint256 v1.2.0 // indirect
github.com/klauspost/compress v1.15.15 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.14.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.39.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect
github.com/tklauser/go-sysconf v0.3.5 // indirect
github.com/tklauser/numcpus v0.2.2 // indirect
golang.org/x/crypto v0.8.0 // indirect
golang.org/x/exp v0.0.0-20230206171751-46f607a40771 // indirect
golang.org/x/sys v0.7.0 // indirect
golang.org/x/text v0.9.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace github.com/ethereum-optimism/cannon v0.0.0 => ../../
replace github.com/ethereum-optimism/cannon/preimage v0.0.0 => ../../preimage
// We need to point to our local Unicorn clone for the shared object to be located correctly in all our Go commands.
// See https://github.com/unicorn-engine/unicorn/blob/7b8c63dfe650b5d4d2bf684526161971925e6350/bindings/go/unicorn/unicorn.go#L11
// The -L../../../ -lunicorn points to the unicorn root directory relative to the unicorn Go bindings.
// Run make libunicorn in the Cannon repo root to create this libunicorn.so dependency.
//
// If you are importing this as a library you will need to also point it to a unicorn clone with a `replace`
// in your `go.mod` (or `go.work`), or use `go build -ldflags="-L../path/to/my/unicorn/build -lunicorn`,
// or simply have it installed globally so Go can find it. The `replace` here will be ignored as library-user.
replace github.com/unicorn-engine/unicorn v0.0.0-20230207094436-7b8c63dfe650 => ../unicorn
This diff is collapsed.
This diff is collapsed.
package unicorntest
import (
"testing"
"github.com/stretchr/testify/require"
uc "github.com/unicorn-engine/unicorn/bindings/go/unicorn"
)
// TestUnicornDelaySlot test that unicorn works, and determine exactly how delay slots behave
func TestUnicornDelaySlot(t *testing.T) {
mu, err := NewUnicorn()
require.NoError(t, err)
defer mu.Close()
require.NoError(t, mu.MemMap(0, 4096))
require.NoError(t, mu.RegWrite(uc.MIPS_REG_RA, 420), "set RA to addr that is multiple of 4")
require.NoError(t, mu.MemWrite(0, []byte{0x03, 0xe0, 0x00, 0x08}), "jr $ra")
require.NoError(t, mu.MemWrite(4, []byte{0x20, 0x09, 0x0a, 0xFF}), "addi $t1 $r0 0x0aff")
require.NoError(t, mu.MemWrite(32, []byte{0x20, 0x09, 0x0b, 0xFF}), "addi $t1 $r0 0x0bff")
_, err = mu.HookAdd(uc.HOOK_CODE, func(mu uc.Unicorn, addr uint64, size uint32) {
t.Logf("addr: %08x", addr)
}, uint64(0), ^uint64(0))
require.NoError(t, err)
// stop at instruction in addr=4, the delay slot
require.NoError(t, mu.StartWithOptions(uint64(0), uint64(4), &uc.UcOptions{
Timeout: 0, // 0 to disable, value is in ms.
Count: 2,
}))
t1, err := mu.RegRead(uc.MIPS_REG_T1)
require.NoError(t, err)
require.NotEqual(t, uint64(0x0aff), t1, "delay slot should not execute")
pc, err := mu.RegRead(uc.MIPS_REG_PC)
require.NoError(t, err)
// unicorn is weird here: when entering a delay slot, it does not update the PC register by itself.
require.Equal(t, uint64(0), pc, "delay slot, no jump yet")
// now restart, but run two instructions, to include the delay slot
require.NoError(t, mu.StartWithOptions(uint64(0), ^uint64(0), &uc.UcOptions{
Timeout: 0, // 0 to disable, value is in ms.
Count: 2,
}))
pc, err = mu.RegRead(uc.MIPS_REG_PC)
require.NoError(t, err)
require.Equal(t, uint64(420), pc, "jumped after NOP delay slot")
t1, err = mu.RegRead(uc.MIPS_REG_T1)
require.NoError(t, err)
require.Equal(t, uint64(0x0aff), t1, "delay slot should execute")
require.NoError(t, mu.StartWithOptions(uint64(32), uint64(32+4), &uc.UcOptions{
Timeout: 0, // 0 to disable, value is in ms.
Count: 1,
}))
t1, err = mu.RegRead(uc.MIPS_REG_T1)
require.NoError(t, err)
require.Equal(t, uint64(0x0bff), t1, "regular instruction should work fine")
}
# cannon-extra
This is a collection of extra / legacy scripts. Cannon v0.3 and onwards reduces scope.
- The challenge/response bookkeeping contract part of the fraud proof is in development
separate from Cannon, hence moved into `extra` and deprecated.
- No usage of Merkle Patricia Tries (MPTs) for the VM state anymore.
- No `minigeth` anymore: see [`op-program`](https://github.com/ethereum-optimism/optimism/tree/develop/op-program) instead.
- No `mipigo` anymore: the ELF-loading and startup preparation is now part of the Go `mipsevm` toolset.
This diff is collapsed.
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.3;
import "./lib/Lib_Keccak256.sol";
import "./lib/Lib_MerkleTrie.sol";
import { Lib_BytesUtils } from "./lib/Lib_BytesUtils.sol";
contract MIPSMemory {
function AddTrieNode(bytes calldata anything) public {
Lib_MerkleTrie.GetTrie()[keccak256(anything)] = anything;
}
struct Preimage {
uint64 length;
mapping(uint => uint64) data;
}
mapping(bytes32 => Preimage) public preimage;
function MissingPreimageRevert(bytes32 outhash, uint offset) internal pure {
Lib_BytesUtils.revertWithHex(abi.encodePacked(outhash, offset));
}
function GetPreimageLength(bytes32 outhash) public view returns (uint32) {
uint64 data = preimage[outhash].length;
if (data == 0) {
MissingPreimageRevert(outhash, 0);
}
return uint32(data);
}
function GetPreimage(bytes32 outhash, uint offset) public view returns (uint32) {
uint64 data = preimage[outhash].data[offset];
if (data == 0) {
MissingPreimageRevert(outhash, offset);
}
return uint32(data);
}
function AddPreimage(bytes calldata anything, uint offset) public {
require(offset & 3 == 0, "offset must be 32-bit aligned");
uint len = anything.length;
require(offset < len, "offset can't be longer than input");
Preimage storage p = preimage[keccak256(anything)];
require(p.length == 0 || uint32(p.length) == len, "length is somehow wrong");
p.length = (1 << 32) | uint64(uint32(len));
p.data[offset] = (1 << 32) |
((len <= (offset+0) ? 0 : uint32(uint8(anything[offset+0]))) << 24) |
((len <= (offset+1) ? 0 : uint32(uint8(anything[offset+1]))) << 16) |
((len <= (offset+2) ? 0 : uint32(uint8(anything[offset+2]))) << 8) |
((len <= (offset+3) ? 0 : uint32(uint8(anything[offset+3]))) << 0);
}
// one per owner (at a time)
struct LargePreimage {
uint offset;
uint len;
uint32 data;
}
mapping(address => LargePreimage) public largePreimage;
// sadly due to soldiity limitations this can't be in the LargePreimage struct
mapping(address => uint64[25]) public largePreimageState;
function AddLargePreimageInit(uint offset) public {
require(offset & 3 == 0, "offset must be 32-bit aligned");
Lib_Keccak256.CTX memory c;
Lib_Keccak256.keccak_init(c);
largePreimageState[msg.sender] = c.A;
largePreimage[msg.sender].offset = offset;
largePreimage[msg.sender].len = 0;
}
// input 136 bytes, as many times as you'd like
// Uses about 500k gas, 3435 gas/byte
function AddLargePreimageUpdate(bytes calldata dat) public {
require(dat.length == 136, "update must be in multiples of 136");
// sha3_process_block
Lib_Keccak256.CTX memory c;
c.A = largePreimageState[msg.sender];
int offset = int(largePreimage[msg.sender].offset) - int(largePreimage[msg.sender].len);
if (offset >= 0 && offset < 136) {
largePreimage[msg.sender].data = fbo(dat, uint(offset));
}
Lib_Keccak256.sha3_xor_input(c, dat);
Lib_Keccak256.sha3_permutation(c);
largePreimageState[msg.sender] = c.A;
largePreimage[msg.sender].len += 136;
}
function AddLargePreimageFinal(bytes calldata idat) public view returns (bytes32, uint32, uint32) {
require(idat.length < 136, "final must be less than 136");
int offset = int(largePreimage[msg.sender].offset) - int(largePreimage[msg.sender].len);
require(offset < int(idat.length), "offset must be less than length");
Lib_Keccak256.CTX memory c;
c.A = largePreimageState[msg.sender];
bytes memory dat = new bytes(136);
for (uint i = 0; i < idat.length; i++) {
dat[i] = idat[i];
}
uint len = largePreimage[msg.sender].len + idat.length;
uint32 data = largePreimage[msg.sender].data;
if (offset >= 0) {
data = fbo(dat, uint(offset));
}
dat[135] = bytes1(uint8(0x80));
dat[idat.length] |= bytes1(uint8(0x1));
Lib_Keccak256.sha3_xor_input(c, dat);
Lib_Keccak256.sha3_permutation(c);
bytes32 outhash = Lib_Keccak256.get_hash(c);
require(len < 0x10000000, "max length is 32-bit");
return (outhash, uint32(len), data);
}
function AddLargePreimageFinalSaved(bytes calldata idat) public {
bytes32 outhash;
uint32 len;
uint32 data;
(outhash, len, data) = AddLargePreimageFinal(idat);
Preimage storage p = preimage[outhash];
require(p.length == 0 || uint32(p.length) == len, "length is somehow wrong");
require(largePreimage[msg.sender].offset < len, "offset is somehow beyond length");
p.length = (1 << 32) | uint64(len);
p.data[largePreimage[msg.sender].offset] = (1 << 32) | data;
}
function tb(uint32 dat) internal pure returns (bytes memory) {
bytes memory ret = new bytes(4);
ret[0] = bytes1(uint8(dat >> 24));
ret[1] = bytes1(uint8(dat >> 16));
ret[2] = bytes1(uint8(dat >> 8));
ret[3] = bytes1(uint8(dat >> 0));
return ret;
}
function fb(bytes memory dat) internal pure returns (uint32) {
require(dat.length == 4, "wrong length value");
uint32 ret = uint32(uint8(dat[0])) << 24 |
uint32(uint8(dat[1])) << 16 |
uint32(uint8(dat[2])) << 8 |
uint32(uint8(dat[3]));
return ret;
}
function fbo(bytes memory dat, uint offset) internal pure returns (uint32) {
uint32 ret = uint32(uint8(dat[offset+0])) << 24 |
uint32(uint8(dat[offset+1])) << 16 |
uint32(uint8(dat[offset+2])) << 8 |
uint32(uint8(dat[offset+3]));
return ret;
}
function WriteMemory(bytes32 stateHash, uint32 addr, uint32 value) public returns (bytes32) {
require(addr & 3 == 0, "write memory must be 32-bit aligned");
return Lib_MerkleTrie.update(tb(addr>>2), tb(value), stateHash);
}
function WriteBytes32(bytes32 stateHash, uint32 addr, bytes32 val) public returns (bytes32) {
for (uint32 i = 0; i < 32; i += 4) {
uint256 tv = uint256(val>>(224-(i*8)));
stateHash = WriteMemory(stateHash, addr+i, uint32(tv));
}
return stateHash;
}
// TODO: refactor writeMemory function to not need these
event DidStep(bytes32 stateHash);
function WriteMemoryWithReceipt(bytes32 stateHash, uint32 addr, uint32 value) public {
bytes32 newStateHash = WriteMemory(stateHash, addr, value);
emit DidStep(newStateHash);
}
function WriteBytes32WithReceipt(bytes32 stateHash, uint32 addr, bytes32 value) public {
bytes32 newStateHash = WriteBytes32(stateHash, addr, value);
emit DidStep(newStateHash);
}
// needed for preimage oracle
function ReadBytes32(bytes32 stateHash, uint32 addr) public view returns (bytes32) {
uint256 ret = 0;
for (uint32 i = 0; i < 32; i += 4) {
ret <<= 32;
ret |= uint256(ReadMemory(stateHash, addr+i));
}
return bytes32(ret);
}
function ReadMemory(bytes32 stateHash, uint32 addr) public view returns (uint32) {
require(addr & 3 == 0, "read memory must be 32-bit aligned");
// zero register is always 0
if (addr == 0xc0000000) {
return 0;
}
// MMIO preimage oracle
if (addr >= 0x31000000 && addr < 0x32000000) {
bytes32 pihash = ReadBytes32(stateHash, 0x30001000);
if (pihash == keccak256("")) {
// both the length and any data are 0
return 0;
}
if (addr == 0x31000000) {
return uint32(GetPreimageLength(pihash));
}
return GetPreimage(pihash, addr-0x31000004);
}
bool exists;
bytes memory value;
(exists, value) = Lib_MerkleTrie.get(tb(addr>>2), stateHash);
if (!exists) {
// this is uninitialized memory
return 0;
} else {
return fb(value);
}
}
}
\ No newline at end of file
// SPDX-License-Identifier: MIT
pragma solidity >0.5.0 <0.8.0;
/**
* @title Lib_BytesUtils
*/
library Lib_BytesUtils {
/**********************
* Internal Functions *
**********************/
function concat(
bytes memory _preBytes,
bytes memory _postBytes
)
internal
pure
returns (bytes memory)
{
bytes memory tempBytes;
assembly {
// Get a location of some free memory and store it in tempBytes as
// Solidity does for memory variables.
tempBytes := mload(0x40)
// Store the length of the first bytes array at the beginning of
// the memory for tempBytes.
let length := mload(_preBytes)
mstore(tempBytes, length)
// Maintain a memory counter for the current write location in the
// temp bytes array by adding the 32 bytes for the array length to
// the starting location.
let mc := add(tempBytes, 0x20)
// Stop copying when the memory counter reaches the length of the
// first bytes array.
let end := add(mc, length)
for {
// Initialize a copy counter to the start of the _preBytes data,
// 32 bytes into its memory.
let cc := add(_preBytes, 0x20)
} lt(mc, end) {
// Increase both counters by 32 bytes each iteration.
mc := add(mc, 0x20)
cc := add(cc, 0x20)
} {
// Write the _preBytes data into the tempBytes memory 32 bytes
// at a time.
mstore(mc, mload(cc))
}
// Add the length of _postBytes to the current length of tempBytes
// and store it as the new length in the first 32 bytes of the
// tempBytes memory.
length := mload(_postBytes)
mstore(tempBytes, add(length, mload(tempBytes)))
// Move the memory counter back from a multiple of 0x20 to the
// actual end of the _preBytes data.
mc := end
// Stop copying when the memory counter reaches the new combined
// length of the arrays.
end := add(mc, length)
for {
let cc := add(_postBytes, 0x20)
} lt(mc, end) {
mc := add(mc, 0x20)
cc := add(cc, 0x20)
} {
mstore(mc, mload(cc))
}
// Update the free-memory pointer by padding our last write location
// to 32 bytes: add 31 bytes to the end of tempBytes to move to the
// next 32 byte block, then round down to the nearest multiple of
// 32. If the sum of the length of the two arrays is zero then add
// one before rounding down to leave a blank 32 bytes (the length block with 0).
mstore(0x40, and(
add(add(end, iszero(add(length, mload(_preBytes)))), 31),
not(31) // Round down to the nearest 32 bytes.
))
}
return tempBytes;
}
function slice(
bytes memory _bytes,
uint256 _start,
uint256 _length
)
internal
pure
returns (bytes memory)
{
require(_length + 31 >= _length, "slice_overflow");
require(_start + _length >= _start, "slice_overflow");
require(_bytes.length >= _start + _length, "slice_outOfBounds");
bytes memory tempBytes;
assembly {
switch iszero(_length)
case 0 {
// Get a location of some free memory and store it in tempBytes as
// Solidity does for memory variables.
tempBytes := mload(0x40)
// The first word of the slice result is potentially a partial
// word read from the original array. To read it, we calculate
// the length of that partial word and start copying that many
// bytes into the array. The first word we copy will start with
// data we don't care about, but the last `lengthmod` bytes will
// land at the beginning of the contents of the new array. When
// we're done copying, we overwrite the full first word with
// the actual length of the slice.
let lengthmod := and(_length, 31)
// The multiplication in the next line is necessary
// because when slicing multiples of 32 bytes (lengthmod == 0)
// the following copy loop was copying the origin's length
// and then ending prematurely not copying everything it should.
let mc := add(add(tempBytes, lengthmod), mul(0x20, iszero(lengthmod)))
let end := add(mc, _length)
for {
// The multiplication in the next line has the same exact purpose
// as the one above.
let cc := add(add(add(_bytes, lengthmod), mul(0x20, iszero(lengthmod))), _start)
} lt(mc, end) {
mc := add(mc, 0x20)
cc := add(cc, 0x20)
} {
mstore(mc, mload(cc))
}
mstore(tempBytes, _length)
//update free-memory pointer
//allocating the array padded to 32 bytes like the compiler does now
mstore(0x40, and(add(mc, 31), not(31)))
}
//if we want a zero-length slice let's just return a zero-length array
default {
tempBytes := mload(0x40)
//zero out the 32 bytes slice we are about to return
//we need to do it because Solidity does not garbage collect
mstore(tempBytes, 0)
mstore(0x40, add(tempBytes, 0x20))
}
}
return tempBytes;
}
function slice(
bytes memory _bytes,
uint256 _start
)
internal
pure
returns (bytes memory)
{
if (_bytes.length - _start == 0) {
return bytes('');
}
return slice(_bytes, _start, _bytes.length - _start);
}
function toBytes32PadLeft(
bytes memory _bytes
)
internal
pure
returns (bytes32)
{
bytes32 ret;
uint256 len = _bytes.length <= 32 ? _bytes.length : 32;
assembly {
ret := shr(mul(sub(32, len), 8), mload(add(_bytes, 32)))
}
return ret;
}
function toBytes32(
bytes memory _bytes
)
internal
pure
returns (bytes32)
{
if (_bytes.length < 32) {
bytes32 ret;
assembly {
ret := mload(add(_bytes, 32))
}
return ret;
}
return abi.decode(_bytes,(bytes32)); // will truncate if input length > 32 bytes
}
function toUint256(
bytes memory _bytes
)
internal
pure
returns (uint256)
{
return uint256(toBytes32(_bytes));
}
function toUint24(bytes memory _bytes, uint256 _start) internal pure returns (uint24) {
require(_start + 3 >= _start, "toUint24_overflow");
require(_bytes.length >= _start + 3 , "toUint24_outOfBounds");
uint24 tempUint;
assembly {
tempUint := mload(add(add(_bytes, 0x3), _start))
}
return tempUint;
}
function toUint8(bytes memory _bytes, uint256 _start) internal pure returns (uint8) {
require(_start + 1 >= _start, "toUint8_overflow");
require(_bytes.length >= _start + 1 , "toUint8_outOfBounds");
uint8 tempUint;
assembly {
tempUint := mload(add(add(_bytes, 0x1), _start))
}
return tempUint;
}
function toAddress(bytes memory _bytes, uint256 _start) internal pure returns (address) {
require(_start + 20 >= _start, "toAddress_overflow");
require(_bytes.length >= _start + 20, "toAddress_outOfBounds");
address tempAddress;
assembly {
tempAddress := div(mload(add(add(_bytes, 0x20), _start)), 0x1000000000000000000000000)
}
return tempAddress;
}
function revertWithHex(
bytes memory _bytes
)
internal
pure
{
bytes memory node = Lib_BytesUtils.toNibbles(_bytes);
for (uint i = 0; i < node.length; i++) {
if (node[i] < bytes1(uint8(10))) {
node[i] = bytes1(uint8(node[i]) + uint8(0x30));
} else {
node[i] = bytes1(uint8(node[i]) + uint8(0x61-10));
}
}
revert(string(node));
}
function toNibbles(
bytes memory _bytes
)
internal
pure
returns (bytes memory)
{
bytes memory nibbles = new bytes(_bytes.length * 2);
for (uint256 i = 0; i < _bytes.length; i++) {
nibbles[i * 2] = _bytes[i] >> 4;
nibbles[i * 2 + 1] = bytes1(uint8(_bytes[i]) % 16);
}
return nibbles;
}
function fromNibbles(
bytes memory _bytes
)
internal
pure
returns (bytes memory)
{
bytes memory ret = new bytes(_bytes.length / 2);
for (uint256 i = 0; i < ret.length; i++) {
ret[i] = (_bytes[i * 2] << 4) | (_bytes[i * 2 + 1]);
}
return ret;
}
function equal(
bytes memory _bytes,
bytes memory _other
)
internal
pure
returns (bool)
{
return keccak256(_bytes) == keccak256(_other);
}
}
// SPDX-License-Identifier: MIT
pragma solidity >0.5.0 <0.8.0;
// https://chenglongma.com/10/simple-keccak/
// https://github.com/firefly/wallet/blob/master/source/libs/ethers/src/keccak256.c
library Lib_Keccak256 {
struct CTX {
uint64[25] A;
}
function get_round_constant(uint round) internal pure returns (uint64) {
uint64 result = 0;
uint8 roundInfo = uint8(0x7421587966164852535d4f3f26350c0e5579211f705e1a01 >> (round*8));
result |= (uint64(roundInfo) << (63-6)) & (1 << 63);
result |= (uint64(roundInfo) << (31-5)) & (1 << 31);
result |= (uint64(roundInfo) << (15-4)) & (1 << 15);
result |= (uint64(roundInfo) << (7-3)) & (1 << 7);
result |= (uint64(roundInfo) << (3-2)) & (1 << 3);
result |= (uint64(roundInfo) << (1-1)) & (1 << 1);
result |= (uint64(roundInfo) << (0-0)) & (1 << 0);
return result;
}
function keccak_theta_rho_pi(CTX memory c) internal pure {
uint64 C0 = c.A[0] ^ c.A[5] ^ c.A[10] ^ c.A[15] ^ c.A[20];
uint64 C1 = c.A[1] ^ c.A[6] ^ c.A[11] ^ c.A[16] ^ c.A[21];
uint64 C2 = c.A[2] ^ c.A[7] ^ c.A[12] ^ c.A[17] ^ c.A[22];
uint64 C3 = c.A[3] ^ c.A[8] ^ c.A[13] ^ c.A[18] ^ c.A[23];
uint64 C4 = c.A[4] ^ c.A[9] ^ c.A[14] ^ c.A[19] ^ c.A[24];
uint64 D0 = (C1 << 1) ^ (C1 >> 63) ^ C4;
uint64 D1 = (C2 << 1) ^ (C2 >> 63) ^ C0;
uint64 D2 = (C3 << 1) ^ (C3 >> 63) ^ C1;
uint64 D3 = (C4 << 1) ^ (C4 >> 63) ^ C2;
uint64 D4 = (C0 << 1) ^ (C0 >> 63) ^ C3;
c.A[0] ^= D0;
uint64 A1 = ((c.A[1] ^ D1) << 1) ^ ((c.A[1] ^ D1) >> (64-1));
c.A[1] = ((c.A[6] ^ D1) << 44) ^ ((c.A[6] ^ D1) >> (64-44));
c.A[6] = ((c.A[9] ^ D4) << 20) ^ ((c.A[9] ^ D4) >> (64-20));
c.A[9] = ((c.A[22] ^ D2) << 61) ^ ((c.A[22] ^ D2) >> (64-61));
c.A[22] = ((c.A[14] ^ D4) << 39) ^ ((c.A[14] ^ D4) >> (64-39));
c.A[14] = ((c.A[20] ^ D0) << 18) ^ ((c.A[20] ^ D0) >> (64-18));
c.A[20] = ((c.A[2] ^ D2) << 62) ^ ((c.A[2] ^ D2) >> (64-62));
c.A[2] = ((c.A[12] ^ D2) << 43) ^ ((c.A[12] ^ D2) >> (64-43));
c.A[12] = ((c.A[13] ^ D3) << 25) ^ ((c.A[13] ^ D3) >> (64-25));
c.A[13] = ((c.A[19] ^ D4) << 8) ^ ((c.A[19] ^ D4) >> (64-8));
c.A[19] = ((c.A[23] ^ D3) << 56) ^ ((c.A[23] ^ D3) >> (64-56));
c.A[23] = ((c.A[15] ^ D0) << 41) ^ ((c.A[15] ^ D0) >> (64-41));
c.A[15] = ((c.A[4] ^ D4) << 27) ^ ((c.A[4] ^ D4) >> (64-27));
c.A[4] = ((c.A[24] ^ D4) << 14) ^ ((c.A[24] ^ D4) >> (64-14));
c.A[24] = ((c.A[21] ^ D1) << 2) ^ ((c.A[21] ^ D1) >> (64-2));
c.A[21] = ((c.A[8] ^ D3) << 55) ^ ((c.A[8] ^ D3) >> (64-55));
c.A[8] = ((c.A[16] ^ D1) << 45) ^ ((c.A[16] ^ D1) >> (64-45));
c.A[16] = ((c.A[5] ^ D0) << 36) ^ ((c.A[5] ^ D0) >> (64-36));
c.A[5] = ((c.A[3] ^ D3) << 28) ^ ((c.A[3] ^ D3) >> (64-28));
c.A[3] = ((c.A[18] ^ D3) << 21) ^ ((c.A[18] ^ D3) >> (64-21));
c.A[18] = ((c.A[17] ^ D2) << 15) ^ ((c.A[17] ^ D2) >> (64-15));
c.A[17] = ((c.A[11] ^ D1) << 10) ^ ((c.A[11] ^ D1) >> (64-10));
c.A[11] = ((c.A[7] ^ D2) << 6) ^ ((c.A[7] ^ D2) >> (64-6));
c.A[7] = ((c.A[10] ^ D0) << 3) ^ ((c.A[10] ^ D0) >> (64-3));
c.A[10] = A1;
}
function keccak_chi(CTX memory c) internal pure {
uint i;
uint64 A0;
uint64 A1;
uint64 A2;
uint64 A3;
uint64 A4;
for (i = 0; i < 25; i+=5) {
A0 = c.A[0 + i];
A1 = c.A[1 + i];
A2 = c.A[2 + i];
A3 = c.A[3 + i];
A4 = c.A[4 + i];
c.A[0 + i] ^= ~A1 & A2;
c.A[1 + i] ^= ~A2 & A3;
c.A[2 + i] ^= ~A3 & A4;
c.A[3 + i] ^= ~A4 & A0;
c.A[4 + i] ^= ~A0 & A1;
}
}
function keccak_init(CTX memory c) internal pure {
// is this needed?
uint i;
for (i = 0; i < 25; i++) {
c.A[i] = 0;
}
}
function sha3_xor_input(CTX memory c, bytes memory dat) internal pure {
for (uint i = 0; i < 17; i++) {
uint bo = i*8;
c.A[i] ^= uint64(uint8(dat[bo+7])) << 56 |
uint64(uint8(dat[bo+6])) << 48 |
uint64(uint8(dat[bo+5])) << 40 |
uint64(uint8(dat[bo+4])) << 32 |
uint64(uint8(dat[bo+3])) << 24 |
uint64(uint8(dat[bo+2])) << 16 |
uint64(uint8(dat[bo+1])) << 8 |
uint64(uint8(dat[bo+0])) << 0;
}
}
function sha3_permutation(CTX memory c) internal pure {
uint round;
for (round = 0; round < 24; round++) {
keccak_theta_rho_pi(c);
keccak_chi(c);
// keccak_iota
c.A[0] ^= get_round_constant(round);
}
}
// https://stackoverflow.com/questions/2182002/convert-big-endian-to-little-endian-in-c-without-using-provided-func
function flip(uint64 val) internal pure returns (uint64) {
val = ((val << 8) & 0xFF00FF00FF00FF00 ) | ((val >> 8) & 0x00FF00FF00FF00FF );
val = ((val << 16) & 0xFFFF0000FFFF0000 ) | ((val >> 16) & 0x0000FFFF0000FFFF );
return (val << 32) | (val >> 32);
}
function get_hash(CTX memory c) internal pure returns (bytes32) {
return bytes32((uint256(flip(c.A[0])) << 192) |
(uint256(flip(c.A[1])) << 128) |
(uint256(flip(c.A[2])) << 64) |
(uint256(flip(c.A[3])) << 0));
}
}
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
// SPDX-License-Identifier: MIT
pragma solidity >0.5.0 <0.8.0;
pragma experimental ABIEncoderV2;
/**
* @title Lib_RLPWriter
* @author Bakaoh (with modifications)
*/
library Lib_RLPWriter {
/**********************
* Internal Functions *
**********************/
/**
* RLP encodes a byte string.
* @param _in The byte string to encode.
* @return The RLP encoded string in bytes.
*/
function writeBytes(
bytes memory _in
)
internal
pure
returns (
bytes memory
)
{
bytes memory encoded;
if (_in.length == 1 && uint8(_in[0]) < 128) {
encoded = _in;
} else {
encoded = abi.encodePacked(_writeLength(_in.length, 128), _in);
}
return encoded;
}
/**
* RLP encodes a list of RLP encoded byte byte strings.
* @param _in The list of RLP encoded byte strings.
* @return The RLP encoded list of items in bytes.
*/
function writeList(
bytes[] memory _in
)
internal
pure
returns (
bytes memory
)
{
bytes memory list = _flatten(_in);
return abi.encodePacked(_writeLength(list.length, 192), list);
}
/**
* RLP encodes a string.
* @param _in The string to encode.
* @return The RLP encoded string in bytes.
*/
function writeString(
string memory _in
)
internal
pure
returns (
bytes memory
)
{
return writeBytes(bytes(_in));
}
/**
* RLP encodes an address.
* @param _in The address to encode.
* @return The RLP encoded address in bytes.
*/
function writeAddress(
address _in
)
internal
pure
returns (
bytes memory
)
{
return writeBytes(abi.encodePacked(_in));
}
/**
* RLP encodes a bytes32 value.
* @param _in The bytes32 to encode.
* @return _out The RLP encoded bytes32 in bytes.
*/
function writeBytes32(
bytes32 _in
)
internal
pure
returns (
bytes memory _out
)
{
return writeBytes(abi.encodePacked(_in));
}
/**
* RLP encodes a uint.
* @param _in The uint256 to encode.
* @return The RLP encoded uint256 in bytes.
*/
function writeUint(
uint256 _in
)
internal
pure
returns (
bytes memory
)
{
return writeBytes(_toBinary(_in));
}
/**
* RLP encodes a bool.
* @param _in The bool to encode.
* @return The RLP encoded bool in bytes.
*/
function writeBool(
bool _in
)
internal
pure
returns (
bytes memory
)
{
bytes memory encoded = new bytes(1);
encoded[0] = (_in ? bytes1(0x01) : bytes1(0x80));
return encoded;
}
/*********************
* Private Functions *
*********************/
/**
* Encode the first byte, followed by the `len` in binary form if `length` is more than 55.
* @param _len The length of the string or the payload.
* @param _offset 128 if item is string, 192 if item is list.
* @return RLP encoded bytes.
*/
function _writeLength(
uint256 _len,
uint256 _offset
)
private
pure
returns (
bytes memory
)
{
bytes memory encoded;
if (_len < 56) {
encoded = new bytes(1);
encoded[0] = byte(uint8(_len) + uint8(_offset));
} else {
uint256 lenLen;
uint256 i = 1;
while (_len / i != 0) {
lenLen++;
i *= 256;
}
encoded = new bytes(lenLen + 1);
encoded[0] = byte(uint8(lenLen) + uint8(_offset) + 55);
for(i = 1; i <= lenLen; i++) {
encoded[i] = byte(uint8((_len / (256**(lenLen-i))) % 256));
}
}
return encoded;
}
/**
* Encode integer in big endian binary form with no leading zeroes.
* @notice TODO: This should be optimized with assembly to save gas costs.
* @param _x The integer to encode.
* @return RLP encoded bytes.
*/
function _toBinary(
uint256 _x
)
private
pure
returns (
bytes memory
)
{
bytes memory b = abi.encodePacked(_x);
uint256 i = 0;
for (; i < 32; i++) {
if (b[i] != 0) {
break;
}
}
bytes memory res = new bytes(32 - i);
for (uint256 j = 0; j < res.length; j++) {
res[j] = b[i++];
}
return res;
}
/**
* Copies a piece of memory to another location.
* @notice From: https://github.com/Arachnid/solidity-stringutils/blob/master/src/strings.sol.
* @param _dest Destination location.
* @param _src Source location.
* @param _len Length of memory to copy.
*/
function _memcpy(
uint256 _dest,
uint256 _src,
uint256 _len
)
private
pure
{
uint256 dest = _dest;
uint256 src = _src;
uint256 len = _len;
for(; len >= 32; len -= 32) {
assembly {
mstore(dest, mload(src))
}
dest += 32;
src += 32;
}
uint256 mask = 256 ** (32 - len) - 1;
assembly {
let srcpart := and(mload(src), not(mask))
let destpart := and(mload(dest), mask)
mstore(dest, or(destpart, srcpart))
}
}
/**
* Flattens a list of byte strings into one byte string.
* @notice From: https://github.com/sammayo/solidity-rlp-encoder/blob/master/RLPEncode.sol.
* @param _list List of byte strings to flatten.
* @return The flattened byte string.
*/
function _flatten(
bytes[] memory _list
)
private
pure
returns (
bytes memory
)
{
if (_list.length == 0) {
return new bytes(0);
}
uint256 len;
uint256 i = 0;
for (; i < _list.length; i++) {
len += _list[i].length;
}
bytes memory flattened = new bytes(len);
uint256 flattenedPtr;
assembly { flattenedPtr := add(flattened, 0x20) }
for(i = 0; i < _list.length; i++) {
bytes memory item = _list[i];
uint256 listPtr;
assembly { listPtr := add(item, 0x20)}
_memcpy(flattenedPtr, listPtr, item.length);
flattenedPtr += _list[i].length;
}
return flattened;
}
}
#!/usr/bin/env bash
# The following variables can be overridden as environment variables:
# * BLOCK (block whose transition will be challenged)
# * SKIP_NODE (skip forking a node, useful if you've already forked a node)
#
# Example usage:
# SKIP_NODE=1 BLOCK=13284469 ./demo/challenge_fault.sh
# --- DOC ----------------------------------------------------------------------
# Unlike the simple scenario (cf. challenge_simple.sh), in this
# challenge-response scenario we use the correct block data (preimages) and
# instead use the `OUTPUTFAULT` environment variable to request a fault in the
# challenger's execution, making his challenge invalid.
#
# The "fault" in question is a behaviour hardcoded in `mipsevm` (Unicorn mode
# only) which triggers when the `OUTPUTFAULT` env var is set: when writing to
# MIPS address 0x30000804 (address where the output hash is written at the end
# of execution), it will write a wrong value instead.
#
# Alternatively, if `REGFAULT` is set, it should contain a MIPS execution step
# number and causes the MIPS register V0 to be set to a bogus value at the given
# execution step. (Just like before, this behaviour is hardcoded in `mipsevm` in
# Unicorn mode and triggers when `REGFAULT` is set.)
#
# This is much slower than the previous scenario because:
#
# - Since we write to the output hash at the end of execution, we will execute ~
# `log(n) * 3/4 * n` MIPS steps (where `n` = number of steps in full
# execution) vs `log(n) * 1/4 * n`in the previous example. (This is the
# difference of having the fault occur in the first vs (one of) the last
# steps.)
#
# - The challenged block contains almost 4x as many transactions as the original
# (8.5M vs 30M gas).
# --- SCRIPT SETUP -------------------------------------------------------------
shout() {
echo ""
echo "----------------------------------------"
echo "$1"
echo "----------------------------------------"
echo ""
}
# Exit if any command fails.
set -e
exit_trap() {
# Print an error if the last command failed
# (in which case the script is exiting because of set -e).
[[ $? == 0 ]] && return
echo "----------------------------------------"
echo "EARLY EXIT: SCRIPT FAILED"
echo "----------------------------------------"
# Kill (send SIGTERM) to the whole process group, also killing
# any background processes.
# I think the trap command resets SIGTERM before resending it to the whole
# group. (cf. https://stackoverflow.com/a/2173421)
trap - SIGTERM && kill -- -$$
}
trap "exit_trap" SIGINT SIGTERM EXIT
# --- BOOT MAINNET FORK --------------------------------------------------------
if [[ ! "$SKIP_NODE" ]]; then
NODE_LOG="challenge_fault_node.log"
shout "BOOTING MAINNET FORK NODE IN BACKGROUND (LOG: $NODE_LOG)"
# get directory containing this file
SCRIPT_DIR=$(dirname "$(readlink -f "$0")")
# run a hardhat mainnet fork node
"$SCRIPT_DIR/forked_node.sh" > "$NODE_LOG" 2>&1 &
# give the node some time to boot up
sleep 10
fi
# --- CHALLENGE SETUP ----------------------------------------------------------
# hardhat network to use
NETWORK=${NETWORK:-l1}
export NETWORK
# block whose transition will be challenged
# this variable is read by challenge.js, respond.js and assert.js
BLOCK=${BLOCK:-13284491}
export BLOCK
# challenge ID, read by respond.js and assert.js
export ID=0
# clear data from previous runs
mkdir -p /tmp/cannon /tmp/cannon_fault && rm -rf /tmp/cannon/* /tmp/cannon_fault/*
# stored in /tmp/cannon/golden.json
shout "GENERATING INITIAL MEMORY STATE CHECKPOINT"
mipsevm/mipsevm --outputGolden
shout "DEPLOYING CONTRACTS"
npx hardhat run scripts/deploy.js --network $NETWORK
# challenger will use same initial memory checkpoint and deployed contracts
cp /tmp/cannon/{golden,deployed}.json /tmp/cannon_fault/
shout "FETCHING PREIMAGES FOR REAL BLOCK"
minigeth/go-ethereum $BLOCK
shout "COMPUTING REAL MIPS FINAL MEMORY CHECKPOINT"
mipsevm/mipsevm --blockNumber=$BLOCK
# these are the preimages for the real block (but go into a different basedir)
shout "FETCHING PREIMAGES FOR FAULTY BLOCK"
BASEDIR=/tmp/cannon_fault minigeth/go-ethereum $BLOCK
# since the computation includes a fault, the output file will be different than
# for the real block
shout "COMPUTE FAKE MIPS CHECKPOINT"
OUTPUTFAULT=1 BASEDIR=/tmp/cannon_fault mipsevm/mipsevm --blockNumber=$BLOCK
# alternatively, to inject a fault in registers instead of memory
# REGFAULT=13240000 BASEDIR=/tmp/cannon_fault mipsevm/mipsevm --blockNumber=$BLOCK
# --- BINARY SEARCH ------------------------------------------------------------
shout "STARTING CHALLENGE"
BASEDIR=/tmp/cannon_fault npx hardhat run scripts/challenge.js --network $NETWORK
shout "BINARY SEARCH"
for i in {1..25}; do
echo ""
echo "--- STEP $i / 25 --"
echo ""
OUTPUTFAULT=1 BASEDIR=/tmp/cannon_fault CHALLENGER=1 npx hardhat run scripts/respond.js --network $NETWORK
npx hardhat run scripts/respond.js --network $NETWORK
done
# --- SINGLE STEP EXECUTION ----------------------------------------------------
shout "ASSERTING AS CHALLENGER (should fail)"
set +e # this should fail!
BASEDIR=/tmp/cannon_fault CHALLENGER=1 npx hardhat run scripts/assert.js --network $NETWORK
set -e
shout "ASSERTING AS DEFENDER (should pass)"
npx hardhat run scripts/assert.js --network $NETWORK
#!/usr/bin/env bash
# The following variables can be overridden as environment variables:
# * BLOCK (block whose transition will be challenged)
# * WRONG_BLOCK (block number used by challenger)
# * SKIP_NODE (skip forking a node, useful if you've already forked a node)
#
# Example usage:
# SKIP_NODE=1 BLOCK=13284469 WRONG_BLOCK=13284491 ./demo/challenge_simple.sh
# --- DOC ----------------------------------------------------------------------
# In this example, the challenger will challenge the transition from a block
# (`BLOCK`), but pretends that chain state before another block (`WRONG_BLOCK`)
# is the state before the challenged block. Consequently, the challenger will
# disagree with the defender on every single step of the challenge game, and the
# single step to execute will be the very first MIPS instruction executed. The
# reason is that the initial MIPS state Merkle root is stored on-chain, and
# immediately modified to reflect the fact that the input hash for the block is
# written at address 0x3000000.
#
# (The input hash is automatically validated against the blockhash, so note that
# in this demo the challenger has to provide the correct (`BLOCK`) input hash to
# the `initiateChallenge` function of `Challenge.sol`, but will execute as
# though the input hash was the one derived from `WRONG_BLOCK`.)
#
# Because the challenger uses the wrong inputs, it will assert a post-state
# (Merkle root) for the first MIPS instruction that has the wrong input hash at
# 0x3000000. Hence, the challenge will fail.
# --- SCRIPT SETUP -------------------------------------------------------------
shout() {
echo ""
echo "----------------------------------------"
echo "$1"
echo "----------------------------------------"
echo ""
}
# Exit if any command fails.
set -e
exit_trap() {
# Print an error if the last command failed
# (in which case the script is exiting because of set -e).
[[ $? == 0 ]] && return
echo "----------------------------------------"
echo "EARLY EXIT: SCRIPT FAILED"
echo "----------------------------------------"
# Kill (send SIGTERM) to the whole process group, also killing
# any background processes.
# I think the trap command resets SIGTERM before resending it to the whole
# group. (cf. https://stackoverflow.com/a/2173421)
trap - SIGTERM && kill -- -$$
}
trap "exit_trap" SIGINT SIGTERM EXIT
# --- BOOT MAINNET FORK --------------------------------------------------------
if [[ ! "$SKIP_NODE" ]]; then
NODE_LOG="challenge_simple_node.log"
shout "BOOTING MAINNET FORK NODE IN BACKGROUND (LOG: $NODE_LOG)"
# get directory containing this file
SCRIPT_DIR=$(dirname "$(readlink -f "$0")")
# run a hardhat mainnet fork node
"$SCRIPT_DIR/forked_node.sh" > "$NODE_LOG" 2>&1 &
# give the node some time to boot up
sleep 10
fi
# --- CHALLENGE SETUP ----------------------------------------------------------
# hardhat network to use
NETWORK=${NETWORK:-l1}
export NETWORK
# challenge ID, read by respond.js and assert.js
export ID=0
# block whose transition will be challenged
# this variable is read by challenge.js, respond.js and assert.js
BLOCK=${BLOCK:-13284469}
export BLOCK
# block whose pre-state is used by the challenger instead of the challenged block's pre-state
WRONG_BLOCK=${WRONG_BLOCK:-13284491}
# clear data from previous runs
mkdir -p /tmp/cannon /tmp/cannon_fault && rm -rf /tmp/cannon/* /tmp/cannon_fault/*
# stored in /tmp/cannon/golden.json
shout "GENERATING INITIAL MEMORY STATE CHECKPOINT"
mipsevm/mipsevm --outputGolden
shout "DEPLOYING CONTRACTS"
npx hardhat run scripts/deploy.js --network $NETWORK
# challenger will use same initial memory checkpoint and deployed contracts
cp /tmp/cannon/{golden,deployed}.json /tmp/cannon_fault/
shout "FETCHING PREIMAGES FOR REAL BLOCK"
minigeth/go-ethereum $BLOCK
shout "COMPUTING REAL MIPS FINAL MEMORY CHECKPOINT"
mipsevm/mipsevm --blockNumber=$BLOCK
shout "FETCHING PREIMAGES FOR WRONG BLOCK"
BASEDIR=/tmp/cannon_fault minigeth/go-ethereum $WRONG_BLOCK
shout "COMPUTING FAKE MIPS FINAL MEMORY CHECKPOINT"
BASEDIR=/tmp/cannon_fault mipsevm/mipsevm --blockNumber=$WRONG_BLOCK
# pretend the wrong block's input, checkpoints and preimages are the right block's
ln -s /tmp/cannon_fault/0_$WRONG_BLOCK /tmp/cannon_fault/0_$BLOCK
# --- BINARY SEARCH ------------------------------------------------------------
shout "STARTING CHALLENGE"
BASEDIR=/tmp/cannon_fault npx hardhat run scripts/challenge.js --network $NETWORK
shout "BINARY SEARCH"
for i in {1..23}; do
echo ""
echo "--- STEP $i / 23 ---"
echo ""
BASEDIR=/tmp/cannon_fault CHALLENGER=1 npx hardhat run scripts/respond.js --network $NETWORK
npx hardhat run scripts/respond.js --network $NETWORK
done
# --- SINGLE STEP EXECUTION ----------------------------------------------------
shout "ASSERTING AS CHALLENGER (should fail)"
set +e # this should fail!
BASEDIR=/tmp/cannon_fault CHALLENGER=1 npx hardhat run scripts/assert.js --network $NETWORK
set -e
shout "ASSERTING AS DEFENDER (should pass)"
npx hardhat run scripts/assert.js --network $NETWORK
#!/usr/bin/env bash
# This runs a hardhat node forked from mainnet at the specified block.
# You need to run this in a separate terminal (or in the background)
# before running challenge_simple.sh or challenge_fault.sh.
#
# RPC_URL and FORK_BLOCK can be overwritten as environment variables. If not
# provided, defaults are used.
# Uncomment this line if you receive the error:
# Error HH604: Error running JSON-RPC server: error:0308010C:digital envelope routines::unsupported
# export NODE_OPTIONS=--openssl-legacy-provider
RPC_URL=${RPC_URL:-"https://mainnet.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161"}
# block at which to fork mainnet
FORK_BLOCK=${FORK_BLOCK:-13284495}
# testing on hardhat (forked mainnet, a few blocks ahead of challenges in
# challenge_simple.sh and challenge_fault.sh)
npx hardhat node --fork $RPC_URL --fork-block-number $FORK_BLOCK
#!/usr/bin/env bash
# Similar to challenge_fault.sh, but runs on L2!
SCRIPT_DIR=$(dirname "$(readlink -f "$0")")
export ETH_RPC_URL=http://127.0.0.1:9545/
export BLOCK=$(cast block-number)
SKIP_NODE=1 NETWORK=l2 "$SCRIPT_DIR/challenge_fault.sh"
#!/usr/bin/env bash
# Similar to challenge_simple.sh, but runs on L2!
SCRIPT_DIR=$(dirname "$(readlink -f "$0")")
export ETH_RPC_URL=http://127.0.0.1:9545/
export BLOCK=$(cast block-number)
export WRONG_BLOCK=$(expr $BLOCK - 1)
SKIP_NODE=1 NETWORK=l2 "$SCRIPT_DIR/challenge_simple.sh"
const { deployed, getTrieNodesForCall, getTrieAtStep } = require("../scripts/lib")
async function main() {
let [c, m, mm] = await deployed()
const challengeId = parseInt(process.env.ID)
const blockNumberN = parseInt(process.env.BLOCK)
const isChallenger = process.env.CHALLENGER == "1"
let step = (await c.getStepNumber(challengeId)).toNumber()
console.log("searching step", step, "at block", blockNumberN)
if (await c.isSearching(challengeId)) {
console.log("search is NOT done")
return
}
let cdat
if (isChallenger) {
// challenger declare victory
cdat = c.interface.encodeFunctionData("confirmStateTransition", [challengeId])
} else {
// defender declare victory
// note: not always possible
cdat = c.interface.encodeFunctionData("denyStateTransition", [challengeId])
}
let startTrie = getTrieAtStep(blockNumberN, step)
let finalTrie = getTrieAtStep(blockNumberN, step+1)
let preimages = Object.assign({}, startTrie['preimages'], finalTrie['preimages']);
let nodes = await getTrieNodesForCall(c, c.address, cdat, preimages)
for (n of nodes) {
await mm.AddTrieNode(n)
}
let ret
if (isChallenger) {
ret = await c.confirmStateTransition(challengeId)
} else {
ret = await c.denyStateTransition(challengeId)
}
let receipt = await ret.wait()
console.log(receipt.events.map((x) => x.event))
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
const fs = require("fs")
const { basedir, deployed, getBlockRlp, getTrieNodesForCall } = require("../scripts/lib")
async function main() {
let [c, m, mm] = await deployed()
const blockNumberN = parseInt(process.env.BLOCK)
if (isNaN(blockNumberN)) {
throw "usage: BLOCK=<number> npx hardhat run challenge.js"
}
console.log("challenging block number", blockNumberN)
// sadly this doesn't work on hosthat
const blockNp1 = await network.provider.send("eth_getBlockByNumber", ["0x"+(blockNumberN+1).toString(16), false])
console.log(blockNp1)
const blockNp1Rlp = getBlockRlp(blockNp1)
console.log(c.address, m.address, mm.address)
// TODO: move this to lib, it's shared with the test
let startTrie = JSON.parse(fs.readFileSync(basedir+"/golden.json"))
const assertionRootBinary = fs.readFileSync(basedir+"/0_"+blockNumberN.toString()+"/output")
var assertionRoot = "0x"
for (var i=0; i<32; i++) {
hex = assertionRootBinary[i].toString(16);
assertionRoot += ("0"+hex).slice(-2);
}
console.log("asserting root", assertionRoot)
let finalTrie = JSON.parse(fs.readFileSync(basedir+"/0_"+blockNumberN.toString()+"/checkpoint_final.json"))
let preimages = Object.assign({}, startTrie['preimages'], finalTrie['preimages']);
const finalSystemState = finalTrie['root']
let args = [blockNumberN, blockNp1Rlp, assertionRoot, finalSystemState, finalTrie['step']]
let cdat = c.interface.encodeFunctionData("initiateChallenge", args)
let nodes = await getTrieNodesForCall(c, c.address, cdat, preimages)
// run "on chain"
for (n of nodes) {
await mm.AddTrieNode(n)
}
// TODO: Setting the gas limit explicitly here shouldn't be necessary, for some
// weird reason (to be investigated), it is for L2.
// let ret = await c.initiateChallenge(...args)
let ret = await c.initiateChallenge(...args, { gasLimit: 10_000_000 })
let receipt = await ret.wait()
// ChallengeCreated event
let challengeId = receipt.events[0].args['challengeId'].toNumber()
console.log("new challenge with id", challengeId)
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
const { deploy } = require("../scripts/lib")
const fs = require("fs")
async function main() {
let [c, m, mm] = await deploy()
let json = {
"Challenge": c.address,
"MIPS": m.address,
"MIPSMemory": mm.address,
}
console.log("deployed", json)
fs.writeFileSync("/tmp/cannon/deployed.json", JSON.stringify(json))
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
const fs = require("fs")
const rlp = require('rlp')
const child_process = require("child_process")
const basedir = process.env.BASEDIR == undefined ? "/tmp/cannon" : process.env.BASEDIR
async function deploy() {
const MIPS = await ethers.getContractFactory("MIPS")
const m = await MIPS.deploy()
const mm = await ethers.getContractAt("MIPSMemory", await m.m())
let startTrie = JSON.parse(fs.readFileSync(basedir+"/golden.json"))
let goldenRoot = startTrie["root"]
console.log("goldenRoot is", goldenRoot)
const Challenge = await ethers.getContractFactory("Challenge")
const c = await Challenge.deploy(m.address, goldenRoot)
return [c,m,mm]
}
function getBlockRlp(block) {
let dat = [
block['parentHash'],
block['sha3Uncles'],
block['miner'],
block['stateRoot'],
block['transactionsRoot'],
block['receiptsRoot'],
block['logsBloom'],
block['difficulty'],
block['number'],
block['gasLimit'],
block['gasUsed'],
block['timestamp'],
block['extraData'],
block['mixHash'],
block['nonce'],
];
// post london
if (block['baseFeePerGas'] !== undefined) {
dat.push(block['baseFeePerGas'])
}
dat = dat.map(x => (x == "0x0") ? "0x" : x)
//console.log(dat)
let rdat = rlp.encode(dat)
if (ethers.utils.keccak256(rdat) != block['hash']) {
throw "block hash doesn't match"
}
return rdat
}
async function deployed() {
let addresses = JSON.parse(fs.readFileSync(basedir+"/deployed.json"))
const c = await ethers.getContractAt("Challenge", addresses["Challenge"])
const m = await ethers.getContractAt("MIPS", addresses["MIPS"])
const mm = await ethers.getContractAt("MIPSMemory", addresses["MIPSMemory"])
return [c,m,mm]
}
class MissingHashError extends Error {
constructor(hash, offset) {
super("hash is missing")
this.hash = hash
this.offset = offset
}
}
async function getTrieNodesForCall(c, caddress, cdat, preimages) {
let nodes = []
while (1) {
try {
// TODO: make this eth call?
// needs something like initiateChallengeWithTrieNodesj
let calldata = c.interface.encodeFunctionData("callWithTrieNodes", [caddress, cdat, nodes])
ret = await ethers.provider.call({
to:c.address,
data:calldata
});
break
} catch(e) {
let missing = e.toString().split("'")[1]
if (missing == undefined) {
// other kind of error from HTTPProvider
missing = e.error.message.toString().split("execution reverted: ")[1]
}
if (missing !== undefined && missing.length == 64) {
console.log("requested node", missing)
let node = preimages["0x"+missing]
if (node === undefined) {
throw("node not found")
}
const bin = Uint8Array.from(Buffer.from(node, 'base64').toString('binary'), c => c.charCodeAt(0))
nodes.push(bin)
continue
} else if (missing !== undefined && missing.length == 128) {
let hash = missing.slice(0, 64)
let offset = parseInt(missing.slice(64, 128), 16)
console.log("requested hash oracle", hash, offset)
throw new MissingHashError(hash, offset)
} else {
console.log(e)
break
}
}
}
return nodes
}
function getTrieAtStep(blockNumberN, step) {
const fn = basedir+"/0_"+blockNumberN.toString()+"/checkpoint_"+step.toString()+".json"
if (!fs.existsSync(fn)) {
console.log("running mipsevm")
child_process.execSync("mipsevm/mipsevm --blockNumber="+blockNumberN.toString()+" --target="+step.toString(), {stdio: 'inherit'})
}
return JSON.parse(fs.readFileSync(fn))
}
async function writeMemory(mm, root, addr, data, bytes32=false) {
if (bytes32) {
ret = await mm.WriteBytes32WithReceipt(root, addr, data)
} else {
ret = await mm.WriteMemoryWithReceipt(root, addr, data)
}
const receipt = await ret.wait()
for (l of receipt.logs) {
if (l.topics[0] == "0x86b89b5c9818dbbf520dd979a5f250d357508fe11b9511d4a43fd9bc6aa1be70") {
root = l.data
}
}
console.log("new hash", root)
return root
}
module.exports = { basedir, deploy, deployed, getTrieNodesForCall, getBlockRlp, getTrieAtStep, writeMemory, MissingHashError }
const fs = require("fs")
const { deployed, getTrieNodesForCall, getTrieAtStep } = require("../scripts/lib")
async function main() {
let [c, m, mm] = await deployed()
const challengeId = parseInt(process.env.ID)
const blockNumberN = parseInt(process.env.BLOCK)
const isChallenger = process.env.CHALLENGER == "1"
let step = (await c.getStepNumber(challengeId)).toNumber()
console.log("searching step", step, "at block", blockNumberN)
if (!(await c.isSearching(challengeId))) {
console.log("search is done")
return
}
// see if it's proposed or not
const proposed = await c.getProposedState(challengeId)
const isProposing = proposed == "0x0000000000000000000000000000000000000000000000000000000000000000"
if (isProposing != isChallenger) {
console.log("bad challenger state")
return
}
console.log("isProposing", isProposing)
let thisTrie = getTrieAtStep(blockNumberN, step)
const root = thisTrie['root']
console.log("new root", root)
let ret
if (isProposing) {
ret = await c.proposeState(challengeId, root)
} else {
ret = await c.respondState(challengeId, root)
}
let receipt = await ret.wait()
console.log("done", receipt.blockNumber)
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
\ No newline at end of file
const { expect } = require("chai")
const fs = require("fs")
const { deploy, getTrieNodesForCall } = require("../scripts/lib")
// This test needs preimages to run correctly.
// It is skipped when running `make test_contracts`, but can be run with `make test_challenge`.
describe("Challenge contract", function () {
if (!fs.existsSync("/tmp/cannon/golden.json")) {
console.log("golden file doesn't exist, skipping test")
return
}
beforeEach(async function () {
[c, m, mm] = await deploy()
})
it("challenge contract deploys", async function() {
console.log("Challenge deployed at", c.address)
})
it("initiate challenge", async function() {
// TODO: is there a better way to get the "HardhatNetworkProvider"?
const hardhat = network.provider._wrapped._wrapped._wrapped._wrapped._wrapped
const blockchain = hardhat._node._blockchain
// get data
const blockNumberN = (await ethers.provider.getBlockNumber())-2
const blockNp1 = blockchain._data._blocksByNumber.get(blockNumberN+1)
const blockNp1Rlp = blockNp1.header.serialize()
const assertionRoot = "0x9e0261efe4509912b8862f3d45a0cb8404b99b239247df9c55871bd3844cebbd"
let startTrie = JSON.parse(fs.readFileSync("/tmp/cannon/golden.json"))
let finalTrie = JSON.parse(fs.readFileSync("/tmp/cannon/0_13284469/checkpoint_final.json"))
let preimages = Object.assign({}, startTrie['preimages'], finalTrie['preimages']);
const finalSystemState = finalTrie['root']
let args = [blockNumberN, blockNp1Rlp, assertionRoot, finalSystemState, finalTrie['step']]
let cdat = c.interface.encodeFunctionData("initiateChallenge", args)
let nodes = await getTrieNodesForCall(c, c.address, cdat, preimages)
// run "on chain"
for (n of nodes) {
await mm.AddTrieNode(n)
}
let ret = await c.initiateChallenge(...args)
let receipt = await ret.wait()
// ChallengeCreated event
let challengeId = receipt.events[0].args['challengeId'].toNumber()
console.log("new challenge with id", challengeId)
// the real issue here is from step 0->1 when we write the input hash
// TODO: prove the challenger wrong?
}).timeout(200_000)
})
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
const { keccak256 } = require("@ethersproject/keccak256");
const { expect } = require("chai");
const chai = require("chai");
const { solidity } = require("ethereum-waffle");
chai.use(solidity);
const { writeMemory } = require("../scripts/lib")
async function loadPreimageAndSelect(mm, data, offset) {
// add in the preimage at offset 4
const hash = keccak256(data)
await mm.AddPreimage(data, offset)
// write the oracle selection address
let root = "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"
root = await writeMemory(mm, root, 0x30001000, hash, true)
return root
}
describe("MIPSMemory oracle", function () {
beforeEach(async function () {
const MIPSMemory = await ethers.getContractFactory("MIPSMemory")
mm = await MIPSMemory.deploy()
await mm.AddTrieNode(new Uint8Array([0x80]))
})
it("simple oracle", async function() {
root = await loadPreimageAndSelect(mm, [0x11,0x22,0x33,0x44,0xaa,0xbb,0xcc,0xdd], 4)
// length is 8
expect(await mm.ReadMemory(root, 0x31000000)).to.equal(8)
// offset 4 is 0xaabbccdd
expect(await mm.ReadMemory(root, 0x31000008)).to.equal(0xaabbccdd)
// offset 0 isn't loaded
await expect(mm.ReadMemory(root, 0x31000004)).to.be.reverted;
})
it("misaligned oracle", async function() {
root = await loadPreimageAndSelect(mm, [0x11,0x22,0x33,0x44,0xaa,0xbb,0xcc], 4)
expect(await mm.ReadMemory(root, 0x31000000)).to.equal(7)
expect(await mm.ReadMemory(root, 0x31000008)).to.equal(0xaabbcc00)
})
})
\ No newline at end of file
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