Commit 73d56a9d authored by mbaxter's avatar mbaxter Committed by GitHub

cannon: Encapsulate syscall helpers (#10978)

* cannon: Increment MIPS.sol version

* cannon: Add syscall args helper

* cannon: Extract mmap helper

* cannon: Extract sysRead helper

Also:
- Extract solidity memory helpers into new lib.
- Reorganize syscall constants.

* cannon: Add NatSpec documentation to solidity syscall helpers

* cannon: Extract sysWrite helper

* cannon: Use consistent naming convention

* cannon: Extract sysFcntl helper, fix typos

* cannon: Consolidate comments, fix formatting

* cannon: Add helper for setting registers, pc fields

* cannon: Reorganize syscall constants

* cannon: Fix MIPSMemory import

* cannon: Run semver-lock and snapshots

* cannon: Explicitly inject proof offsets into helper fns

Proofs offsets may differ between implementations, so these values must
be passed into helper functions rather than calculated internally.

* cannon: Remove hard-coded state.memRoot references from MIPSMemory.sol

* cannon: Work around stack too deep error

* cannon: Rework stack-too-deep fix

* cannon: Run semver-lock and snapshots

* cannon: Validate calldata size directly in readMem, writeMem

* cannon: Run semver-lock and snapshots
parent d8bde214
......@@ -30,7 +30,7 @@ fuzz:
go test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime 10s -fuzz=FuzzStateSyscallClone ./mipsevm
go test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime 10s -fuzz=FuzzStateSyscallMmap ./mipsevm
go test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime 10s -fuzz=FuzzStateSyscallExitGroup ./mipsevm
go test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime 10s -fuzz=FuzzStateSyscallFnctl ./mipsevm
go test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime 10s -fuzz=FuzzStateSyscallFcntl ./mipsevm
go test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime 10s -fuzz=FuzzStateHintRead ./mipsevm
go test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime 20s -fuzz=FuzzStatePreimageRead ./mipsevm
go test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime 10s -fuzz=FuzzStateHintWrite ./mipsevm
......
......@@ -229,7 +229,7 @@ func FuzzStateSyscallExitGroup(f *testing.F) {
})
}
func FuzzStateSyscallFnctl(f *testing.F) {
func FuzzStateSyscallFcntl(f *testing.F) {
contracts, addrs := testContractsSetup(f)
f.Fuzz(func(t *testing.T, fd uint32, cmd uint32) {
state := &State{
......
......@@ -39,21 +39,6 @@ type InstrumentedState struct {
debugEnabled bool
}
const (
fdStdin = 0
fdStdout = 1
fdStderr = 2
fdHintRead = 3
fdHintWrite = 4
fdPreimageRead = 5
fdPreimageWrite = 6
)
const (
MipsEBADF = 0x9
MipsEINVAL = 0x16
)
func NewInstrumentedState(state *State, po PreimageOracle, stdOut, stdErr io.Writer) *InstrumentedState {
return &InstrumentedState{
state: state,
......
......@@ -3,17 +3,9 @@ package mipsevm
import (
"encoding/binary"
"fmt"
"io"
)
const (
sysMmap = 4090
sysBrk = 4045
sysClone = 4120
sysExitGroup = 4246
sysRead = 4003
sysWrite = 4004
sysFcntl = 4055
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
)
func (m *InstrumentedState) readPreimage(key [32]byte, offset uint32) (dat [32]byte, datLen uint32) {
......@@ -43,29 +35,17 @@ func (m *InstrumentedState) trackMemAccess(effAddr uint32) {
}
func (m *InstrumentedState) handleSyscall() error {
syscallNum := m.state.Registers[2] // v0
syscallNum, a0, a1, a2 := getSyscallArgs(&m.state.Registers)
v0 := uint32(0)
v1 := uint32(0)
a0 := m.state.Registers[4]
a1 := m.state.Registers[5]
a2 := m.state.Registers[6]
//fmt.Printf("syscall: %d\n", syscallNum)
switch syscallNum {
case sysMmap:
sz := a1
if sz&PageAddrMask != 0 { // adjust size to align with page size
sz += PageSize - (sz & PageAddrMask)
}
if a0 == 0 {
v0 = m.state.Heap
//fmt.Printf("mmap heap 0x%x size 0x%x\n", v0, sz)
m.state.Heap += sz
} else {
v0 = a0
//fmt.Printf("mmap hint 0x%x size 0x%x\n", v0, sz)
}
var newHeap uint32
v0, v1, newHeap = handleSysMmap(a0, a1, m.state.Heap)
m.state.Heap = newHeap
case sysBrk:
v0 = 0x40000000
case sysClone: // clone (not supported)
......@@ -75,107 +55,22 @@ func (m *InstrumentedState) handleSyscall() error {
m.state.ExitCode = uint8(a0)
return nil
case sysRead:
// args: a0 = fd, a1 = addr, a2 = count
// returns: v0 = read, v1 = err code
switch a0 {
case fdStdin:
// leave v0 and v1 zero: read nothing, no error
case fdPreimageRead: // pre-image oracle
effAddr := a1 & 0xFFffFFfc
m.trackMemAccess(effAddr)
mem := m.state.Memory.GetMemory(effAddr)
dat, datLen := m.readPreimage(m.state.PreimageKey, m.state.PreimageOffset)
//fmt.Printf("reading pre-image data: addr: %08x, offset: %d, datLen: %d, data: %x, key: %s count: %d\n", a1, m.state.PreimageOffset, datLen, dat[:datLen], m.state.PreimageKey, a2)
alignment := a1 & 3
space := 4 - alignment
if space < datLen {
datLen = space
}
if a2 < datLen {
datLen = a2
}
var outMem [4]byte
binary.BigEndian.PutUint32(outMem[:], mem)
copy(outMem[alignment:], dat[:datLen])
m.state.Memory.SetMemory(effAddr, binary.BigEndian.Uint32(outMem[:]))
m.state.PreimageOffset += datLen
v0 = datLen
//fmt.Printf("read %d pre-image bytes, new offset: %d, eff addr: %08x mem: %08x\n", datLen, m.state.PreimageOffset, effAddr, outMem)
case fdHintRead: // hint response
// don't actually read into memory, just say we read it all, we ignore the result anyway
v0 = a2
default:
v0 = 0xFFffFFff
v1 = MipsEBADF
}
var newPreimageOffset uint32
v0, v1, newPreimageOffset = handleSysRead(a0, a1, a2, m.state.PreimageKey, m.state.PreimageOffset, m.readPreimage, m.state.Memory, m.trackMemAccess)
m.state.PreimageOffset = newPreimageOffset
case sysWrite:
// args: a0 = fd, a1 = addr, a2 = count
// returns: v0 = written, v1 = err code
switch a0 {
case fdStdout:
_, _ = io.Copy(m.stdOut, m.state.Memory.ReadMemoryRange(a1, a2))
v0 = a2
case fdStderr:
_, _ = io.Copy(m.stdErr, m.state.Memory.ReadMemoryRange(a1, a2))
v0 = a2
case fdHintWrite:
hintData, _ := io.ReadAll(m.state.Memory.ReadMemoryRange(a1, a2))
m.state.LastHint = append(m.state.LastHint, hintData...)
for len(m.state.LastHint) >= 4 { // process while there is enough data to check if there are any hints
hintLen := binary.BigEndian.Uint32(m.state.LastHint[:4])
if hintLen <= uint32(len(m.state.LastHint[4:])) {
hint := m.state.LastHint[4 : 4+hintLen] // without the length prefix
m.state.LastHint = m.state.LastHint[4+hintLen:]
m.preimageOracle.Hint(hint)
} else {
break // stop processing hints if there is incomplete data buffered
}
}
v0 = a2
case fdPreimageWrite:
effAddr := a1 & 0xFFffFFfc
m.trackMemAccess(effAddr)
mem := m.state.Memory.GetMemory(effAddr)
key := m.state.PreimageKey
alignment := a1 & 3
space := 4 - alignment
if space < a2 {
a2 = space
}
copy(key[:], key[a2:])
var tmp [4]byte
binary.BigEndian.PutUint32(tmp[:], mem)
copy(key[32-a2:], tmp[alignment:])
m.state.PreimageKey = key
m.state.PreimageOffset = 0
//fmt.Printf("updating pre-image key: %s\n", m.state.PreimageKey)
v0 = a2
default:
v0 = 0xFFffFFff
v1 = MipsEBADF
}
var newLastHint hexutil.Bytes
var newPreimageKey common.Hash
var newPreimageOffset uint32
v0, v1, newLastHint, newPreimageKey, newPreimageOffset = handleSysWrite(a0, a1, a2, m.state.LastHint, m.state.PreimageKey, m.state.PreimageOffset, m.preimageOracle, m.state.Memory, m.trackMemAccess, m.stdOut, m.stdErr)
m.state.LastHint = newLastHint
m.state.PreimageKey = newPreimageKey
m.state.PreimageOffset = newPreimageOffset
case sysFcntl:
// args: a0 = fd, a1 = cmd
if a1 == 3 { // F_GETFL: get file descriptor flags
switch a0 {
case fdStdin, fdPreimageRead, fdHintRead:
v0 = 0 // O_RDONLY
case fdStdout, fdStderr, fdPreimageWrite, fdHintWrite:
v0 = 1 // O_WRONLY
default:
v0 = 0xFFffFFff
v1 = MipsEBADF
}
} else {
v0 = 0xFFffFFff
v1 = MipsEINVAL // cmd not recognized by this kernel
}
v0, v1 = handleSysFcntl(a0, a1)
}
m.state.Registers[2] = v0
m.state.Registers[7] = v1
m.state.Cpu.PC = m.state.Cpu.NextPC
m.state.Cpu.NextPC = m.state.Cpu.NextPC + 4
handleSyscallUpdates(&m.state.Cpu, &m.state.Registers, v0, v1)
return nil
}
......
package mipsevm
import (
"encoding/binary"
"io"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
)
const (
sysMmap = 4090
sysBrk = 4045
sysClone = 4120
sysExitGroup = 4246
sysRead = 4003
sysWrite = 4004
sysFcntl = 4055
)
const (
fdStdin = 0
fdStdout = 1
fdStderr = 2
fdHintRead = 3
fdHintWrite = 4
fdPreimageRead = 5
fdPreimageWrite = 6
)
const (
MipsEBADF = 0x9
MipsEINVAL = 0x16
)
type PreimageReader func(key [32]byte, offset uint32) (dat [32]byte, datLen uint32)
type MemTracker func(addr uint32)
func getSyscallArgs(registers *[32]uint32) (syscallNum, a0, a1, a2 uint32) {
syscallNum = registers[2] // v0
a0 = registers[4]
a1 = registers[5]
a2 = registers[6]
return syscallNum, a0, a1, a2
}
func handleSysMmap(a0, a1, heap uint32) (v0, v1, newHeap uint32) {
v1 = uint32(0)
newHeap = heap
sz := a1
if sz&PageAddrMask != 0 { // adjust size to align with page size
sz += PageSize - (sz & PageAddrMask)
}
if a0 == 0 {
v0 = heap
//fmt.Printf("mmap heap 0x%x size 0x%x\n", v0, sz)
newHeap += sz
} else {
v0 = a0
//fmt.Printf("mmap hint 0x%x size 0x%x\n", v0, sz)
}
return v0, v1, newHeap
}
func handleSysRead(a0, a1, a2 uint32, preimageKey [32]byte, preimageOffset uint32, preimageReader PreimageReader, memory *Memory, memTracker MemTracker) (v0, v1, newPreimageOffset uint32) {
// args: a0 = fd, a1 = addr, a2 = count
// returns: v0 = read, v1 = err code
v0 = uint32(0)
v1 = uint32(0)
newPreimageOffset = preimageOffset
switch a0 {
case fdStdin:
// leave v0 and v1 zero: read nothing, no error
case fdPreimageRead: // pre-image oracle
effAddr := a1 & 0xFFffFFfc
memTracker(effAddr)
mem := memory.GetMemory(effAddr)
dat, datLen := preimageReader(preimageKey, preimageOffset)
//fmt.Printf("reading pre-image data: addr: %08x, offset: %d, datLen: %d, data: %x, key: %s count: %d\n", a1, m.state.PreimageOffset, datLen, dat[:datLen], m.state.PreimageKey, a2)
alignment := a1 & 3
space := 4 - alignment
if space < datLen {
datLen = space
}
if a2 < datLen {
datLen = a2
}
var outMem [4]byte
binary.BigEndian.PutUint32(outMem[:], mem)
copy(outMem[alignment:], dat[:datLen])
memory.SetMemory(effAddr, binary.BigEndian.Uint32(outMem[:]))
newPreimageOffset += datLen
v0 = datLen
//fmt.Printf("read %d pre-image bytes, new offset: %d, eff addr: %08x mem: %08x\n", datLen, m.state.PreimageOffset, effAddr, outMem)
case fdHintRead: // hint response
// don't actually read into memory, just say we read it all, we ignore the result anyway
v0 = a2
default:
v0 = 0xFFffFFff
v1 = MipsEBADF
}
return v0, v1, newPreimageOffset
}
func handleSysWrite(a0, a1, a2 uint32, lastHint hexutil.Bytes, preimageKey [32]byte, preimageOffset uint32, oracle PreimageOracle, memory *Memory, memTracker MemTracker, stdOut, stdErr io.Writer) (v0, v1 uint32, newLastHint hexutil.Bytes, newPreimageKey common.Hash, newPreimageOffset uint32) {
// args: a0 = fd, a1 = addr, a2 = count
// returns: v0 = written, v1 = err code
v1 = uint32(0)
newLastHint = lastHint
newPreimageKey = preimageKey
newPreimageOffset = preimageOffset
switch a0 {
case fdStdout:
_, _ = io.Copy(stdOut, memory.ReadMemoryRange(a1, a2))
v0 = a2
case fdStderr:
_, _ = io.Copy(stdErr, memory.ReadMemoryRange(a1, a2))
v0 = a2
case fdHintWrite:
hintData, _ := io.ReadAll(memory.ReadMemoryRange(a1, a2))
lastHint = append(lastHint, hintData...)
for len(lastHint) >= 4 { // process while there is enough data to check if there are any hints
hintLen := binary.BigEndian.Uint32(lastHint[:4])
if hintLen <= uint32(len(lastHint[4:])) {
hint := lastHint[4 : 4+hintLen] // without the length prefix
lastHint = lastHint[4+hintLen:]
oracle.Hint(hint)
} else {
break // stop processing hints if there is incomplete data buffered
}
}
newLastHint = lastHint
v0 = a2
case fdPreimageWrite:
effAddr := a1 & 0xFFffFFfc
memTracker(effAddr)
mem := memory.GetMemory(effAddr)
key := preimageKey
alignment := a1 & 3
space := 4 - alignment
if space < a2 {
a2 = space
}
copy(key[:], key[a2:])
var tmp [4]byte
binary.BigEndian.PutUint32(tmp[:], mem)
copy(key[32-a2:], tmp[alignment:])
newPreimageKey = key
newPreimageOffset = 0
//fmt.Printf("updating pre-image key: %s\n", m.state.PreimageKey)
v0 = a2
default:
v0 = 0xFFffFFff
v1 = MipsEBADF
}
return v0, v1, newLastHint, newPreimageKey, newPreimageOffset
}
func handleSysFcntl(a0, a1 uint32) (v0, v1 uint32) {
// args: a0 = fd, a1 = cmd
v1 = uint32(0)
if a1 == 3 { // F_GETFL: get file descriptor flags
switch a0 {
case fdStdin, fdPreimageRead, fdHintRead:
v0 = 0 // O_RDONLY
case fdStdout, fdStderr, fdPreimageWrite, fdHintWrite:
v0 = 1 // O_WRONLY
default:
v0 = 0xFFffFFff
v1 = MipsEBADF
}
} else {
v0 = 0xFFffFFff
v1 = MipsEINVAL // cmd not recognized by this kernel
}
return v0, v1
}
func handleSyscallUpdates(cpu *CpuScalars, registers *[32]uint32, v0, v1 uint32) {
registers[2] = v0
registers[7] = v1
cpu.PC = cpu.NextPC
cpu.NextPC = cpu.NextPC + 4
}
......@@ -5,7 +5,7 @@
.ent test
test:
# fnctl(0, 3)
# fcntl(0, 3)
li $v0, 4055
li $a0, 0x0
li $a1, 0x3
......
......@@ -124,8 +124,8 @@
"sourceCodeHash": "0x3ff4a3f21202478935412d47fd5ef7f94a170402ddc50e5c062013ce5544c83f"
},
"src/cannon/MIPS.sol": {
"initCodeHash": "0x1742f31c43d067f94e669e50e632875ba2a2795127ea5bf429acf7ed39ddbc48",
"sourceCodeHash": "0xa50b47ddaee92c52c5f7cfb4b526f9d734c2eec524e7bb609b991d608f124c02"
"initCodeHash": "0xe9183ee3b69d9ec9594d6b3923d78c86c996cd738ccbc09675bb281284c060af",
"sourceCodeHash": "0x7c2eab73da8b2eeadba30eadb39f20e91307bc29218938fadfc5f73fadcf13bc"
},
"src/cannon/PreimageOracle.sol": {
"initCodeHash": "0xe5db668fe41436f53995e910488c7c140766ba8745e19743773ebab508efd090",
......
......@@ -5,7 +5,9 @@ import { ISemver } from "src/universal/ISemver.sol";
import { IPreimageOracle } from "./interfaces/IPreimageOracle.sol";
import { PreimageKeyLib } from "./PreimageKeyLib.sol";
import { MIPSInstructions as ins } from "src/cannon/libraries/MIPSInstructions.sol";
import { MIPSSyscalls as sys } from "src/cannon/libraries/MIPSSyscalls.sol";
import { MIPSState as st } from "src/cannon/libraries/MIPSState.sol";
import { MIPSMemory } from "src/cannon/libraries/MIPSMemory.sol";
/// @title MIPS
/// @notice The MIPS contract emulates a single MIPS instruction.
......@@ -46,22 +48,14 @@ contract MIPS is ISemver {
/// @notice The semantic version of the MIPS contract.
/// @custom:semver 1.0.1
string public constant version = "1.1.0-beta.3";
uint32 internal constant FD_STDIN = 0;
uint32 internal constant FD_STDOUT = 1;
uint32 internal constant FD_STDERR = 2;
uint32 internal constant FD_HINT_READ = 3;
uint32 internal constant FD_HINT_WRITE = 4;
uint32 internal constant FD_PREIMAGE_READ = 5;
uint32 internal constant FD_PREIMAGE_WRITE = 6;
uint32 internal constant EBADF = 0x9;
uint32 internal constant EINVAL = 0x16;
string public constant version = "1.1.0-beta.4";
/// @notice The preimage oracle contract.
IPreimageOracle internal immutable ORACLE;
// The offset of the start of proof calldata (_proof.offset) in the step() function
uint256 internal constant STEP_PROOF_OFFSET = 420;
/// @param _oracle The address of the preimage oracle contract.
constructor(IPreimageOracle _oracle) {
ORACLE = _oracle;
......@@ -148,277 +142,59 @@ contract MIPS is ISemver {
state := 0x80
}
// Load the syscall number from the registers
uint32 syscall_no = state.registers[2];
// Load the syscall numbers and args from the registers
(uint32 syscall_no, uint32 a0, uint32 a1, uint32 a2) = sys.getSyscallArgs(state.registers);
uint32 v0 = 0;
uint32 v1 = 0;
// Load the syscall arguments from the registers
uint32 a0 = state.registers[4];
uint32 a1 = state.registers[5];
uint32 a2 = state.registers[6];
// mmap: Allocates a page from the heap.
if (syscall_no == 4090) {
uint32 sz = a1;
if (sz & 4095 != 0) {
// adjust size to align with page size
sz += 4096 - (sz & 4095);
}
if (a0 == 0) {
v0 = state.heap;
state.heap += sz;
} else {
v0 = a0;
}
}
// brk: Returns a fixed address for the program break at 0x40000000
else if (syscall_no == 4045) {
if (syscall_no == sys.SYS_MMAP) {
(v0, v1, state.heap) = sys.handleSysMmap(a0, a1, state.heap);
} else if (syscall_no == sys.SYS_BRK) {
// brk: Returns a fixed address for the program break at 0x40000000
v0 = BRK_START;
}
// clone (not supported) returns 1
else if (syscall_no == 4120) {
} else if (syscall_no == sys.SYS_CLONE) {
// clone (not supported) returns 1
v0 = 1;
}
// exit group: Sets the Exited and ExitCode states to true and argument 0.
else if (syscall_no == 4246) {
} else if (syscall_no == sys.SYS_EXIT_GROUP) {
// exit group: Sets the Exited and ExitCode states to true and argument 0.
state.exited = true;
state.exitCode = uint8(a0);
return outputState();
}
// read: Like Linux read syscall. Splits unaligned reads into aligned reads.
else if (syscall_no == 4003) {
// args: a0 = fd, a1 = addr, a2 = count
// returns: v0 = read, v1 = err code
if (a0 == FD_STDIN) {
// Leave v0 and v1 zero: read nothing, no error
}
// pre-image oracle read
else if (a0 == FD_PREIMAGE_READ) {
// verify proof 1 is correct, and get the existing memory.
uint32 mem = readMem(a1 & 0xFFffFFfc, 1); // mask the addr to align it to 4 bytes
bytes32 preimageKey = state.preimageKey;
// If the preimage key is a local key, localize it in the context of the caller.
if (uint8(preimageKey[0]) == 1) {
preimageKey = PreimageKeyLib.localize(preimageKey, _localContext);
}
(bytes32 dat, uint256 datLen) = ORACLE.readPreimage(preimageKey, state.preimageOffset);
// Transform data for writing to memory
// We use assembly for more precise ops, and no var count limit
assembly {
let alignment := and(a1, 3) // the read might not start at an aligned address
let space := sub(4, alignment) // remaining space in memory word
if lt(space, datLen) { datLen := space } // if less space than data, shorten data
if lt(a2, datLen) { datLen := a2 } // if requested to read less, read less
dat := shr(sub(256, mul(datLen, 8)), dat) // right-align data
dat := shl(mul(sub(sub(4, datLen), alignment), 8), dat) // position data to insert into memory
// word
let mask := sub(shl(mul(sub(4, alignment), 8), 1), 1) // mask all bytes after start
let suffixMask := sub(shl(mul(sub(sub(4, alignment), datLen), 8), 1), 1) // mask of all bytes
// starting from end, maybe none
mask := and(mask, not(suffixMask)) // reduce mask to just cover the data we insert
mem := or(and(mem, not(mask)), dat) // clear masked part of original memory, and insert data
}
// Write memory back
writeMem(a1 & 0xFFffFFfc, 1, mem);
state.preimageOffset += uint32(datLen);
v0 = uint32(datLen);
}
// hint response
else if (a0 == FD_HINT_READ) {
// Don't read into memory, just say we read it all
// The result is ignored anyway
v0 = a2;
} else {
v0 = 0xFFffFFff;
v1 = EBADF;
}
}
// write: like Linux write syscall. Splits unaligned writes into aligned writes.
else if (syscall_no == 4004) {
// args: a0 = fd, a1 = addr, a2 = count
// returns: v0 = written, v1 = err code
if (a0 == FD_STDOUT || a0 == FD_STDERR || a0 == FD_HINT_WRITE) {
v0 = a2; // tell program we have written everything
}
// pre-image oracle
else if (a0 == FD_PREIMAGE_WRITE) {
uint32 mem = readMem(a1 & 0xFFffFFfc, 1); // mask the addr to align it to 4 bytes
bytes32 key = state.preimageKey;
// Construct pre-image key from memory
// We use assembly for more precise ops, and no var count limit
assembly {
let alignment := and(a1, 3) // the read might not start at an aligned address
let space := sub(4, alignment) // remaining space in memory word
if lt(space, a2) { a2 := space } // if less space than data, shorten data
key := shl(mul(a2, 8), key) // shift key, make space for new info
let mask := sub(shl(mul(a2, 8), 1), 1) // mask for extracting value from memory
mem := and(shr(mul(sub(space, a2), 8), mem), mask) // align value to right, mask it
key := or(key, mem) // insert into key
}
// Write pre-image key to oracle
state.preimageKey = key;
state.preimageOffset = 0; // reset offset, to read new pre-image data from the start
v0 = a2;
} else {
v0 = 0xFFffFFff;
v1 = EBADF;
}
}
// fcntl: Like linux fcntl syscall, but only supports minimal file-descriptor control commands,
// to retrieve the file-descriptor R/W flags.
else if (syscall_no == 4055) {
// fcntl
// args: a0 = fd, a1 = cmd
if (a1 == 3) {
// F_GETFL: get file descriptor flags
if (a0 == FD_STDIN || a0 == FD_PREIMAGE_READ || a0 == FD_HINT_READ) {
v0 = 0; // O_RDONLY
} else if (a0 == FD_STDOUT || a0 == FD_STDERR || a0 == FD_PREIMAGE_WRITE || a0 == FD_HINT_WRITE) {
v0 = 1; // O_WRONLY
} else {
v0 = 0xFFffFFff;
v1 = EBADF;
}
} else {
v0 = 0xFFffFFff;
v1 = EINVAL; // cmd not recognized by this kernel
}
} else if (syscall_no == sys.SYS_READ) {
(v0, v1, state.preimageOffset, state.memRoot) = sys.handleSysRead({
_a0: a0,
_a1: a1,
_a2: a2,
_preimageKey: state.preimageKey,
_preimageOffset: state.preimageOffset,
_localContext: _localContext,
_oracle: ORACLE,
_proofOffset: MIPSMemory.memoryProofOffset(STEP_PROOF_OFFSET, 1),
_memRoot: state.memRoot
});
} else if (syscall_no == sys.SYS_WRITE) {
(v0, v1, state.preimageKey, state.preimageOffset) = sys.handleSysWrite({
_a0: a0,
_a1: a1,
_a2: a2,
_preimageKey: state.preimageKey,
_preimageOffset: state.preimageOffset,
_proofOffset: MIPSMemory.memoryProofOffset(STEP_PROOF_OFFSET, 1),
_memRoot: state.memRoot
});
} else if (syscall_no == sys.SYS_FCNTL) {
(v0, v1) = sys.handleSysFcntl(a0, a1);
}
// Write the results back to the state registers
state.registers[2] = v0;
state.registers[7] = v1;
// Update the PC and nextPC
state.pc = state.nextPC;
state.nextPC = state.nextPC + 4;
st.CpuScalars memory cpu = getCpuScalars(state);
sys.handleSyscallUpdates(cpu, state.registers, v0, v1);
setStateCpuScalars(state, cpu);
out_ = outputState();
}
}
/// @notice Computes the offset of the proof in the calldata.
/// @param _proofIndex The index of the proof in the calldata.
/// @return offset_ The offset of the proof in the calldata.
function proofOffset(uint8 _proofIndex) internal pure returns (uint256 offset_) {
unchecked {
// A proof of 32 bit memory, with 32-byte leaf values, is (32-5)=27 bytes32 entries.
// And the leaf value itself needs to be encoded as well. And proof.offset == 420
offset_ = 420 + (uint256(_proofIndex) * (28 * 32));
uint256 s = 0;
assembly {
s := calldatasize()
}
require(s >= (offset_ + 28 * 32), "check that there is enough calldata");
return offset_;
}
}
/// @notice Reads a 32-bit value from memory.
/// @param _addr The address to read from.
/// @param _proofIndex The index of the proof in the calldata.
/// @return out_ The hashed MIPS state.
function readMem(uint32 _addr, uint8 _proofIndex) internal pure returns (uint32 out_) {
unchecked {
// Compute the offset of the proof in the calldata.
uint256 offset = proofOffset(_proofIndex);
assembly {
// Validate the address alignement.
if and(_addr, 3) { revert(0, 0) }
// Load the leaf value.
let leaf := calldataload(offset)
offset := add(offset, 32)
// Convenience function to hash two nodes together in scratch space.
function hashPair(a, b) -> h {
mstore(0, a)
mstore(32, b)
h := keccak256(0, 64)
}
// Start with the leaf node.
// Work back up by combining with siblings, to reconstruct the root.
let path := shr(5, _addr)
let node := leaf
for { let i := 0 } lt(i, 27) { i := add(i, 1) } {
let sibling := calldataload(offset)
offset := add(offset, 32)
switch and(shr(i, path), 1)
case 0 { node := hashPair(node, sibling) }
case 1 { node := hashPair(sibling, node) }
}
// Load the memory root from the first field of state.
let memRoot := mload(0x80)
// Verify the root matches.
if iszero(eq(node, memRoot)) {
mstore(0, 0x0badf00d)
revert(0, 32)
}
// Bits to shift = (32 - 4 - (addr % 32)) * 8
let shamt := shl(3, sub(sub(32, 4), and(_addr, 31)))
out_ := and(shr(shamt, leaf), 0xFFffFFff)
}
}
}
/// @notice Writes a 32-bit value to memory.
/// This function first overwrites the part of the leaf.
/// Then it recomputes the memory merkle root.
/// @param _addr The address to write to.
/// @param _proofIndex The index of the proof in the calldata.
/// @param _val The value to write.
function writeMem(uint32 _addr, uint8 _proofIndex, uint32 _val) internal pure {
unchecked {
// Compute the offset of the proof in the calldata.
uint256 offset = proofOffset(_proofIndex);
assembly {
// Validate the address alignement.
if and(_addr, 3) { revert(0, 0) }
// Load the leaf value.
let leaf := calldataload(offset)
let shamt := shl(3, sub(sub(32, 4), and(_addr, 31)))
// Mask out 4 bytes, and OR in the value
leaf := or(and(leaf, not(shl(shamt, 0xFFffFFff))), shl(shamt, _val))
offset := add(offset, 32)
// Convenience function to hash two nodes together in scratch space.
function hashPair(a, b) -> h {
mstore(0, a)
mstore(32, b)
h := keccak256(0, 64)
}
// Start with the leaf node.
// Work back up by combining with siblings, to reconstruct the root.
let path := shr(5, _addr)
let node := leaf
for { let i := 0 } lt(i, 27) { i := add(i, 1) } {
let sibling := calldataload(offset)
offset := add(offset, 32)
switch and(shr(i, path), 1)
case 0 { node := hashPair(node, sibling) }
case 1 { node := hashPair(sibling, node) }
}
// Store the new memory root in the first field of state.
mstore(0x80, node)
}
}
}
/// @notice Executes a single step of the vm.
/// Will revert if any required input state is missing.
/// @param _stateData The encoded state witness data.
......@@ -443,7 +219,7 @@ contract MIPS is ISemver {
// 32*4+4=132 expected state data offset
revert(0, 0)
}
if iszero(eq(_proof.offset, 420)) {
if iszero(eq(_proof.offset, STEP_PROOF_OFFSET)) {
// 132+32+256=420 expected proof offset
revert(0, 0)
}
......@@ -485,7 +261,8 @@ contract MIPS is ISemver {
state.step += 1;
// instruction fetch
uint32 insn = readMem(state.pc, 0);
uint256 insnProofOffset = MIPSMemory.memoryProofOffset(STEP_PROOF_OFFSET, 0);
uint32 insn = MIPSMemory.readMem(state.memRoot, state.pc, insnProofOffset);
uint32 opcode = insn >> 26; // 6-bits
// j-type j/jal
......@@ -550,7 +327,8 @@ contract MIPS is ISemver {
// M[R[rs]+SignExtImm]
rs += ins.signExtend(insn & 0xFFFF, 16);
uint32 addr = rs & 0xFFFFFFFC;
mem = readMem(addr, 1);
uint256 memProofOffset = MIPSMemory.memoryProofOffset(STEP_PROOF_OFFSET, 1);
mem = MIPSMemory.readMem(state.memRoot, addr, memProofOffset);
if (opcode >= 0x28 && opcode != 0x30) {
// store
storeAddr = addr;
......@@ -610,7 +388,8 @@ contract MIPS is ISemver {
// write memory
if (storeAddr != 0xFF_FF_FF_FF) {
writeMem(storeAddr, 1, val);
uint256 memProofOffset = MIPSMemory.memoryProofOffset(STEP_PROOF_OFFSET, 1);
state.memRoot = MIPSMemory.writeMem(storeAddr, memProofOffset, val);
}
// write back the value to destination register
......
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
library MIPSMemory {
/// @notice Reads a 32-bit value from memory.
/// @param _memRoot The current memory root
/// @param _addr The address to read from.
/// @param _proofOffset The offset of the memory proof in calldata.
/// @return out_ The hashed MIPS state.
function readMem(bytes32 _memRoot, uint32 _addr, uint256 _proofOffset) internal pure returns (uint32 out_) {
unchecked {
validateMemoryProofAvailability(_proofOffset);
assembly {
// Validate the address alignement.
if and(_addr, 3) { revert(0, 0) }
// Load the leaf value.
let leaf := calldataload(_proofOffset)
_proofOffset := add(_proofOffset, 32)
// Convenience function to hash two nodes together in scratch space.
function hashPair(a, b) -> h {
mstore(0, a)
mstore(32, b)
h := keccak256(0, 64)
}
// Start with the leaf node.
// Work back up by combining with siblings, to reconstruct the root.
let path := shr(5, _addr)
let node := leaf
for { let i := 0 } lt(i, 27) { i := add(i, 1) } {
let sibling := calldataload(_proofOffset)
_proofOffset := add(_proofOffset, 32)
switch and(shr(i, path), 1)
case 0 { node := hashPair(node, sibling) }
case 1 { node := hashPair(sibling, node) }
}
// Verify the root matches.
if iszero(eq(node, _memRoot)) {
mstore(0, 0x0badf00d)
revert(0, 32)
}
// Bits to shift = (32 - 4 - (addr % 32)) * 8
let shamt := shl(3, sub(sub(32, 4), and(_addr, 31)))
out_ := and(shr(shamt, leaf), 0xFFffFFff)
}
}
}
/// @notice Writes a 32-bit value to memory.
/// This function first overwrites the part of the leaf.
/// Then it recomputes the memory merkle root.
/// @param _addr The address to write to.
/// @param _proofOffset The offset of the memory proof in calldata.
/// @param _val The value to write.
/// @return newMemRoot_ The new memory root after modification
function writeMem(uint32 _addr, uint256 _proofOffset, uint32 _val) internal pure returns (bytes32 newMemRoot_) {
unchecked {
validateMemoryProofAvailability(_proofOffset);
assembly {
// Validate the address alignement.
if and(_addr, 3) { revert(0, 0) }
// Load the leaf value.
let leaf := calldataload(_proofOffset)
let shamt := shl(3, sub(sub(32, 4), and(_addr, 31)))
// Mask out 4 bytes, and OR in the value
leaf := or(and(leaf, not(shl(shamt, 0xFFffFFff))), shl(shamt, _val))
_proofOffset := add(_proofOffset, 32)
// Convenience function to hash two nodes together in scratch space.
function hashPair(a, b) -> h {
mstore(0, a)
mstore(32, b)
h := keccak256(0, 64)
}
// Start with the leaf node.
// Work back up by combining with siblings, to reconstruct the root.
let path := shr(5, _addr)
let node := leaf
for { let i := 0 } lt(i, 27) { i := add(i, 1) } {
let sibling := calldataload(_proofOffset)
_proofOffset := add(_proofOffset, 32)
switch and(shr(i, path), 1)
case 0 { node := hashPair(node, sibling) }
case 1 { node := hashPair(sibling, node) }
}
newMemRoot_ := node
}
}
return newMemRoot_;
}
/// @notice Computes the offset of a memory proof in the calldata.
/// @param _proofDataOffset The offset of the set of all memory proof data within calldata (proof.offset)
/// Equal to the offset of the first memory proof (at _proofIndex 0).
/// @param _proofIndex The index of the proof in the calldata.
/// @return offset_ The offset of the memory proof at the given _proofIndex in the calldata.
function memoryProofOffset(uint256 _proofDataOffset, uint8 _proofIndex) internal pure returns (uint256 offset_) {
unchecked {
// A proof of 32 bit memory, with 32-byte leaf values, is (32-5)=27 bytes32 entries.
// And the leaf value itself needs to be encoded as well: (27 + 1) = 28 bytes32 entries.
offset_ = _proofDataOffset + (uint256(_proofIndex) * (28 * 32));
return offset_;
}
}
/// @notice Validates that enough calldata is available to hold a full memory proof at the given offset
/// @param _proofStartOffset The index of the first byte of the target memory proof in calldata
function validateMemoryProofAvailability(uint256 _proofStartOffset) internal pure {
uint256 s = 0;
assembly {
s := calldatasize()
}
// A memory proof consists of 28 bytes32 values - verify we have enough calldata
require(s >= (_proofStartOffset + 28 * 32), "check that there is enough calldata");
}
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import { MIPSMemory } from "src/cannon/libraries/MIPSMemory.sol";
import { MIPSState as st } from "src/cannon/libraries/MIPSState.sol";
import { IPreimageOracle } from "src/cannon/interfaces/IPreimageOracle.sol";
import { PreimageKeyLib } from "src/cannon/PreimageKeyLib.sol";
library MIPSSyscalls {
uint32 internal constant SYS_MMAP = 4090;
uint32 internal constant SYS_BRK = 4045;
uint32 internal constant SYS_CLONE = 4120;
uint32 internal constant SYS_EXIT_GROUP = 4246;
uint32 internal constant SYS_READ = 4003;
uint32 internal constant SYS_WRITE = 4004;
uint32 internal constant SYS_FCNTL = 4055;
uint32 internal constant FD_STDIN = 0;
uint32 internal constant FD_STDOUT = 1;
uint32 internal constant FD_STDERR = 2;
uint32 internal constant FD_HINT_READ = 3;
uint32 internal constant FD_HINT_WRITE = 4;
uint32 internal constant FD_PREIMAGE_READ = 5;
uint32 internal constant FD_PREIMAGE_WRITE = 6;
uint32 internal constant EBADF = 0x9;
uint32 internal constant EINVAL = 0x16;
/// @notice Extract syscall num and arguments from registers.
/// @param _registers The cpu registers.
/// @return sysCallNum_ The syscall number.
/// @return a0_ The first argument available to the syscall operation.
/// @return a1_ The second argument available to the syscall operation.
/// @return a2_ The third argument available to the syscall operation.
function getSyscallArgs(uint32[32] memory _registers)
internal
pure
returns (uint32 sysCallNum_, uint32 a0_, uint32 a1_, uint32 a2_)
{
sysCallNum_ = _registers[2];
a0_ = _registers[4];
a1_ = _registers[5];
a2_ = _registers[6];
return (sysCallNum_, a0_, a1_, a2_);
}
/// @notice Like a Linux mmap syscall. Allocates a page from the heap.
/// @param _a0 The address for the new mapping
/// @param _a1 The size of the new mapping
/// @param _heap The current value of the heap pointer
/// @return v0_ The address of the new mapping
/// @return v1_ Unused error code (0)
/// @return newHeap_ The new value for the heap, may be unchanged
function handleSysMmap(
uint32 _a0,
uint32 _a1,
uint32 _heap
)
internal
pure
returns (uint32 v0_, uint32 v1_, uint32 newHeap_)
{
v1_ = uint32(0);
newHeap_ = _heap;
uint32 sz = _a1;
if (sz & 4095 != 0) {
// adjust size to align with page size
sz += 4096 - (sz & 4095);
}
if (_a0 == 0) {
v0_ = _heap;
newHeap_ += sz;
} else {
v0_ = _a0;
}
return (v0_, v1_, newHeap_);
}
/// @notice Like a Linux read syscall. Splits unaligned reads into aligned reads.
/// @param _a0 The file descriptor.
/// @param _a1 The memory location where data should be read to.
/// @param _a2 The number of bytes to read from the file
/// @param _preimageKey The key of the preimage to read.
/// @param _preimageOffset The offset of the preimage to read.
/// @param _localContext The local context for the preimage key.
/// @param _oracle The address of the preimage oracle.
/// @param _proofOffset The offset of the memory proof in calldata.
/// @param _memRoot The current memory root.
/// @return v0_ The number of bytes read, -1 on error.
/// @return v1_ The error code, 0 if there is no error.
/// @return newPreimageOffset_ The new value for the preimage offset.
/// @return newMemRoot_ The new memory root.
function handleSysRead(
uint32 _a0,
uint32 _a1,
uint32 _a2,
bytes32 _preimageKey,
uint32 _preimageOffset,
bytes32 _localContext,
IPreimageOracle _oracle,
uint256 _proofOffset,
bytes32 _memRoot
)
internal
view
returns (uint32 v0_, uint32 v1_, uint32 newPreimageOffset_, bytes32 newMemRoot_)
{
v0_ = uint32(0);
v1_ = uint32(0);
newMemRoot_ = _memRoot;
newPreimageOffset_ = _preimageOffset;
// args: _a0 = fd, _a1 = addr, _a2 = count
// returns: v0_ = read, v1_ = err code
if (_a0 == FD_STDIN) {
// Leave v0_ and v1_ zero: read nothing, no error
}
// pre-image oracle read
else if (_a0 == FD_PREIMAGE_READ) {
// verify proof is correct, and get the existing memory.
// mask the addr to align it to 4 bytes
uint32 mem = MIPSMemory.readMem(_memRoot, _a1 & 0xFFffFFfc, _proofOffset);
// If the preimage key is a local key, localize it in the context of the caller.
if (uint8(_preimageKey[0]) == 1) {
_preimageKey = PreimageKeyLib.localize(_preimageKey, _localContext);
}
(bytes32 dat, uint256 datLen) = _oracle.readPreimage(_preimageKey, _preimageOffset);
// Transform data for writing to memory
// We use assembly for more precise ops, and no var count limit
assembly {
let alignment := and(_a1, 3) // the read might not start at an aligned address
let space := sub(4, alignment) // remaining space in memory word
if lt(space, datLen) { datLen := space } // if less space than data, shorten data
if lt(_a2, datLen) { datLen := _a2 } // if requested to read less, read less
dat := shr(sub(256, mul(datLen, 8)), dat) // right-align data
dat := shl(mul(sub(sub(4, datLen), alignment), 8), dat) // position data to insert into memory
// word
let mask := sub(shl(mul(sub(4, alignment), 8), 1), 1) // mask all bytes after start
let suffixMask := sub(shl(mul(sub(sub(4, alignment), datLen), 8), 1), 1) // mask of all bytes
// starting from end, maybe none
mask := and(mask, not(suffixMask)) // reduce mask to just cover the data we insert
mem := or(and(mem, not(mask)), dat) // clear masked part of original memory, and insert data
}
// Write memory back
newMemRoot_ = MIPSMemory.writeMem(_a1 & 0xFFffFFfc, _proofOffset, mem);
newPreimageOffset_ += uint32(datLen);
v0_ = uint32(datLen);
}
// hint response
else if (_a0 == FD_HINT_READ) {
// Don't read into memory, just say we read it all
// The result is ignored anyway
v0_ = _a2;
} else {
v0_ = 0xFFffFFff;
v1_ = EBADF;
}
return (v0_, v1_, newPreimageOffset_, newMemRoot_);
}
/// @notice Like a Linux write syscall. Splits unaligned writes into aligned writes.
/// @param _a0 The file descriptor.
/// @param _a1 The memory address to read from.
/// @param _a2 The number of bytes to read.
/// @param _preimageKey The current preimaageKey.
/// @param _preimageOffset The current preimageOffset.
/// @param _proofOffset The offset of the memory proof in calldata.
/// @param _memRoot The current memory root.
/// @return v0_ The number of bytes written, or -1 on error.
/// @return v1_ The error code, or 0 if empty.
/// @return newPreimageKey_ The new preimageKey.
/// @return newPreimageOffset_ The new preimageOffset.
function handleSysWrite(
uint32 _a0,
uint32 _a1,
uint32 _a2,
bytes32 _preimageKey,
uint32 _preimageOffset,
uint256 _proofOffset,
bytes32 _memRoot
)
internal
pure
returns (uint32 v0_, uint32 v1_, bytes32 newPreimageKey_, uint32 newPreimageOffset_)
{
// args: _a0 = fd, _a1 = addr, _a2 = count
// returns: v0_ = written, v1_ = err code
v0_ = uint32(0);
v1_ = uint32(0);
newPreimageKey_ = _preimageKey;
newPreimageOffset_ = _preimageOffset;
if (_a0 == FD_STDOUT || _a0 == FD_STDERR || _a0 == FD_HINT_WRITE) {
v0_ = _a2; // tell program we have written everything
}
// pre-image oracle
else if (_a0 == FD_PREIMAGE_WRITE) {
// mask the addr to align it to 4 bytes
uint32 mem = MIPSMemory.readMem(_memRoot, _a1 & 0xFFffFFfc, _proofOffset);
bytes32 key = _preimageKey;
// Construct pre-image key from memory
// We use assembly for more precise ops, and no var count limit
assembly {
let alignment := and(_a1, 3) // the read might not start at an aligned address
let space := sub(4, alignment) // remaining space in memory word
if lt(space, _a2) { _a2 := space } // if less space than data, shorten data
key := shl(mul(_a2, 8), key) // shift key, make space for new info
let mask := sub(shl(mul(_a2, 8), 1), 1) // mask for extracting value from memory
mem := and(shr(mul(sub(space, _a2), 8), mem), mask) // align value to right, mask it
key := or(key, mem) // insert into key
}
// Write pre-image key to oracle
newPreimageKey_ = key;
newPreimageOffset_ = 0; // reset offset, to read new pre-image data from the start
v0_ = _a2;
} else {
v0_ = 0xFFffFFff;
v1_ = EBADF;
}
return (v0_, v1_, newPreimageKey_, newPreimageOffset_);
}
/// @notice Like Linux fcntl (file control) syscall, but only supports minimal file-descriptor control commands, to
/// retrieve the file-descriptor R/W flags.
/// @param _a0 The file descriptor.
/// @param _a1 The control command.
/// @param v0_ The file status flag (only supported command is F_GETFL), or -1 on error.
/// @param v1_ An error number, or 0 if there is no error.
function handleSysFcntl(uint32 _a0, uint32 _a1) internal pure returns (uint32 v0_, uint32 v1_) {
v0_ = uint32(0);
v1_ = uint32(0);
// args: _a0 = fd, _a1 = cmd
if (_a1 == 3) {
// F_GETFL: get file descriptor flags
if (_a0 == FD_STDIN || _a0 == FD_PREIMAGE_READ || _a0 == FD_HINT_READ) {
v0_ = 0; // O_RDONLY
} else if (_a0 == FD_STDOUT || _a0 == FD_STDERR || _a0 == FD_PREIMAGE_WRITE || _a0 == FD_HINT_WRITE) {
v0_ = 1; // O_WRONLY
} else {
v0_ = 0xFFffFFff;
v1_ = EBADF;
}
} else {
v0_ = 0xFFffFFff;
v1_ = EINVAL; // cmd not recognized by this kernel
}
return (v0_, v1_);
}
function handleSyscallUpdates(
st.CpuScalars memory _cpu,
uint32[32] memory _registers,
uint32 _v0,
uint32 _v1
)
internal
pure
{
// Write the results back to the state registers
_registers[2] = _v0;
_registers[7] = _v1;
// Update the PC and nextPC
_cpu.pc = _cpu.nextPC;
_cpu.nextPC = _cpu.nextPC + 4;
}
}
......@@ -1470,7 +1470,7 @@ contract MIPS_Test is CommonTest {
function test_fcntl_succeeds() external {
uint32 insn = 0x0000000c; // syscall
(MIPS.State memory state, bytes memory proof) = constructMIPSState(0, insn, 0x4, 0);
state.registers[2] = 4055; // fnctl syscall
state.registers[2] = 4055; // fcntl syscall
state.registers[4] = 0x0; // a0
state.registers[5] = 0x3; // a1
......
......@@ -25,7 +25,7 @@ contract DeploymentSummaryFaultProofs is DeploymentSummaryFaultProofsCode {
address internal constant l1ERC721BridgeProxyAddress = 0xD31598c909d9C935a9e35bA70d9a3DD47d4D5865;
address internal constant l1StandardBridgeAddress = 0xb7900B27Be8f0E0fF65d1C3A4671e1220437dd2b;
address internal constant l1StandardBridgeProxyAddress = 0xDeF3bca8c80064589E6787477FFa7Dd616B5574F;
address internal constant mipsAddress = 0x49E77cdE01fFBAB1266813b9a2FaADc1B757F435;
address internal constant mipsAddress = 0x28bF1582225713139c0E898326Db808B6484cFd4;
address internal constant optimismPortal2Address = 0xfcbb237388CaF5b08175C9927a37aB6450acd535;
address internal constant optimismPortalProxyAddress = 0x978e3286EB805934215a88694d80b09aDed68D90;
address internal constant preimageOracleAddress = 0x3bd7E801E51d48c5d94Ea68e8B801DFFC275De75;
......
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