From cd6acd4fc010c993e0692fb5bdc4bfaefc5e28e0 Mon Sep 17 00:00:00 2001 From: Daniel Kraft Date: Wed, 16 Oct 2024 07:54:51 +0200 Subject: [PATCH 1/2] Switch from Truffle to Foundry. This replaces Truffle to build the integrated smart contracts to Foundry. We use forge to build all the Solidity code, run the unit tests (recoded in Solidity), and update various places that access the build artefacts to the new path structure from Foundry. Dependencies (in particular, the XayaAccounts contract) are included now as git submodules, rather than via npm. The Docker build is updated, too. Since it seems very hard to install Foundry in Alpine (using prebuilt binaries that depend on glibc), we switch the Docker build to be based on Ubuntu. --- .gitignore | 2 - .gitmodules | 15 ++ Makefile.am | 2 - docker/Dockerfile | 35 ++-- docker/entrypoint.sh | 2 +- eth/gen-contract-constants.py | 19 +- eth/solidity/.gitignore | 3 +- eth/solidity/Makefile.am | 6 +- eth/solidity/foundry.toml | 6 + eth/solidity/lib/base64 | 1 + eth/solidity/lib/forge-std | 1 + eth/solidity/lib/openzeppelin-contracts | 1 + eth/solidity/lib/polygon-contract | 1 + eth/solidity/lib/wchi | 1 + eth/solidity/node_modules | 1 - eth/solidity/remappings.txt | 4 + eth/solidity/src/BuildWCHI.sol | 9 + eth/solidity/src/BuildXayaPolicy.sol | 11 ++ .../{contracts => src}/CallForwarder.sol | 0 .../{contracts => src}/TrackingAccounts.sol | 0 eth/solidity/test/MoveTracking.t.sol | 140 +++++++++++++++ .../{contracts => test}/MultiMover.sol | 0 eth/solidity/test/TestToken.sol | 21 +++ eth/solidity/test/movetracking.js | 166 ------------------ eth/solidity/truffle-config.js | 15 -- eth/tests/ethtest.py | 6 +- package.json | 11 -- xayax/Makefile.am | 8 +- xayax/eth.py | 9 +- 29 files changed, 259 insertions(+), 237 deletions(-) create mode 100644 .gitmodules create mode 100644 eth/solidity/foundry.toml create mode 160000 eth/solidity/lib/base64 create mode 160000 eth/solidity/lib/forge-std create mode 160000 eth/solidity/lib/openzeppelin-contracts create mode 160000 eth/solidity/lib/polygon-contract create mode 160000 eth/solidity/lib/wchi delete mode 120000 eth/solidity/node_modules create mode 100644 eth/solidity/remappings.txt create mode 100644 eth/solidity/src/BuildWCHI.sol create mode 100644 eth/solidity/src/BuildXayaPolicy.sol rename eth/solidity/{contracts => src}/CallForwarder.sol (100%) rename eth/solidity/{contracts => src}/TrackingAccounts.sol (100%) create mode 100644 eth/solidity/test/MoveTracking.t.sol rename eth/solidity/{contracts => test}/MultiMover.sol (100%) create mode 100644 eth/solidity/test/TestToken.sol delete mode 100644 eth/solidity/test/movetracking.js delete mode 100644 eth/solidity/truffle-config.js delete mode 100644 package.json diff --git a/.gitignore b/.gitignore index 14fa77d..7dc5c9b 100644 --- a/.gitignore +++ b/.gitignore @@ -12,8 +12,6 @@ libtool ltmain.sh m4 missing -node_modules -package-lock.json py-compile stamp-h1 test-driver diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..cada53b --- /dev/null +++ b/.gitmodules @@ -0,0 +1,15 @@ +[submodule "eth/solidity/lib/polygon-contract"] + path = eth/solidity/lib/polygon-contract + url = https://github.com/xaya/polygon-contract +[submodule "eth/solidity/lib/openzeppelin-contracts"] + path = eth/solidity/lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts +[submodule "eth/solidity/lib/base64"] + path = eth/solidity/lib/base64 + url = https://github.com/Brechtpd/base64 +[submodule "eth/solidity/lib/wchi"] + path = eth/solidity/lib/wchi + url = https://github.com/xaya/wchi +[submodule "eth/solidity/lib/forge-std"] + path = eth/solidity/lib/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/Makefile.am b/Makefile.am index 088a7b2..d3accf3 100644 --- a/Makefile.am +++ b/Makefile.am @@ -1,5 +1,3 @@ ACLOCAL_AMFLAGS = ${ACLOCAL_FLAGS} -Im4 SUBDIRS = src xayax xayacore eth - -EXTRA_DIST = node_modules diff --git a/docker/Dockerfile b/docker/Dockerfile index 112a798..dae9190 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -5,25 +5,32 @@ # libxayagame directly, it depends on libxayautil and many of the same base # packages like SQLite or libjson-rpc-cpp, so that reusing them for the # build from the existing package makes sense. -FROM xaya/libxayagame AS build -RUN apk add --no-cache \ +# +# It seems very hard to get the pre-built foundry binaries to run on Alpine, +# so we base the image on Ubuntu instead. +FROM xaya/libxayagame:ubuntu AS build +RUN apt update && apt -y install \ autoconf \ autoconf-archive \ automake \ - boost-dev \ - build-base \ - ca-certificates \ + libboost-all-dev \ + build-essential \ cmake \ + curl \ git \ - gflags-dev \ + libgflags-dev \ libtool \ - mariadb-dev \ - npm \ - pkgconfig \ - py3-pip \ + libmariadb-dev \ + pkg-config \ + python3-pip \ python3-dev -RUN pip3 install --break-system-packages web3 -RUN npm install -g truffle +RUN pip3 install web3 + +# Install Foundry. It seems pretty hard to get "foundryup" to run inside +# Docker, so we just download and manually install a specific release. +ARG FOUNDRY_RELEASE="nightly-adb6abae69c7a0d766db123f66686cc890c22dd0" +WORKDIR /usr/local/bin +RUN curl -L "https://github.com/foundry-rs/foundry/releases/download/${FOUNDRY_RELEASE}/foundry_nightly_linux_amd64.tar.gz" | tar zxv # Build and install libunivalue. ARG UNIVALUE_VERSION="v1.0.5" @@ -51,8 +58,6 @@ RUN ./autogen.sh && ./configure && make && make install-strip WORKDIR /usr/src/xayax COPY . . RUN make distclean || true -RUN npm install -RUN ln -s ../../node_modules eth/solidity/node_modules RUN ./autogen.sh && ./configure # Build the solidity contracts first, so that they are already # available for the python script that generates contract-constants.cpp. @@ -70,7 +75,7 @@ RUN for b in /usr/local/bin/xayax-*; \ COPY docker/entrypoint.sh bin/ # Construct the final image. -FROM alpine +FROM ubuntu:22.04 COPY --from=build /jail /usr/local/ COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ LABEL description="Xaya X connector binaries" diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 21a2502..8b91572 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -1,4 +1,4 @@ -#!/bin/sh -e +#!/bin/bash -e # If no HOST is explicitly set, try to detect it automatically. if [[ -z $HOST ]] diff --git a/eth/gen-contract-constants.py b/eth/gen-contract-constants.py index 0fd3f2d..4296aad 100755 --- a/eth/gen-contract-constants.py +++ b/eth/gen-contract-constants.py @@ -46,10 +46,15 @@ def genSignature (abi, typ, name): i["type"] for i in inp ]) + ")" +def contractPath (nm): + """ + Returns the path of the contract build artefact. + """ + + return os.path.join ("solidity", "out", f"{nm}.sol", f"{nm}.json") + # Load the XayaAccounts ABI to generate signature hashes. -with open (os.path.join ("solidity", "node_modules", "@xaya", - "eth-account-registry", "build", "contracts", - "XayaAccounts.json")) as f: +with open (contractPath ("XayaAccounts"), "rt") as f: accAbi = json.load (f)["abi"] sgn = genSignature (accAbi, "event", "Move") print ("const std::string MOVE_EVENT = \"" @@ -63,17 +68,15 @@ def outputFcnSelector (abi, name, var): + hexWithPrefix (Web3.keccak (sgn.encode ("ascii"))[:4]) + "\";") outputFcnSelector (accAbi, "wchiToken", "ACCOUNT_WCHI_FCN") -with open (os.path.join ("solidity", "build", "contracts", - "CallForwarder.json")) as f: +with open (contractPath ("CallForwarder"), "rt") as f: abi = json.load (f)["abi"] outputFcnSelector (abi, "execute", "FORWARDER_EXECUTE_FCN") # Store the deploying bytecode for our overlay contracts. def outputContract (name, var): - with open (os.path.join ("solidity", "build", "contracts", - f"{name}.json")) as f: + with open (contractPath (name), "rt") as f: data = json.load (f) - print (f"const std::string {var} = \"{data['bytecode']}\";") + print (f"const std::string {var} = \"{data['bytecode']['object']}\";") outputContract ("CallForwarder", "CALL_FORWARDER_CODE") outputContract ("TrackingAccounts", "TRACKING_ACCOUNTS_CODE") diff --git a/eth/solidity/.gitignore b/eth/solidity/.gitignore index 378eac2..1e4ded7 100644 --- a/eth/solidity/.gitignore +++ b/eth/solidity/.gitignore @@ -1 +1,2 @@ -build +cache +out diff --git a/eth/solidity/Makefile.am b/eth/solidity/Makefile.am index d7df03c..3e5fa70 100644 --- a/eth/solidity/Makefile.am +++ b/eth/solidity/Makefile.am @@ -1,8 +1,8 @@ all-local: - truffle compile + forge build check-local: - truffle test + forge test clean-local: - rm -rf build + rm -rf cache out diff --git a/eth/solidity/foundry.toml b/eth/solidity/foundry.toml new file mode 100644 index 0000000..955e1c7 --- /dev/null +++ b/eth/solidity/foundry.toml @@ -0,0 +1,6 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] +optimizer = true +optimizer_runs = 1_000 diff --git a/eth/solidity/lib/base64 b/eth/solidity/lib/base64 new file mode 160000 index 0000000..dcbf852 --- /dev/null +++ b/eth/solidity/lib/base64 @@ -0,0 +1 @@ +Subproject commit dcbf852ba545b3d15de0ac0ef88dce934c090c8e diff --git a/eth/solidity/lib/forge-std b/eth/solidity/lib/forge-std new file mode 160000 index 0000000..1de6eec --- /dev/null +++ b/eth/solidity/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 1de6eecf821de7fe2c908cc48d3ab3dced20717f diff --git a/eth/solidity/lib/openzeppelin-contracts b/eth/solidity/lib/openzeppelin-contracts new file mode 160000 index 0000000..dc44c9f --- /dev/null +++ b/eth/solidity/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit dc44c9f1a4c3b10af99492eed84f83ed244203f6 diff --git a/eth/solidity/lib/polygon-contract b/eth/solidity/lib/polygon-contract new file mode 160000 index 0000000..ddca994 --- /dev/null +++ b/eth/solidity/lib/polygon-contract @@ -0,0 +1 @@ +Subproject commit ddca9946f7ee85fc36c4632d2c028854384b8b70 diff --git a/eth/solidity/lib/wchi b/eth/solidity/lib/wchi new file mode 160000 index 0000000..a68ca6a --- /dev/null +++ b/eth/solidity/lib/wchi @@ -0,0 +1 @@ +Subproject commit a68ca6a5ad4e308ee9da1f24ec7c2feeb676070d diff --git a/eth/solidity/node_modules b/eth/solidity/node_modules deleted file mode 120000 index da10d6d..0000000 --- a/eth/solidity/node_modules +++ /dev/null @@ -1 +0,0 @@ -../../node_modules/ \ No newline at end of file diff --git a/eth/solidity/remappings.txt b/eth/solidity/remappings.txt new file mode 100644 index 0000000..5ddccdb --- /dev/null +++ b/eth/solidity/remappings.txt @@ -0,0 +1,4 @@ +base64-sol/=lib/base64/ +@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ +@xaya/eth-account-registry/contracts/=lib/polygon-contract/contracts/ +@xaya/wchi/contracts/=lib/wchi/contracts/ diff --git a/eth/solidity/src/BuildWCHI.sol b/eth/solidity/src/BuildWCHI.sol new file mode 100644 index 0000000..063bb6c --- /dev/null +++ b/eth/solidity/src/BuildWCHI.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +// Copyright (C) 2024 The Xaya developers + +pragma solidity ^0.7.6; + +/* This file is here just to force Forge to build the WCHI contract, + which is used for the Python testing package. */ + +import "@xaya/wchi/contracts/WCHI.sol"; diff --git a/eth/solidity/src/BuildXayaPolicy.sol b/eth/solidity/src/BuildXayaPolicy.sol new file mode 100644 index 0000000..bc20c81 --- /dev/null +++ b/eth/solidity/src/BuildXayaPolicy.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +// Copyright (C) 2024 The Xaya developers + +pragma solidity ^0.8.4; + +/* This file is here just to force Forge to build the XayaPolicy and NftMetadata + contracts, too, whose build artefacts we need for the Python testing + package. */ + +import "@xaya/eth-account-registry/contracts/NftMetadata.sol"; +import "@xaya/eth-account-registry/contracts/XayaPolicy.sol"; diff --git a/eth/solidity/contracts/CallForwarder.sol b/eth/solidity/src/CallForwarder.sol similarity index 100% rename from eth/solidity/contracts/CallForwarder.sol rename to eth/solidity/src/CallForwarder.sol diff --git a/eth/solidity/contracts/TrackingAccounts.sol b/eth/solidity/src/TrackingAccounts.sol similarity index 100% rename from eth/solidity/contracts/TrackingAccounts.sol rename to eth/solidity/src/TrackingAccounts.sol diff --git a/eth/solidity/test/MoveTracking.t.sol b/eth/solidity/test/MoveTracking.t.sol new file mode 100644 index 0000000..fec2938 --- /dev/null +++ b/eth/solidity/test/MoveTracking.t.sol @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: MIT +// Copyright (C) 2021-2024 The Xaya developers + +pragma solidity ^0.8.4; + +import "./MultiMover.sol"; +import "./TestToken.sol"; +import "../src/CallForwarder.sol"; +import "../src/TrackingAccounts.sol"; + +import "@xaya/eth-account-registry/contracts/TestPolicy.sol"; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { Test } from "forge-std/Test.sol"; + +/** + * @dev Unit tests for call forwarding and tracking moves with the + * instrumentation contracts. + */ +contract MoveTrackingTest is Test +{ + + address public constant operator = address (1); + + IERC20 public wchi; + + TrackingAccounts public xa; + CallForwarder public fwd; + MultiMover public mover; + + function setUp () public + { + wchi = new TestToken (operator, 78e6 * 1e8); + IXayaPolicy policy = new TestPolicy (); + + vm.startPrank (operator); + xa = new TrackingAccounts (wchi); + xa.schedulePolicyChange (policy); + vm.warp (xa.policyTimelock () + 1); + xa.enactPolicyChange (); + vm.stopPrank (); + + fwd = new CallForwarder (xa); + mover = new MultiMover (xa); + + /* The TestPolicy imposes a WCHI fee for moves, so we need to make sure that + the fwd contract has WCHI and the necessary approvals. */ + vm.startPrank (operator); + wchi.transfer (address (fwd), 1e8); + wchi.transfer (address (mover), 1e8); + vm.startPrank (address (fwd)); + wchi.approve (address (xa), type (uint256).max); + wchi.approve (address (mover), type (uint256).max); + xa.setApprovalForAll (address (mover), true); + vm.stopPrank (); + + /* The test name is owned by the fwd contract, and can thus be moved + by a forwarded call. */ + vm.prank (address (fwd)); + xa.register ("p", "test"); + } + + function test_trackDirectMoves () public + { + TrackingAccounts.MoveData[] memory res = fwd.execute (address (xa), + abi.encodeWithSelector (TrackingAccounts.move.selector, + "p", "test", "x", type (uint256).max, 0, address (0))); + assertEq (res.length, 1); + assertEq (res[0].ns, "p"); + assertEq (res[0].name, "test"); + assertEq (res[0].move, "x"); + assertEq (res[0].nonce, 0); + assertEq (res[0].mover, address (fwd)); + assertEq (res[0].amount, 0); + assertEq (res[0].receiver, address (0)); + + res = fwd.execute (address (xa), + abi.encodeWithSelector (TrackingAccounts.move.selector, + "p", "test", "y", type (uint256).max, 0, address (0))); + assertEq (res.length, 1); + assertEq (res[0].move, "y"); + assertEq (res[0].nonce, 1); + } + + function test_revertingMove () public + { + vm.expectRevert (); + fwd.execute (address (xa), + abi.encodeWithSelector (TrackingAccounts.move.selector, + "p", "test", "", type (uint256).max, 0, address (0))); + } + + function test_moveWithChiPayment () public + { + address to = address (2); + TrackingAccounts.MoveData[] memory res = fwd.execute (address (xa), + abi.encodeWithSelector (TrackingAccounts.move.selector, + "p", "test", "x", type (uint256).max, 42, to)); + assertEq (res.length, 1); + assertEq (res[0].amount, 42); + assertEq (res[0].receiver, to); + assertEq (wchi.balanceOf (to), 42); + } + + function test_multipleMoves () public + { + string[] memory ns = new string[] (1); + ns[0] = "p"; + string[] memory name = new string[] (1); + name[0] = "test"; + string[] memory values = new string[] (2); + values[0] = "x"; + values[1] = "y"; + + TrackingAccounts.MoveData[] memory res = fwd.execute (address (mover), + abi.encodeWithSelector (MultiMover.send.selector, ns, name, values)); + assertEq (res.length, 2); + assertEq (res[0].move, "x"); + assertEq (res[0].nonce, 0); + assertEq (res[1].move, "y"); + assertEq (res[1].nonce, 1); + } + + function test_ethPayment () public + { + bytes memory inner = abi.encodeWithSelector (MultiMover.requireEth.selector, + "p", "test", "x"); + (bool sent, bytes memory data) = address (fwd).call {value: 20} ( + abi.encodeWithSelector (CallForwarder.execute.selector, + address (mover), inner)); + assertTrue (sent); + + TrackingAccounts.MoveData[] memory res + = abi.decode (data, (TrackingAccounts.MoveData[])); + assertEq (res.length, 1); + assertEq (res[0].move, "x"); + } + +} diff --git a/eth/solidity/contracts/MultiMover.sol b/eth/solidity/test/MultiMover.sol similarity index 100% rename from eth/solidity/contracts/MultiMover.sol rename to eth/solidity/test/MultiMover.sol diff --git a/eth/solidity/test/TestToken.sol b/eth/solidity/test/TestToken.sol new file mode 100644 index 0000000..45d79eb --- /dev/null +++ b/eth/solidity/test/TestToken.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +// Copyright (C) 2024 The Xaya developers + +pragma solidity ^0.8.4; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +/** + * @dev Simple test token (representing WCHI) that just mints all the + * supply to a given address. + */ +contract TestToken is ERC20 +{ + + constructor (address holder, uint supply) + ERC20 ("Wrapped CHI", "WCHI") + { + _mint (holder, supply); + } + +} diff --git a/eth/solidity/test/movetracking.js b/eth/solidity/test/movetracking.js deleted file mode 100644 index 2f8a84a..0000000 --- a/eth/solidity/test/movetracking.js +++ /dev/null @@ -1,166 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright (C) 2021-2022 Autonomous Worlds Ltd - -const truffleAssert = require ("truffle-assertions"); -const truffleContract = require ("@truffle/contract"); -const { time } = require ("@openzeppelin/test-helpers"); - -/* We want to use chai-subset for checking the MoveData structs easily, - but due to an open issue with Truffle we can only apply the plugin - if we use our own Chai (for these assertions at least): - - https://github.com/trufflesuite/truffle/issues/2090 -*/ -const chai = require ("chai"); -const chaiSubset = require ("chai-subset"); -chai.use (chaiSubset); - -const loadXayaContract = (pkg, name) => { - const path = "@xaya/" + pkg + "/build/contracts/" + name + ".json"; - const data = require (path); - const res = truffleContract (data); - res.setProvider (web3.currentProvider); - return res; -}; - -const WCHI = loadXayaContract ("wchi", "WCHI"); -const TestPolicy = loadXayaContract ("eth-account-registry", "TestPolicy"); - -const TrackingAccounts = artifacts.require ("TrackingAccounts"); -const CallForwarder = artifacts.require ("CallForwarder"); -const MultiMover = artifacts.require ("MultiMover"); - -const zeroAddr = "0x0000000000000000000000000000000000000000"; -const maxUint256 = "115792089237316195423570985008687907853269984665640564039457584007913129639935"; -const bnMaxUint256 = web3.utils.toBN (maxUint256); -const noNonce = bnMaxUint256; - -/*** ************************************************************************ */ - -contract ("CallForwarder", accounts => { - const operator = accounts[0]; - const op = {"from": operator}; - - let wchi, policy; - before (async () => { - wchi = await WCHI.new (op); - policy = await TestPolicy.new (op); - }); - - let xa, fwd, mover; - beforeEach (async () => { - xa = await TrackingAccounts.new (wchi.address, op); - await wchi.approve (xa.address, bnMaxUint256, op); - - await xa.schedulePolicyChange (policy.address, op); - time.increase ((await xa.policyTimelock ()) + 1); - await xa.enactPolicyChange (op); - - fwd = await CallForwarder.new (xa.address, op); - mover = await MultiMover.new (xa.address, op); - - /* The test name is owned by the fwd contract, and can thus be moved - by a forwarded call. */ - await xa.register ("p", "test", op); - const token = await xa.tokenIdForName ("p", "test"); - await xa.transferFrom (operator, fwd.address, token, op); - - /* The TestPolicy imposes a WCHI fee for moves, so we need to make sure - that the fwd contract has WCHI and the necessary approvals. */ - await wchi.transfer (fwd.address, 100, op); - await wchi.transfer (mover.address, 100, op); - const execFwd = (to, call) => { - const data = call.encodeABI (); - return fwd.execute (to, data, op); - }; - await execFwd (wchi.address, - wchi.contract.methods.approve (xa.address, 100)); - await execFwd (wchi.address, - wchi.contract.methods.approve (mover.address, 100)); - await execFwd (xa.address, - xa.contract.methods.setApprovalForAll (mover.address, true)); - }); - - /* Helper method that performs a (simulated) call through the - forwarder contract. to is the target address and call the - web3 contract method instance. */ - const forward = (to, call) => { - const data = call.encodeABI (); - return fwd.execute.call (to, data); - }; - - /* ************************************************************************ */ - - it ("should track direct moves", async () => { - let res; - res = await forward (xa.address, - xa.contract.methods.move ( - "p", "test", "x", noNonce, 0, zeroAddr)); - assert.lengthOf (res, 1); - chai.expect (res[0]).to.containSubset ({ - "ns": "p", - "name": "test", - "move": "x", - "nonce": "0", - "mover": fwd.address, - "amount": "0", - "receiver": zeroAddr, - }); - - res = await forward (xa.address, - xa.contract.methods.move ( - "p", "test", "y", noNonce, 0, zeroAddr)); - assert.lengthOf (res, 1); - chai.expect (res[0]).to.containSubset ({ - "move": "y", - }); - }); - - it ("should handle moves that revert", async () => { - await truffleAssert.reverts ( - forward (xa.address, - xa.contract.methods.move ( - "p", "test", "", noNonce, 0, zeroAddr))); - }); - - it ("should track moves with CHI payment", async () => { - const to = accounts[1]; - const res = await forward (xa.address, - xa.contract.methods.move ( - "p", "test", "x", noNonce, 42, to)); - assert.lengthOf (res, 1); - chai.expect (res[0]).to.containSubset ({ - "amount": "42", - "receiver": to, - }); - }); - - it ("should handle multiple moves in a transaction", async () => { - const res = await forward (mover.address, - mover.contract.methods.send ( - ["p"], ["test"], ["x", "y"])); - assert.lengthOf (res, 2); - chai.expect (res[0]).to.containSubset ({ - "move": "x", - "nonce": "0", - }); - chai.expect (res[1]).to.containSubset ({ - "move": "y", - "nonce": "1", - }); - }); - - it ("should handle ETH payments with forwarded calls", async () => { - const call = mover.contract.methods.requireEth ("p", "test", "x"); - const data = call.encodeABI (); - - const res = await fwd.execute.call (mover.address, data, {"value": 20}); - assert.lengthOf (res, 1); - chai.expect (res[0]).to.containSubset ({ - "move": "x", - }); - }); - - /* ************************************************************************ */ - -}); diff --git a/eth/solidity/truffle-config.js b/eth/solidity/truffle-config.js deleted file mode 100644 index 20ba80d..0000000 --- a/eth/solidity/truffle-config.js +++ /dev/null @@ -1,15 +0,0 @@ -module.exports = { - compilers: { - solc: { - version: "^0.8.4", - settings: { - optimizer: { - enabled: true, - runs: 100 - } - }, - evmVersion: "london" - } - }, - plugins: ["solidity-coverage"] -}; diff --git a/eth/tests/ethtest.py b/eth/tests/ethtest.py index d180ed1..7041989 100644 --- a/eth/tests/ethtest.py +++ b/eth/tests/ethtest.py @@ -64,10 +64,10 @@ def deployMultiMover (self, env=None): """ scriptPath = os.path.dirname (os.path.abspath (__file__)) - contracts = os.path.join (scriptPath, "..", - "solidity", "build", "contracts") + outdir = os.path.join (scriptPath, "..", "solidity", "out") - with open (os.path.join (contracts, "MultiMover.json")) as f: + with open (os.path.join (outdir, "MultiMover.sol", "MultiMover.json"), + "rt") as f: data = json.load (f) if env is None: diff --git a/package.json b/package.json deleted file mode 100644 index 20e634b..0000000 --- a/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "devDependencies": { - "chai": "", - "chai-subset": "", - "truffle-assertions": "", - "@openzeppelin/test-helpers": "", - "@truffle/contract": "", - "@xaya/wchi": "1.0.0", - "@xaya/eth-account-registry": "1.0.0" - } -} diff --git a/xayax/Makefile.am b/xayax/Makefile.am index 512fce3..1fb5e94 100644 --- a/xayax/Makefile.am +++ b/xayax/Makefile.am @@ -13,8 +13,6 @@ xayax_PYTHON = __init__.py \ testcase.py xayax_DATA = $(CONTRACTS) -xayamod = $(top_srcdir)/node_modules/@xaya -$(WCHI_CONTRACTS): %: $(xayamod)/wchi/build/contracts/% - cp $< $@ -$(ACCOUNTS_CONTRACTS): %: $(xayamod)/eth-account-registry/build/contracts/% - cp $< $@ +forgeout = $(top_builddir)/eth/solidity/out +$(CONTRACTS): %.json: $(forgeout)/%.sol + cp $ Date: Fri, 18 Oct 2024 14:42:55 +0200 Subject: [PATCH 2/2] Use anvil instead of ganache. For integration testing on EVM chains (also the environment that will be used from libxayagame), use Foundry's anvil instead of ganache. --- eth/ethchain.cpp | 4 ++ eth/pending.cpp | 1 + eth/tests/ethtest.py | 8 ++-- eth/tests/pending.py | 2 +- xayax/eth.py | 96 +++++++++++++++++++++----------------------- 5 files changed, 56 insertions(+), 55 deletions(-) diff --git a/eth/ethchain.cpp b/eth/ethchain.cpp index a874514..96bb420 100644 --- a/eth/ethchain.cpp +++ b/eth/ethchain.cpp @@ -80,6 +80,10 @@ const std::map CHAIN_IDS = {137, "polygon"}, {80'001, "mumbai"}, {1'337, "ganache"}, + /* This is anvil from Foundry, but in essence does the same as ganache + (i.e. local regtest-like testing on EVM chains). Which it is does not + matter from the game's point of view. */ + {31'337, "ganache"}, }; /** diff --git a/eth/pending.cpp b/eth/pending.cpp index 302a8ab..0ee03ff 100644 --- a/eth/pending.cpp +++ b/eth/pending.cpp @@ -128,6 +128,7 @@ PendingDataExtractor::GetMoves (EthRpcClient& rpc, possible, while returning any move events generated. */ Json::Value tx(Json::objectValue); tx["to"] = from.GetChecksummed (); + tx["from"] = from.GetChecksummed (); tx["value"] = data["value"]; tx["data"] = AbiEncoder::ConcatHex (FORWARDER_EXECUTE_FCN, execArgs.Finalise ()); diff --git a/eth/tests/ethtest.py b/eth/tests/ethtest.py index 7041989..5c561b7 100644 --- a/eth/tests/ethtest.py +++ b/eth/tests/ethtest.py @@ -1,4 +1,4 @@ -# Copyright (C) 2021 The Xaya developers +# Copyright (C) 2021-2024 The Xaya developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. @@ -29,7 +29,7 @@ def getXayaXExtraArgs (self): @contextmanager def environment (self): with super ().environment (): - self.w3 = self.env.ganache.w3 + self.w3 = self.env.evm.w3 yield def createBaseChain (self): @@ -73,8 +73,8 @@ def deployMultiMover (self, env=None): if env is None: env = self.env contracts = env.contracts - deployed = env.ganache.deployContract (contracts.account, data, - contracts.registry.address) + deployed = env.evm.deployContract (contracts.account, data, + contracts.registry.address) contracts.registry.functions\ .setApprovalForAll (deployed.address, True)\ diff --git a/eth/tests/pending.py b/eth/tests/pending.py index a8853d0..049be77 100755 --- a/eth/tests/pending.py +++ b/eth/tests/pending.py @@ -187,7 +187,7 @@ def extractMvids (data): }, ]) - # FIXME: It seems Ganache is not resurrecting transactions. + # FIXME: It seems Anvil is not resurrecting transactions. # Look into this and see what to do for testing reorgs. if False: snapshot.restore () diff --git a/xayax/eth.py b/xayax/eth.py index 4276630..58bb264 100644 --- a/xayax/eth.py +++ b/xayax/eth.py @@ -168,10 +168,9 @@ def run (self, *args, **kwargs): self.stop () -class Ganache: +class EvmNode: """ - A process running a local Ethereum-like blockchain using ganache-cli - for testing. + A process running a local Ethereum-like blockchain for testing. """ def __init__ (self, basedir, rpcPort): @@ -180,17 +179,12 @@ def __init__ (self, basedir, rpcPort): """ self.log = logging.getLogger ("xayax.eth") - self.datadir = os.path.join (basedir, "ganache-cli") + self.basedir = basedir self.port = rpcPort self.rpcurl = "http://localhost:%d" % self.port self.wsurl = "ws://localhost:%d" % self.port - self.log.info ("Creating fresh data directory for Ganache-CLI in %s" - % self.datadir) - shutil.rmtree (self.datadir, ignore_errors=True) - os.mkdir (self.datadir) - self.proc = None def start (self): @@ -199,30 +193,25 @@ def start (self): """ if self.proc is not None: - self.log.error ("Ganache process is already running, not starting again") + self.log.error ("EVM node process is already running, not starting again") return - self.log.info ("Starting new Ganache-CLI process") - args = ["/usr/bin/env", "ganache"] + self.log.info ("Starting new EVM node process") + args = ["/usr/bin/env", "anvil"] args.extend (["-p", str (self.port)]) - args.extend (["--db", self.datadir]) - # By default, Ganache mines a new block on each transaction sent. - # This is not what we want for testing in a Xaya-like environment; thus - # we set a very long block interval, which will be so long that no blocks - # get actually mined automatically. Tests can mine on demand instead. - args.extend (["-b", str (1_000_000)]) + # By default, Anvil mines a new block on each transaction sent. + # This is not what we want for testing in a Xaya-like environment; + # tests will mine on demand instead. + args.append ("--no-mining") # Use a timestamp "early" into Xaya history. This ensures that the # genesis block will be before anything programmed into a particular # game, as would be the case with a real network. - args.extend (["-t", "2018-01-01Z00:00:00"]) - # We want eth_call to throw in case of a tx revert (this is also what - # e.g. geth does). - args.append ("--chain.vmErrorsOnRPCResponse") - self.logFile = open (os.path.join (self.datadir, "ganache.log"), "wt") + args.extend (["--timestamp", "1514764800"]) + self.logFile = open (os.path.join (self.basedir, "anvil.log"), "wt") self.proc = subprocess.Popen (args, stderr=subprocess.STDOUT, stdout=self.logFile) - # The Ganache wrapper has an optional mock time, which can be set + # The EVM node wrapper has an optional mock time, which can be set # from the outside and which (if set) is used as argument to evm_mine # to determine the next block's timestamp. self.mockTime = None @@ -232,7 +221,7 @@ def start (self): while True: try: chainId = int (self.rpc.eth_chainId (), 16) - self.log.info ("Ganache is up, chain = %d" % chainId) + self.log.info ("EVM node is up, chain = %d" % chainId) break except: time.sleep (0.1) @@ -241,7 +230,7 @@ def start (self): def stop (self): if self.proc is None: - self.log.error ("No Ganache process is running, cannot stop it") + self.log.error ("No EVM node process is running, cannot stop it") return if self.logFile is not None: @@ -250,7 +239,7 @@ def stop (self): self.w3 = None - self.log.info ("Stopping Ganache process") + self.log.info ("Stopping EVM node process") self.proc.terminate () self.log.info ("Waiting for process to stop...") @@ -263,7 +252,12 @@ def createRpc (self): be used if multiple threads need to send RPCs in parallel. """ - return jsonrpclib.ServerProxy (self.rpcurl) + # Anvil does not accept the default application/json-rpc content type + # sent by jsonrpclib, and wants application/json as content type. + config = jsonrpclib.config.DEFAULT + config.content_type = "application/json" + + return jsonrpclib.ServerProxy (self.rpcurl, config=config) @contextmanager def run (self, *args, **kwargs): @@ -287,6 +281,9 @@ def mine (self): self.rpc.evm_mine () else: self.rpc.evm_mine (self.mockTime) + # We need to bump the mock time at least by one second + # so the next block can be mined, too. + self.mockTime += 1 def deployContract (self, addr, data, *args, **kwargs): """ @@ -352,11 +349,11 @@ class XayaDeployment: class ChainSnapshot: """ - A snapshot of the Ganache test blockchain. It can be created from the + A snapshot of the EVM test blockchain. It can be created from the current state, and then we can always revert back to it at later points in time as desired. - This is a wrapper around Ganache's evm_snapshot and evm_revert RPC methods, + This is a wrapper around the debug evm_snapshot and evm_revert RPC methods, which takes care of the underlying snapshot ID and also allows us to revert back to a snapshot multiple times. @@ -374,7 +371,7 @@ def takeSnapshot (self): def restore (self): self.rpc.evm_revert (self.id) - # Ganache allows a snapshot to be used only once. Thus take a new one + # The node allows a snapshot to be used only once. Thus take a new one # now (at the same state) so we can revert again in the future. self.takeSnapshot () @@ -382,12 +379,11 @@ def restore (self): class Environment: """ A full test environment consisting of a local Ethereum chain - (using ganache-cli) with the Xaya contracts deployed, and a Xaya X - process connected to it. + with the Xaya contracts deployed, and a Xaya X process connected to it. When running, it exposes the RPC interface of the Xaya X process, which should be connected to the GSP, and also the RPC interface of the - ganache-cli process (through JSON-RPC as well as Web3.py) for controlling + EVM node process (through JSON-RPC as well as Web3.py) for controlling of the environment in a test. """ @@ -395,7 +391,7 @@ def __init__ (self, basedir, portgen, xayaxBinary): zmqPorts = { "hashblock": next (portgen), } - self.ganache = Ganache (basedir, next (portgen)) + self.evm = EvmNode (basedir, next (portgen)) self.xnode = Instance (basedir, portgen, xayaxBinary) self.log = logging.getLogger ("xayax.eth") self.watchForPending = [] @@ -441,29 +437,29 @@ def run (self): as a context manager. """ - with self.ganache.run (): + with self.evm.run (): self.signerAccounts = {} - self.contracts = self.ganache.deployXaya () + self.contracts = self.evm.deployXaya () self.log.info ("WCHI contract: %s" % self.contracts.wchi.address) self.log.info ("Accounts contract: %s" % self.contracts.registry.address) - self.ganache.w3.eth.default_account = self.contracts.account + self.evm.w3.eth.default_account = self.contracts.account self.clearRegisteredCache () if self.defaultPending: self.addWatchedContract (self.contracts.registry.address) for cb in self.deploymentCbs: cb (self) - with self.xnode.run (self.contracts.registry.address, self.ganache.rpcurl, - ws=self.ganache.wsurl, + with self.xnode.run (self.contracts.registry.address, self.evm.rpcurl, + ws=self.evm.wsurl, watchForPending=self.watchForPending): yield self - def createGanacheRpc (self): + def createEvmRpc (self): """ - Returns a fresh RPC handle for talking directly to the Ganache-CLI - node, e.g. for sending moves, mining or triggering reorgs. + Returns a fresh RPC handle for talking directly to the EVM node, + e.g. for sending moves, mining or triggering reorgs. """ - return self.ganache.createRpc () + return self.evm.createRpc () def getXRpcUrl (self): """ @@ -482,7 +478,7 @@ def snapshot (self): at a later time (e.g. for testing reorgs). """ - return ChainSnapshot (self.createGanacheRpc ()) + return ChainSnapshot (self.createEvmRpc ()) def setMockTime (self, timestamp): """ @@ -490,8 +486,8 @@ def setMockTime (self, timestamp): in the future will use. """ - self.log.info ("Setting mocktime for Ganache to %d" % timestamp) - self.ganache.mockTime = timestamp + self.log.info ("Setting mocktime for EVM node to %d" % timestamp) + self.evm.mockTime = timestamp def clearRegisteredCache (self): """ @@ -508,13 +504,13 @@ def clearRegisteredCache (self): def generate (self, num): blks = [] while len (blks) < num: - self.ganache.mine () + self.evm.mine () blk, _ = self.getChainTip () blks.append (blk) return blks def getChainTip (self): - data = self.ganache.w3.eth.get_block ("latest") + data = self.evm.w3.eth.get_block ("latest") return uintToXaya (data["hash"].hex ()), data["number"] def createSignerAddress (self): @@ -526,7 +522,7 @@ def signMessage (self, addr, msg): account = self.lookupSignerAccount (addr) assert account is not None, "%s is not a signer address" % addr full = "Xaya signature for chain %d:\n\n%s" \ - % (self.ganache.w3.eth.chain_id, msg) + % (self.evm.w3.eth.chain_id, msg) encoded = messages.encode_defunct (text=full) rawSgn = account.sign_message (encoded).signature return codecs.decode (base64.b64encode (rawSgn), "ascii")