From ed5306e53122867d7b989653a06b41343940a4b6 Mon Sep 17 00:00:00 2001 From: Ferran Borreguero Date: Fri, 5 Jan 2024 21:25:11 +0100 Subject: [PATCH] Suave forge integration without SuaveForge.sol (#11) * Some hacks on forge * return sig-stripped calldata from fallback (#10) * Oh my... this really works * Do not mirror SuaveForge.sol anymore * More updates * Write comment * Fix CI * Enable ffi on ci test * Add REAMDE * Add lang to readme code --------- Co-authored-by: brock smedley <2791467+zeroXbrock@users.noreply.github.com> --- .github/workflows/ci.yml | 23 +++- .github/workflows/suave-lib-sync.yml | 2 - README.md | 35 ++++++ src/Test.sol | 15 +++ src/forge/Connector.sol | 47 ++++++++ src/forge/Registry.sol | 40 +++++++ src/suavelib/SuaveForge.sol | 167 -------------------------- test/Forge.t.sol | 20 ++++ tools/forge-gen/README.md | 13 +++ tools/forge-gen/go.mod | 3 + tools/forge-gen/main.go | 168 +++++++++++++++++++++++++++ 11 files changed, 363 insertions(+), 170 deletions(-) create mode 100644 src/Test.sol create mode 100644 src/forge/Connector.sol create mode 100644 src/forge/Registry.sol delete mode 100644 src/suavelib/SuaveForge.sol create mode 100644 test/Forge.t.sol create mode 100644 tools/forge-gen/README.md create mode 100644 tools/forge-gen/go.mod create mode 100644 tools/forge-gen/main.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fb7f9dc..88886fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,6 +14,27 @@ jobs: steps: - uses: actions/checkout@v2 + - name: Checkout suave-geth repo + uses: actions/checkout@v4 + with: + repository: flashbots/suave-geth + path: suave-geth + persist-credentials: false + fetch-depth: 0 + + - name: Build suave + run: | + cd suave-geth + make suave + + - name: Include the binary on $PATH + run: | + echo "$(pwd)/suave-geth/build/bin" >> $GITHUB_PATH + + - name: Run suave + run: | + ./suave-geth/build/bin/suave --suave.dev & + - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: @@ -23,4 +44,4 @@ jobs: run: forge install - name: Run tests - run: forge test + run: forge test --ffi diff --git a/.github/workflows/suave-lib-sync.yml b/.github/workflows/suave-lib-sync.yml index 09552e7..cc581d5 100644 --- a/.github/workflows/suave-lib-sync.yml +++ b/.github/workflows/suave-lib-sync.yml @@ -36,9 +36,7 @@ jobs: - name: Mirror run: | cp suave-geth/suave/sol/libraries/Suave.sol ./src/suavelib/Suave.sol - cp suave-geth/suave/sol/libraries/SuaveForge.sol ./src/suavelib/SuaveForge.sol git add ./src/suavelib/Suave.sol - git add ./src/suavelib/SuaveForge.sol rm -rf suave-geth - name: Create Pull Request diff --git a/README.md b/README.md index 27c171a..144eebd 100644 --- a/README.md +++ b/README.md @@ -39,3 +39,38 @@ contract Example { } } ``` + +## Forge integration + +In order to use `forge`, you need to have a running `Suave` node and the `suave` binary in your path. + +To run `Suave` in development mode, use the following command: + +```bash +$ suave --suave.dev +``` + +Then, your `forge` scripts/test must import the `SuaveEnabled` contract from the `suave-std/Test.sol` file. + +```solidity +// SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import "suave-std/Test.sol"; +import "suave-std/Suave.sol"; + +contract TestForge is Test, SuaveEnabled { + address[] public addressList = [0xC8df3686b4Afb2BB53e60EAe97EF043FE03Fb829]; + + function testConfidentialStore() public { + Suave.DataRecord memory record = Suave.newDataRecord(0, addressList, addressList, "namespace"); + + bytes memory value = abi.encode("suave works with forge!"); + Suave.confidentialStore(record.id, "key1", value); + + bytes memory found = Suave.confidentialRetrieve(record.id, "key1"); + assertEq(keccak256(found), keccak256(value)); + } +} +``` diff --git a/src/Test.sol b/src/Test.sol new file mode 100644 index 0000000..9a78e0f --- /dev/null +++ b/src/Test.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.8; + +import "./forge/Registry.sol"; + +contract SuaveEnabled { + function setUp() public { + // TODO: Add checks to validate that: + // - User is running the test with ffi. Since vm.ffi is deployed as a contract, the error if ffi is not active + // is reported as a Suave.PeekerReverted error and it is not clear what the problem is. + // - Suave binary is on $PATH and Suave is running. This could be done with ffi calls to the suave binary. + // Put this logic inside `enable` itself. + Registry.enable(); + } +} diff --git a/src/forge/Connector.sol b/src/forge/Connector.sol new file mode 100644 index 0000000..2e2f4fd --- /dev/null +++ b/src/forge/Connector.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.8; + +interface connectorVM { + function ffi(string[] calldata commandInput) external view returns (bytes memory result); +} + +contract Connector { + connectorVM constant vm = connectorVM(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + + function forgeIt(bytes memory addr, bytes memory data) internal view returns (bytes memory) { + string memory addrHex = iToHex(addr); + string memory dataHex = iToHex(data); + + string[] memory inputs = new string[](4); + inputs[0] = "suave"; + inputs[1] = "forge"; + inputs[2] = addrHex; + inputs[3] = dataHex; + + bytes memory res = vm.ffi(inputs); + return res; + } + + function iToHex(bytes memory buffer) public pure returns (string memory) { + bytes memory converted = new bytes(buffer.length * 2); + + bytes memory _base = "0123456789abcdef"; + + for (uint256 i = 0; i < buffer.length; i++) { + converted[i * 2] = _base[uint8(buffer[i]) / _base.length]; + converted[i * 2 + 1] = _base[uint8(buffer[i]) % _base.length]; + } + + return string(abi.encodePacked("0x", converted)); + } + + fallback() external { + bytes memory msgdata = forgeIt(abi.encodePacked(address(this)), msg.data); + + assembly { + let location := msgdata + let length := mload(msgdata) + return(add(location, 0x20), length) + } + } +} diff --git a/src/forge/Registry.sol b/src/forge/Registry.sol new file mode 100644 index 0000000..526181d --- /dev/null +++ b/src/forge/Registry.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.8; + +import "../suavelib/Suave.sol"; + +interface registryVM { + function etch(address, bytes calldata) external; +} + +library Registry { + registryVM constant vm = registryVM(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + + function enableLib(address addr) public { + // code for Wrapper + bytes memory code = + hex"608060405234801561001057600080fd5b506004361061002b5760003560e01c8063671ff786146100a1575b6040516bffffffffffffffffffffffff193060601b1660208201526000906100959060340160408051808303601f19018152602036601f8101829004820285018201909352828452909291600091819084018382808284376000920191909152506100ca92505050565b90508081518060208301f35b6100b46100af36600461048c565b610259565b6040516100c1919061055c565b60405180910390f35b606060006100d784610259565b905060006100e484610259565b60408051600480825260a0820190925291925060009190816020015b606081526020019060019003908161010057905050905060405180604001604052806005815260200164737561766560d81b8152508160008151811061014857610148610576565b602002602001018190525060405180604001604052806005815260200164666f72676560d81b8152508160018151811061018457610184610576565b602002602001018190525082816002815181106101a3576101a3610576565b602002602001018190525081816003815181106101c2576101c2610576565b6020908102919091010152604051638916046760e01b8152600090737109709ecfa91a80626ff3989d68f67f5b1dd12d9063891604679061020790859060040161058c565b600060405180830381865afa158015610224573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405261024c91908101906105ee565b9450505050505b92915050565b606060008251600261026b919061067b565b67ffffffffffffffff8111156102835761028361041d565b6040519080825280601f01601f1916602001820160405280156102ad576020820181803683370190505b5060408051808201909152601081526f181899199a1a9b1b9c1cb0b131b232b360811b602082015290915060005b84518110156103f3578182518683815181106102f9576102f9610576565b016020015161030b919060f81c6106a8565b8151811061031b5761031b610576565b01602001516001600160f81b0319168361033683600261067b565b8151811061034657610346610576565b60200101906001600160f81b031916908160001a90535081825186838151811061037257610372610576565b0160200151610384919060f81c6106bc565b8151811061039457610394610576565b01602001516001600160f81b031916836103af83600261067b565b6103ba9060016106d0565b815181106103ca576103ca610576565b60200101906001600160f81b031916908160001a905350806103eb816106e3565b9150506102db565b508160405160200161040591906106fc565b60405160208183030381529060405292505050919050565b634e487b7160e01b600052604160045260246000fd5b604051601f8201601f1916810167ffffffffffffffff8111828210171561045c5761045c61041d565b604052919050565b600067ffffffffffffffff82111561047e5761047e61041d565b50601f01601f191660200190565b60006020828403121561049e57600080fd5b813567ffffffffffffffff8111156104b557600080fd5b8201601f810184136104c657600080fd5b80356104d96104d482610464565b610433565b8181528560208385010111156104ee57600080fd5b81602084016020830137600091810160200191909152949350505050565b60005b8381101561052757818101518382015260200161050f565b50506000910152565b6000815180845261054881602086016020860161050c565b601f01601f19169290920160200192915050565b60208152600061056f6020830184610530565b9392505050565b634e487b7160e01b600052603260045260246000fd5b6000602080830181845280855180835260408601915060408160051b870101925083870160005b828110156105e157603f198886030184526105cf858351610530565b945092850192908501906001016105b3565b5092979650505050505050565b60006020828403121561060057600080fd5b815167ffffffffffffffff81111561061757600080fd5b8201601f8101841361062857600080fd5b80516106366104d482610464565b81815285602083850101111561064b57600080fd5b61065c82602083016020860161050c565b95945050505050565b634e487b7160e01b600052601160045260246000fd5b808202811582820484141761025357610253610665565b634e487b7160e01b600052601260045260246000fd5b6000826106b7576106b7610692565b500490565b6000826106cb576106cb610692565b500690565b8082018082111561025357610253610665565b6000600182016106f5576106f5610665565b5060010190565b61060f60f31b81526000825161071981600285016020870161050c565b919091016002019291505056fea2646970667358221220e66d500bc9a9ca9c0748086adfc51de57c85b7c9f66cc760e823099f0439820b64736f6c63430008130033"; + vm.etch(addr, code); + } + + function enable() public { + enableLib(Suave.IS_CONFIDENTIAL_ADDR); + enableLib(Suave.BUILD_ETH_BLOCK); + enableLib(Suave.CONFIDENTIAL_INPUTS); + enableLib(Suave.CONFIDENTIAL_RETRIEVE); + enableLib(Suave.CONFIDENTIAL_STORE); + enableLib(Suave.DO_HTTPREQUEST); + enableLib(Suave.ETHCALL); + enableLib(Suave.EXTRACT_HINT); + enableLib(Suave.FETCH_DATA_RECORDS); + enableLib(Suave.FILL_MEV_SHARE_BUNDLE); + enableLib(Suave.NEW_BUILDER); + enableLib(Suave.NEW_DATA_RECORD); + enableLib(Suave.SIGN_ETH_TRANSACTION); + enableLib(Suave.SIGN_MESSAGE); + enableLib(Suave.SIMULATE_BUNDLE); + enableLib(Suave.SIMULATE_TRANSACTION); + enableLib(Suave.SUBMIT_BUNDLE_JSON_RPC); + enableLib(Suave.SUBMIT_ETH_BLOCK_TO_RELAY); + } +} diff --git a/src/suavelib/SuaveForge.sol b/src/suavelib/SuaveForge.sol deleted file mode 100644 index e42d6c8..0000000 --- a/src/suavelib/SuaveForge.sol +++ /dev/null @@ -1,167 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.8; - -import "./Suave.sol"; - -interface Vm { - function ffi(string[] calldata commandInput) external view returns (bytes memory result); -} - -library SuaveForge { - Vm constant vm = Vm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); - - function forgeIt(string memory addr, bytes memory data) internal view returns (bytes memory) { - string memory dataHex = iToHex(data); - - string[] memory inputs = new string[](4); - inputs[0] = "suave"; - inputs[1] = "forge"; - inputs[2] = addr; - inputs[3] = dataHex; - - bytes memory res = vm.ffi(inputs); - return res; - } - - function iToHex(bytes memory buffer) public pure returns (string memory) { - bytes memory converted = new bytes(buffer.length * 2); - - bytes memory _base = "0123456789abcdef"; - - for (uint256 i = 0; i < buffer.length; i++) { - converted[i * 2] = _base[uint8(buffer[i]) / _base.length]; - converted[i * 2 + 1] = _base[uint8(buffer[i]) % _base.length]; - } - - return string(abi.encodePacked("0x", converted)); - } - - function buildEthBlock(Suave.BuildBlockArgs memory blockArgs, Suave.DataId dataId, string memory namespace) - internal - view - returns (bytes memory, bytes memory) - { - bytes memory data = - forgeIt("0x0000000000000000000000000000000042100001", abi.encode(blockArgs, dataId, namespace)); - - return abi.decode(data, (bytes, bytes)); - } - - function confidentialInputs() internal view returns (bytes memory) { - bytes memory data = forgeIt("0x0000000000000000000000000000000042010001", abi.encode()); - - return data; - } - - function confidentialRetrieve(Suave.DataId dataId, string memory key) internal view returns (bytes memory) { - bytes memory data = forgeIt("0x0000000000000000000000000000000042020001", abi.encode(dataId, key)); - - return data; - } - - function confidentialStore(Suave.DataId dataId, string memory key, bytes memory data1) internal view { - bytes memory data = forgeIt("0x0000000000000000000000000000000042020000", abi.encode(dataId, key, data1)); - } - - function doHTTPRequest(Suave.HttpRequest memory request) internal view returns (bytes memory) { - bytes memory data = forgeIt("0x0000000000000000000000000000000043200002", abi.encode(request)); - - return abi.decode(data, (bytes)); - } - - function ethcall(address contractAddr, bytes memory input1) internal view returns (bytes memory) { - bytes memory data = forgeIt("0x0000000000000000000000000000000042100003", abi.encode(contractAddr, input1)); - - return abi.decode(data, (bytes)); - } - - function extractHint(bytes memory bundleData) internal view returns (bytes memory) { - bytes memory data = forgeIt("0x0000000000000000000000000000000042100037", abi.encode(bundleData)); - - return data; - } - - function fetchDataRecords(uint64 cond, string memory namespace) internal view returns (Suave.DataRecord[] memory) { - bytes memory data = forgeIt("0x0000000000000000000000000000000042030001", abi.encode(cond, namespace)); - - return abi.decode(data, (Suave.DataRecord[])); - } - - function fillMevShareBundle(Suave.DataId dataId) internal view returns (bytes memory) { - bytes memory data = forgeIt("0x0000000000000000000000000000000043200001", abi.encode(dataId)); - - return data; - } - - function newBuilder() internal view returns (string memory) { - bytes memory data = forgeIt("0x0000000000000000000000000000000053200001", abi.encode()); - - return abi.decode(data, (string)); - } - - function newDataRecord( - uint64 decryptionCondition, - address[] memory allowedPeekers, - address[] memory allowedStores, - string memory dataType - ) internal view returns (Suave.DataRecord memory) { - bytes memory data = forgeIt( - "0x0000000000000000000000000000000042030000", - abi.encode(decryptionCondition, allowedPeekers, allowedStores, dataType) - ); - - return abi.decode(data, (Suave.DataRecord)); - } - - function signEthTransaction(bytes memory txn, string memory chainId, string memory signingKey) - internal - view - returns (bytes memory) - { - bytes memory data = forgeIt("0x0000000000000000000000000000000040100001", abi.encode(txn, chainId, signingKey)); - - return abi.decode(data, (bytes)); - } - - function signMessage(bytes memory digest, string memory signingKey) internal view returns (bytes memory) { - bytes memory data = forgeIt("0x0000000000000000000000000000000040100003", abi.encode(digest, signingKey)); - - return abi.decode(data, (bytes)); - } - - function simulateBundle(bytes memory bundleData) internal view returns (uint64) { - bytes memory data = forgeIt("0x0000000000000000000000000000000042100000", abi.encode(bundleData)); - - return abi.decode(data, (uint64)); - } - - function simulateTransaction(string memory session, bytes memory txn) - internal - view - returns (Suave.SimulateTransactionResult memory) - { - bytes memory data = forgeIt("0x0000000000000000000000000000000053200002", abi.encode(session, txn)); - - return abi.decode(data, (Suave.SimulateTransactionResult)); - } - - function submitBundleJsonRPC(string memory url, string memory method, bytes memory params) - internal - view - returns (bytes memory) - { - bytes memory data = forgeIt("0x0000000000000000000000000000000043000001", abi.encode(url, method, params)); - - return data; - } - - function submitEthBlockToRelay(string memory relayUrl, bytes memory builderBid) - internal - view - returns (bytes memory) - { - bytes memory data = forgeIt("0x0000000000000000000000000000000042100002", abi.encode(relayUrl, builderBid)); - - return data; - } -} diff --git a/test/Forge.t.sol b/test/Forge.t.sol new file mode 100644 index 0000000..b0840db --- /dev/null +++ b/test/Forge.t.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import "src/Test.sol"; +import "src/suavelib/Suave.sol"; + +contract TestForge is Test, SuaveEnabled { + address[] public addressList = [0xC8df3686b4Afb2BB53e60EAe97EF043FE03Fb829]; + + function testConfidentialStore() public { + Suave.DataRecord memory record = Suave.newDataRecord(0, addressList, addressList, "namespace"); + + bytes memory value = abi.encode("suave works with forge!"); + Suave.confidentialStore(record.id, "key1", value); + + bytes memory found = Suave.confidentialRetrieve(record.id, "key1"); + assertEq(keccak256(found), keccak256(value)); + } +} diff --git a/tools/forge-gen/README.md b/tools/forge-gen/README.md new file mode 100644 index 0000000..3e8f186 --- /dev/null +++ b/tools/forge-gen/README.md @@ -0,0 +1,13 @@ +# Forge-gen command + +In the `forge` integration, a `forge/Connector.sol` contract is deployed for each of the `Suave` precompiles. The contract uses the fallback function to make an `vm.ffi` call to the `suave forge` command to peform the logic request. + +The `forge-gen` command creates the `forge/Registry.sol` contract which deploys the `Connector.sol` contract in all the precompile addresses using the `vm.etch` function. + +## Usage + +```bash +$ go run tools/forge-gen/main.go --apply +``` + +Use the `apply` flag to write the contract. Otherwise, it prints the contract on the standard output. diff --git a/tools/forge-gen/go.mod b/tools/forge-gen/go.mod new file mode 100644 index 0000000..8250b74 --- /dev/null +++ b/tools/forge-gen/go.mod @@ -0,0 +1,3 @@ +module github.com/flashbots/suave-std/tools/forge-gen + +go 1.21.0 diff --git a/tools/forge-gen/main.go b/tools/forge-gen/main.go new file mode 100644 index 0000000..80fefff --- /dev/null +++ b/tools/forge-gen/main.go @@ -0,0 +1,168 @@ +package main + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "html/template" + "os" + "os/exec" + "regexp" + "strings" +) + +var applyFlag bool + +func main() { + flag.BoolVar(&applyFlag, "apply", false, "write to file") + flag.Parse() + + bytecode, err := getForgeConnectorBytecode() + if err != nil { + fmt.Printf("failed to get forge wrapper bytecode: %v\n", err) + os.Exit(1) + } + + precompileNames, err := getPrecompileNames() + if err != nil { + fmt.Printf("failed to get precompile names: %v\n", err) + os.Exit(1) + } + + if err := applyTemplate(bytecode, precompileNames); err != nil { + fmt.Printf("failed to apply template: %v\n", err) + os.Exit(1) + } +} + +var templateFile = `// SPDX-License-Identifier: UNLICENSED +// DO NOT edit this file. Code generated by forge-gen. +pragma solidity ^0.8.8; + +import "../suavelib/Suave.sol"; + +interface registryVM { + function etch(address, bytes calldata) external; +} + +library Registry { + registryVM constant vm = registryVM(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + + function enableLib(address addr) public { + // code for Wrapper + bytes memory code = + hex"{{.Bytecode}}"; + vm.etch(addr, code); + } + + function enable() public { + {{range .PrecompileNames}} + enableLib(Suave.{{.}}); + {{- end}} + } +}` + +func applyTemplate(bytecode string, precompileNames []string) error { + t, err := template.New("template").Parse(templateFile) + if err != nil { + return err + } + + input := map[string]interface{}{ + "Bytecode": bytecode, + "PrecompileNames": precompileNames, + } + + var outputRaw bytes.Buffer + if err = t.Execute(&outputRaw, input); err != nil { + return err + } + + str := outputRaw.String() + if str, err = formatSolidity(str); err != nil { + return err + } + + if applyFlag { + if err := os.WriteFile("./src/forge/Registry.sol", []byte(str), 0644); err != nil { + return err + } + } else { + fmt.Println(str) + } + return nil +} + +func getForgeConnectorBytecode() (string, error) { + abiContent, err := os.ReadFile("./out/Connector.sol/Connector.json") + if err != nil { + return "", err + } + + var abiArtifact struct { + DeployedBytecode struct { + Object string + } + } + if err := json.Unmarshal(abiContent, &abiArtifact); err != nil { + return "", err + } + + bytecode := abiArtifact.DeployedBytecode.Object[2:] + return bytecode, nil +} + +func getPrecompileNames() ([]string, error) { + content, err := os.ReadFile("./src/suavelib/Suave.sol") + if err != nil { + return nil, err + } + + addrRegexp := regexp.MustCompile(`constant\s+([A-Za-z_]\w*)\s+=`) + + matches := addrRegexp.FindAllStringSubmatch(string(content), -1) + + names := []string{} + for _, match := range matches { + if len(match) > 1 { + name := strings.TrimSpace(match[1]) + if name == "ANYALLOWED" { + continue + } + names = append(names, name) + } + } + + return names, nil +} + +func formatSolidity(code string) (string, error) { + // Check if "forge" command is available in PATH + _, err := exec.LookPath("forge") + if err != nil { + return "", fmt.Errorf("forge command not found in PATH: %v", err) + } + + // Command and arguments for forge fmt + command := "forge" + args := []string{"fmt", "--raw", "-"} + + // Create a command to run the forge fmt command + cmd := exec.Command(command, args...) + + // Set up input from stdin + cmd.Stdin = bytes.NewBufferString(code) + + // Set up output buffer + var outBuf, errBuf bytes.Buffer + cmd.Stdout = &outBuf + cmd.Stderr = &errBuf + + // Run the command + if err = cmd.Run(); err != nil { + return "", fmt.Errorf("error running command: %v", err) + } + + return outBuf.String(), nil +}