Skip to content

Commit 16e809f

Browse files
authored
Merge pull request #20 from ethereum-optimism/tip/pcw109550/asterisc-contract-test
feat: Asterisc contracts VM tests - FFI
2 parents 181233f + d172a19 commit 16e809f

File tree

6 files changed

+250
-3
lines changed

6 files changed

+250
-3
lines changed

.github/workflows/ci.yaml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,17 @@ jobs:
2929
steps:
3030
- name: Checkout repository
3131
uses: actions/checkout@v4
32+
- name: Install Golang
33+
uses: actions/setup-go@v4
34+
with:
35+
go-version: '1.21.x'
36+
- name: Build FFI
37+
run: go build
38+
working-directory: rvgo/scripts/go-ffi
3239
- name: Install Foundry
3340
uses: foundry-rs/foundry-toolchain@v1
3441
- name: Run foundry tests
35-
run: forge test -vvv
42+
run: forge test -vvv --ffi
3643
working-directory: rvsol
3744

3845
# go-lint:

rvgo/scripts/go-ffi/bin.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package main
2+
3+
import (
4+
"log"
5+
"os"
6+
)
7+
8+
func main() {
9+
if len(os.Args) < 2 {
10+
log.Fatal("Must pass a subcommand")
11+
}
12+
switch os.Args[1] {
13+
case "diff":
14+
DiffTestUtils()
15+
default:
16+
log.Fatalf("Unrecognized subcommand: %s", os.Args[1])
17+
}
18+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package main
2+
3+
import (
4+
"encoding/binary"
5+
"encoding/hex"
6+
"fmt"
7+
"os"
8+
"strconv"
9+
"strings"
10+
11+
"github.com/ethereum-optimism/asterisc/rvgo/fast"
12+
"github.com/ethereum/go-ethereum/accounts/abi"
13+
"github.com/ethereum/go-ethereum/common"
14+
"github.com/ethereum/go-ethereum/common/hexutil"
15+
)
16+
17+
// ABI types
18+
var (
19+
asteriscMemoryProof, _ = abi.NewType("tuple", "AsteriscMemoryProof", []abi.ArgumentMarshaling{
20+
{Name: "memRoot", Type: "bytes32"},
21+
{Name: "proof", Type: "bytes"},
22+
})
23+
asteriscMemoryProofArgs = abi.Arguments{
24+
{Name: "encodedAsteriscMemoryProof", Type: asteriscMemoryProof},
25+
}
26+
)
27+
28+
func DiffTestUtils() {
29+
args := os.Args[2:]
30+
variant := args[0]
31+
32+
// This command requires arguments
33+
if len(args) == 0 {
34+
panic("Error: No arguments provided")
35+
}
36+
37+
switch variant {
38+
case "asteriscMemoryProof":
39+
// <pc, insn, [memAddr, memValue]>
40+
if len(args) != 3 && len(args) != 5 {
41+
panic("Error: asteriscMemoryProofWithProof requires 2 or 4 arguments")
42+
}
43+
mem := fast.NewMemory()
44+
pc, err := strconv.ParseUint(args[1], 10, 64)
45+
checkErr(err, "Error decoding addr")
46+
insn, err := strconv.ParseUint(args[2], 10, 32)
47+
checkErr(err, "Error decoding insn")
48+
instBytes := make([]byte, 4)
49+
binary.LittleEndian.PutUint32(instBytes, uint32(insn))
50+
mem.SetUnaligned(uint64(pc), instBytes)
51+
52+
// proof size: 64-5+1=60 (a 64-bit mem-address branch to 32 byte leaf, incl leaf itself), all 32 bytes
53+
// 60 * 32 = 1920
54+
var insnProof, memProof [1920]byte
55+
if len(args) == 5 {
56+
memAddr, err := strconv.ParseUint(args[3], 10, 64)
57+
checkErr(err, "Error decoding memAddr")
58+
memValue, err := hex.DecodeString(strings.TrimPrefix(args[4], "0x"))
59+
checkErr(err, "Error decoding memValue")
60+
mem.SetUnaligned(uint64(memAddr), memValue)
61+
memProof = mem.MerkleProof(uint64(memAddr))
62+
}
63+
insnProof = mem.MerkleProof(uint64(pc))
64+
65+
output := struct {
66+
MemRoot common.Hash
67+
Proof []byte
68+
}{
69+
MemRoot: mem.MerkleRoot(),
70+
Proof: append(insnProof[:], memProof[:]...),
71+
}
72+
packed, err := asteriscMemoryProofArgs.Pack(&output)
73+
checkErr(err, "Error encoding output")
74+
fmt.Print(hexutil.Encode(packed[32:]))
75+
default:
76+
panic(fmt.Errorf("unknown command: %s", args[0]))
77+
}
78+
}

rvgo/scripts/go-ffi/utils.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package main
2+
3+
import "fmt"
4+
5+
// checkErr checks if err is not nil, and throws if so.
6+
// Shorthand to ease go's god awful error handling
7+
// from https://github.com/ethereum-optimism/optimism/blob/5bd1f4633195411b93d292f7b34e2565da7b773e/packages/contracts-bedrock/scripts/differential-testing/utils.go
8+
func checkErr(err error, failReason string) {
9+
if err != nil {
10+
panic(fmt.Errorf("%s: %w", failReason, err))
11+
}
12+
}

rvsol/test/CommonTest.sol

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity 0.8.15;
3+
4+
import {Test} from "forge-std/Test.sol";
5+
import {Vm} from "forge-std/Vm.sol";
6+
7+
/// @title FFIInterface
8+
/// @notice This contract is set into state using `etch` and therefore must not have constructor logic.
9+
/// It also MUST be compiled with `0.8.15` because `vm.getDeployedCode` will break if there
10+
/// are multiple artifacts for different compiler versions.
11+
contract FFIInterface {
12+
Vm internal constant vm = Vm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);
13+
14+
function getAsteriscMemoryProof(uint64 pc, uint32 insn) external returns (bytes32, bytes memory) {
15+
string[] memory cmds = new string[](5);
16+
cmds[0] = "../rvgo/scripts/go-ffi/go-ffi";
17+
cmds[1] = "diff";
18+
cmds[2] = "asteriscMemoryProof";
19+
cmds[3] = vm.toString(pc);
20+
cmds[4] = vm.toString(insn);
21+
bytes memory result = vm.ffi(cmds);
22+
(bytes32 memRoot, bytes memory proof) = abi.decode(result, (bytes32, bytes));
23+
return (memRoot, proof);
24+
}
25+
26+
function getAsteriscMemoryProof(uint64 pc, uint32 insn, uint64 memAddr, bytes32 memVal)
27+
external
28+
returns (bytes32, bytes memory)
29+
{
30+
string[] memory cmds = new string[](7);
31+
cmds[0] = "../rvgo/scripts/go-ffi/go-ffi";
32+
cmds[1] = "diff";
33+
cmds[2] = "asteriscMemoryProof";
34+
cmds[3] = vm.toString(pc);
35+
cmds[4] = vm.toString(insn);
36+
cmds[5] = vm.toString(memAddr);
37+
cmds[6] = vm.toString(memVal); // 0x prefixed hex string
38+
bytes memory result = vm.ffi(cmds);
39+
(bytes32 memRoot, bytes memory proof) = abi.decode(result, (bytes32, bytes));
40+
return (memRoot, proof);
41+
}
42+
}
43+
44+
/// @title CommonTest
45+
/// @dev An extension to `Test` that sets up the optimism smart contracts.
46+
contract CommonTest is Test {
47+
FFIInterface constant ffi = FFIInterface(address(uint160(uint256(keccak256(abi.encode("optimism.ffi"))))));
48+
49+
function setUp() public virtual {
50+
vm.etch(address(ffi), vm.getDeployedCode("CommonTest.sol:FFIInterface"));
51+
vm.label(address(ffi), "FFIInterface");
52+
}
53+
}

rvsol/test/RISCV.t.sol

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@ pragma solidity 0.8.15;
33
import {Test} from "forge-std/Test.sol";
44
import {RISCV} from "src/RISCV.sol";
55
import {PreimageOracle} from "@optimism/src/cannon/PreimageOracle.sol";
6+
import {CommonTest} from "./CommonTest.sol";
7+
// FIXME: somehow this import gives a multiple declaration error
8+
// This import is for the VMStatus
9+
// import "@optimism/src/libraries/DisputeTypes.sol";
610

7-
contract RISCV_Test is Test {
11+
contract RISCV_Test is CommonTest {
812
/// @notice Stores the VM state.
913
/// Total state size: 32 + 32 + 8 * 2 + 1 * 2 + 8 * 3 + 32 * 8 = 362 bytes
1014
/// Note that struct is not used for step execution and used only for testing
@@ -25,7 +29,8 @@ contract RISCV_Test is Test {
2529
RISCV internal riscv;
2630
PreimageOracle internal oracle;
2731

28-
function setUp() public {
32+
function setUp() public virtual override {
33+
super.setUp();
2934
oracle = new PreimageOracle(0, 0, 0);
3035
riscv = new RISCV(oracle);
3136
vm.store(address(riscv), 0x0, bytes32(abi.encode(address(oracle))));
@@ -55,6 +60,25 @@ contract RISCV_Test is Test {
5560
assertTrue(postState != bytes32(0));
5661
}
5762

63+
function test_add_succeeds() public {
64+
uint32 insn = encodeRType(0x33, 1, 0, 2, 3, 0); // add x1, x2, x3
65+
(State memory state, bytes memory proof) = constructRISCVState(0, insn);
66+
state.registers[2] = 0x3030;
67+
state.registers[3] = 0x3131;
68+
bytes memory encodedState = encodeState(state);
69+
70+
State memory expect;
71+
expect.memRoot = state.memRoot;
72+
expect.pc = state.pc + 4;
73+
expect.step = state.step + 1;
74+
expect.registers[1] = state.registers[2] + state.registers[3];
75+
expect.registers[2] = state.registers[2];
76+
expect.registers[3] = state.registers[3];
77+
78+
bytes32 postState = riscv.step(encodedState, proof, 0);
79+
assertEq(postState, outputState(expect), "unexpected post state");
80+
}
81+
5882
function encodeState(State memory state) internal pure returns (bytes memory) {
5983
bytes memory registers;
6084
for (uint256 i = 0; i < state.registers.length; i++) {
@@ -74,4 +98,59 @@ contract RISCV_Test is Test {
7498
);
7599
return stateData;
76100
}
101+
102+
/// @dev RISCV VM status codes:
103+
/// 0. Exited with success (Valid)
104+
/// 1. Exited with success (Invalid)
105+
/// 2. Exited with failure (Panic)
106+
/// 3. Unfinished
107+
// TODO: import DisputeTypes.sol. For some reason, import is not working
108+
function vmStatus(State memory state) internal pure returns (uint8 out_) {
109+
if (!state.exited) {
110+
return 3; // VMStatuses.UNFINISHED
111+
} else if (state.exitCode == 0) {
112+
return 0; // VMStatuses.VALID
113+
} else if (state.exitCode == 1) {
114+
return 1; // VMStatuses.INVALID
115+
} else {
116+
return 2; // VMStatuses.PANIC
117+
}
118+
}
119+
120+
function outputState(State memory state) internal pure returns (bytes32 out_) {
121+
bytes memory enc = encodeState(state);
122+
uint8 status = vmStatus(state);
123+
assembly {
124+
out_ := keccak256(add(enc, 0x20), 362)
125+
out_ := or(and(not(shl(248, 0xFF)), out_), shl(248, status))
126+
}
127+
}
128+
129+
function constructRISCVState(uint64 pc, uint32 insn, uint64 addr, bytes32 val)
130+
internal
131+
returns (State memory state, bytes memory proof)
132+
{
133+
(state.memRoot, proof) = ffi.getAsteriscMemoryProof(pc, insn, addr, val);
134+
state.pc = pc;
135+
}
136+
137+
function constructRISCVState(uint64 pc, uint32 insn) internal returns (State memory state, bytes memory proof) {
138+
(state.memRoot, proof) = ffi.getAsteriscMemoryProof(pc, insn);
139+
state.pc = pc;
140+
}
141+
142+
function encodeRType(uint8 opcode, uint8 rd, uint8 funct3, uint8 rs1, uint8 rs2, uint8 funct7)
143+
internal
144+
pure
145+
returns (uint32 insn)
146+
{
147+
// insn := [funct7] | [rs2] | [rs1] | [funct3] | [rd] | [opcode]
148+
// example: 0000000 | 00011 | 00010 | 000 | 00001 | 0110011
149+
insn = uint32(funct7 & 0x7F) << (7 + 5 + 3 + 5 + 5);
150+
insn |= uint32(rs2 & 0x1F) << (7 + 5 + 3 + 5);
151+
insn |= uint32(rs1 & 0x1F) << (7 + 5 + 3);
152+
insn |= uint32(funct3 & 0x7) << (7 + 5);
153+
insn |= uint32(rd & 0x1F) << 7;
154+
insn |= uint32(opcode & 0x7F);
155+
}
77156
}

0 commit comments

Comments
 (0)