diff --git a/.github/workflows/bento.yml b/.github/workflows/bento.yml index 32a5db0a7..30b24d52c 100644 --- a/.github/workflows/bento.yml +++ b/.github/workflows/bento.yml @@ -29,7 +29,7 @@ env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} RISC0_TOOLCHAIN_VERSION: 1.88.0 RISC0_CRATE_VERSION: "3.0.1" - FOUNDRY_VERSION: v1.2.3 + FOUNDRY_VERSION: v1.5.0 jobs: bento-test: diff --git a/.github/workflows/broker-release.yml b/.github/workflows/broker-release.yml index 952def50c..cc3608436 100644 --- a/.github/workflows/broker-release.yml +++ b/.github/workflows/broker-release.yml @@ -21,7 +21,7 @@ env: RUST_VERSION: "1.88.0" GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - FOUNDRY_VERSION: v1.2.3 + FOUNDRY_VERSION: v1.5.0 jobs: build-broker: diff --git a/.github/workflows/contracts.yml b/.github/workflows/contracts.yml index 921a7bbb2..ec837b0c3 100644 --- a/.github/workflows/contracts.yml +++ b/.github/workflows/contracts.yml @@ -15,7 +15,7 @@ permissions: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - FOUNDRY_VERSION: v1.2.3 + FOUNDRY_VERSION: v1.5.0 RISC0_TOOLCHAIN_VERSION: 1.88.0 RISC0_CRATE_VERSION: "3.0.3" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index be654b4be..cd9835d0a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -21,7 +21,7 @@ env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} RISC0_TOOLCHAIN_VERSION: 1.88.0 RISC0_CRATE_VERSION: "3.0.3" - FOUNDRY_VERSION: v1.0.0 + FOUNDRY_VERSION: v1.5.0 jobs: # see: https://github.com/orgs/community/discussions/26822 diff --git a/contracts/deployment-test/VerifierDeployment.t.sol b/contracts/deployment-test/VerifierDeployment.t.sol new file mode 100644 index 000000000..a5141ba3c --- /dev/null +++ b/contracts/deployment-test/VerifierDeployment.t.sol @@ -0,0 +1,250 @@ +// Copyright 2025 Boundless Foundation, Inc. +// +// Use of this source code is governed by the Business Source License +// as found in the LICENSE-BSL file. + +pragma solidity ^0.8.9; + +import {Test} from "forge-std/Test.sol"; +import {console2} from "forge-std/console2.sol"; +import {Pausable} from "openzeppelin/contracts/utils/Pausable.sol"; +import {TimelockController} from "openzeppelin/contracts/governance/TimelockController.sol"; +import {RiscZeroVerifierRouter} from "../src/verifier/RiscZeroVerifierRouter.sol"; +import {VerifierLayeredRouter} from "../src/verifier/VerifierLayeredRouter.sol"; +import { + IRiscZeroVerifier, Receipt as RiscZeroReceipt, ReceiptClaim, ReceiptClaimLib +} from "risc0/IRiscZeroVerifier.sol"; +import {ConfigLoader, Deployment, DeploymentLib, VerifierDeployment} from "../src/config/VerifierConfig.sol"; +import {IRiscZeroSelectable} from "risc0/IRiscZeroSelectable.sol"; +import {RiscZeroVerifierEmergencyStop} from "risc0/RiscZeroVerifierEmergencyStop.sol"; +import {TestReceipt} from "../test/receipts/Blake3Groth16TestReceipt.sol"; +import {TestReceipt as Groth16Receipt} from "../test/receipts/Groth16TestReceiptV3_0.sol"; +import {TestSetInclusionReceipt as SetInclusionReceipt} from "../test/receipts/SetInclusionTestReceiptV0_9.sol"; + + +library TestReceipts { + using ReceiptClaimLib for ReceiptClaim; + + // HACK: Get first 4 bytes of a memory bytes vector + function getFirst4Bytes(bytes memory data) internal pure returns (bytes4) { + require(data.length >= 4, "Data too short"); + bytes4 result; + assembly { + result := mload(add(data, 32)) + } + return result; + } + + function getTestReceipt(bytes4 selector) internal pure returns (bool, RiscZeroReceipt memory) { + if (selector == getFirst4Bytes(Groth16Receipt.SEAL)) { + bytes32 claimDigest = ReceiptClaimLib.ok(Groth16Receipt.IMAGE_ID, sha256(Groth16Receipt.JOURNAL)).digest(); + return (true, RiscZeroReceipt({seal: Groth16Receipt.SEAL, claimDigest: claimDigest})); + } + if (selector == getFirst4Bytes(SetInclusionReceipt.SEAL)) { + bytes32 claimDigest = ReceiptClaimLib.ok(SetInclusionReceipt.IMAGE_ID, sha256(SetInclusionReceipt.JOURNAL)).digest(); + return (true, RiscZeroReceipt({seal: SetInclusionReceipt.SEAL, claimDigest: claimDigest})); + } + if (selector == getFirst4Bytes(TestReceipt.SEAL)) { + return (true, RiscZeroReceipt({seal: TestReceipt.SEAL, claimDigest: TestReceipt.CLAIM_DIGEST})); + } + return (false, RiscZeroReceipt({seal: new bytes(0), claimDigest: bytes32(0)})); + } + + function getGroth16TestReceipt() internal pure returns (RiscZeroReceipt memory) { + bytes32 claimDigest = ReceiptClaimLib.ok(Groth16Receipt.IMAGE_ID, sha256(Groth16Receipt.JOURNAL)).digest(); + return RiscZeroReceipt({seal: Groth16Receipt.SEAL, claimDigest: claimDigest}); + } + + function getSetInclusionTestReceipt() internal pure returns (RiscZeroReceipt memory) { + bytes32 claimDigest = ReceiptClaimLib.ok(SetInclusionReceipt.IMAGE_ID, sha256(SetInclusionReceipt.JOURNAL)).digest(); + return RiscZeroReceipt({seal: SetInclusionReceipt.SEAL, claimDigest: claimDigest}); + } +} + +/// Test designed to be run against a chain with an active deployment of the verifier contracts. +/// Checks that the deployment matches what is recorded in the deployment.toml file. +contract VerifierDeploymentTest is Test { + using DeploymentLib for Deployment; + + Deployment internal deployment; + + TimelockController internal timelockController; + VerifierLayeredRouter internal router; + + function setUp() external { + string memory configPath = vm.envOr("DEPLOYMENT_CONFIG", string.concat(vm.projectRoot(), "/", "contracts/deployment_verifier.toml")); + console2.log("Loading deployment config from %s", configPath); + ConfigLoader.loadDeploymentConfig(configPath).copyTo(deployment); + + // Wrap the control addresses with their respective contract implementations. + // NOTE: These addresses may be zero, so this does not guarantee contracts are deployed. + timelockController = TimelockController(payable(deployment.timelockController)); + router = VerifierLayeredRouter(deployment.router); + } + + function testAdminIsSet() external view { + require(deployment.admin != address(0), "no admin address is set"); + } + + function testTimelockControllerIsDeployed() external view { + require(address(timelockController) != address(0), "no timelock controller address is set"); + require( + keccak256(address(timelockController).code) != keccak256(bytes("")), "timelock controller code is empty" + ); + } + + function testRouterIsDeployed() external view { + require(address(router) != address(0), "no router address is set"); + require(keccak256(address(router).code) != keccak256(bytes("")), "router code is empty"); + } + + function testTimelockControllerIsConfiguredProperly() external view { + require( + timelockController.hasRole(timelockController.PROPOSER_ROLE(), deployment.admin), + "admin does not have proposer role" + ); + require( + timelockController.hasRole(timelockController.EXECUTOR_ROLE(), deployment.admin), + "admin does not have executor role" + ); + require( + timelockController.hasRole(timelockController.CANCELLER_ROLE(), deployment.admin), + "admin does not have canceller role" + ); + uint256 deployedDelay = timelockController.getMinDelay(); + console2.log( + "Min delay on timelock controller is %d; expected value is %d", deployedDelay, deployment.timelockDelay + ); + require( + timelockController.getMinDelay() == deployment.timelockDelay, + "timelock controller min delay is not as expected" + ); + } + + function testVerifieLayeredRouterIsConfiguredProperly() external view { + require(router.owner() == address(timelockController), "router is not owned by timelock controller"); + + for (uint256 i = 0; i < deployment.verifiers.length; i++) { + VerifierDeployment storage verifierConfig = deployment.verifiers[i]; + console2.log( + "Checking for deployment to the router of verifier with selector %x and version %s", + uint256(uint32(verifierConfig.selector)), + verifierConfig.version + ); + if (verifierConfig.unroutable) { + // When a verifier is specified to be unroutable, confirm that it is indeed not added to the router. + try router.getVerifier(verifierConfig.selector) { + revert("expected router.getVerifier to revert"); + } catch (bytes memory err) { + // NOTE: We could allow SelectorRemoved as well here. + require( + keccak256(err) + == keccak256( + abi.encodeWithSelector( + RiscZeroVerifierRouter.SelectorUnknown.selector, verifierConfig.selector + ) + ) + ); + console2.log( + "Verifier with selector %x is unroutable, as configured", + uint256(uint32(verifierConfig.selector)) + ); + } + continue; + } + + IRiscZeroVerifier routedVerifier = router.getVerifier(verifierConfig.selector); + require(address(routedVerifier) != address(0), "verifier router returned the zero address"); + require( + address(routedVerifier) == address(verifierConfig.estop), "verifier router returned the wrong address" + ); + } + } + + function testParentRouterIsConfiguredProperly() external view { + if (deployment.parentRouter != address(0)) { + VerifierLayeredRouter parentRouter = VerifierLayeredRouter(deployment.parentRouter); + require(address(parentRouter) != address(0), "parent router is the zero address"); + require( + keccak256(address(parentRouter).code) != keccak256(bytes("")), "parent router has no deployed code" + ); + require(router.getParentRouter() == parentRouter, "router parent router is not configured properly"); + } else { + revert("router parent router should be the zero address"); + } + + RiscZeroReceipt memory groth16Receipt = TestReceipts.getGroth16TestReceipt(); + router.verifyIntegrity(groth16Receipt); + console2.log("Parent router successfully verified Groth16 test receipt"); + + RiscZeroReceipt memory setInclusionReceipt = TestReceipts.getSetInclusionTestReceipt(); + router.verifyIntegrity(setInclusionReceipt); + console2.log("Parent router successfully verified Set Inclusion test receipt"); + } + + function testVerifierEstopsProperlyConfigured() external view { + for (uint256 i = 0; i < deployment.verifiers.length; i++) { + VerifierDeployment storage verifierConfig = deployment.verifiers[i]; + console2.log( + "Checking for configuration of verifier with selector %x and version %s", + uint256(uint32(verifierConfig.selector)), + verifierConfig.version + ); + + RiscZeroVerifierEmergencyStop verifierEstop = RiscZeroVerifierEmergencyStop(verifierConfig.estop); + require(address(verifierEstop) != address(0), "verifier estop is the zero address"); + require( + keccak256(address(verifierEstop).code) != keccak256(bytes("")), "verifier estop has no deployed code" + ); + require(verifierEstop.owner() == deployment.admin, "estop owner is not the admin address"); + if (verifierConfig.stopped) { + require(verifierEstop.paused(), "verifier estop is not stopped"); + } else { + require(!verifierEstop.paused(), "verifier estop is stopped"); + } + + IRiscZeroVerifier verifierImpl = verifierEstop.verifier(); + console2.log("verifier implementation is at %s", address(verifierImpl)); + require(address(verifierImpl) != address(0), "verifier impl is the zero address"); + require(address(verifierImpl) == address(verifierConfig.verifier), "verifier impl is the wrong address"); + require(keccak256(address(verifierImpl).code) != keccak256(bytes("")), "verifier impl has no deployed code"); + + IRiscZeroSelectable verifierSelectable = IRiscZeroSelectable(address(verifierImpl)); + require(verifierConfig.selector == verifierSelectable.SELECTOR(), "selector mismatch"); + + // Ensure that stopped and unroutable verifiers _cannot_ be used to verify a receipt. + (bool testReceiptExists, RiscZeroReceipt memory testReceipt) = + TestReceipts.getTestReceipt(verifierConfig.selector); + if (testReceiptExists) { + // Check that a direct call to the verifier works. Note that this bypasses the estop. + console2.log( + "Running direct verification of receipt with selector %x", uint256(uint32(verifierConfig.selector)) + ); + verifierImpl.verifyIntegrity(testReceipt); + + // Check that a direct call to the verifier works. Note that this bypasses the estop. + console2.log( + "Running estop verification of receipt with selector %x", uint256(uint32(verifierConfig.selector)) + ); + if (!verifierConfig.stopped) { + verifierEstop.verifyIntegrity(testReceipt); + console2.log("Verifier with selector %x accepts receipt", uint256(uint32(verifierConfig.selector))); + } else { + try verifierEstop.verifyIntegrity(testReceipt) { + revert("expected verifierEstop.verifyIntegrity to revert"); + } catch (bytes memory err) { + require(keccak256(err) == keccak256(abi.encodePacked(Pausable.EnforcedPause.selector))); + console2.log( + "Verifier with selector %x fails as stopped, as configured", + uint256(uint32(verifierConfig.selector)) + ); + } + } + } else { + console2.log( + "Skipping verification of receipt with selector %x", uint256(uint32(verifierConfig.selector)) + ); + } + } + } +} diff --git a/contracts/deployment_verifier.toml b/contracts/deployment_verifier.toml new file mode 100644 index 000000000..df8706b31 --- /dev/null +++ b/contracts/deployment_verifier.toml @@ -0,0 +1,70 @@ +[chains.ethereum-mainnet] +name = "Ethereum Mainnet" +id = 1 +etherscan-url = "https://etherscan.io/" + +# Accounts +admin = "0xb04d1a222789a76e74168a919b43b20f66e24f0b" + +# Contracts +timelock-controller = "0x0000000000000000000000000000000000000000" +timelock-delay = 259200 +router = "0x0000000000000000000000000000000000000000" +parent-router = "0x8EaB2D97Dfce405A1692a21b3ff3A172d593D319" + +### + +[chains.ethereum-sepolia] +name = "Ethereum Sepolia" +id = 11155111 +etherscan-url = "https://sepolia.etherscan.io/" + +# Accounts +admin = "0xb04d1a222789a76e74168a919b43b20f66e24f0b" + +# Contracts +timelock-controller = "0x0000000000000000000000000000000000000000" +timelock-delay = 1 +router = "0x0000000000000000000000000000000000000000" +parent-router = "0x925d8331ddc0a1F0d96E68CF073DFE1d92b69187" + +### + +[chains.base-mainnet] +name = "Base Mainnet" +id = 8453 +etherscan-url = "https://basescan.org/" + +# Accounts +admin = "0xb04d1a222789a76e74168a919b43b20f66e24f0b" + +# Contracts +timelock-controller = "0x0000000000000000000000000000000000000000" +timelock-delay = 259200 +router = "0x0000000000000000000000000000000000000000" +parent-router = "0x0b144e07a0826182b6b59788c34b32bfa86fb711" + +### + +[chains.base-sepolia] +name = "Base Sepolia" +id = 84532 +etherscan-url = "https://sepolia.basescan.org/" + +# Accounts +admin = "0xb04d1a222789a76e74168a919b43b20f66e24f0b" + +# Contracts +timelock-controller = "0x20c64F0C59ac248F10B5a5ddDd3a418Bed75c91C" +timelock-delay = 0 +router = "0xA326b2eb45A5C3C206dF905A58970DcA57B8719e" +parent-router = "0x0b144e07a0826182b6b59788c34b32bfa86fb711" + +[[chains.base-sepolia.verifiers]] +name = "Blake3Groth16Verifier" +version = "0.0.1" +selector = "0x62f049f6" +verifier = "0x7bdf79856cf17D9f8D778249E1A5120cdA7cEA93" +estop = "0x9A59695891d60A120741A0Bb35A1a7d002b4242E" + +### diff --git a/contracts/scripts/BoundlessScript.s.sol b/contracts/scripts/BoundlessScript.s.sol index f97bd49dd..fe4f2eb6c 100644 --- a/contracts/scripts/BoundlessScript.s.sol +++ b/contracts/scripts/BoundlessScript.s.sol @@ -132,9 +132,7 @@ abstract contract BoundlessScriptBase is Script { /// @param adminField2 Second admin field to check (e.g., "admin-2") /// @param removedAdmin The admin address being removed /// @dev Only clears the TOML field that contains the specific admin address being removed - function _removeAdminFromToml(string memory adminField1, string memory adminField2, address removedAdmin) - internal - { + function _removeAdminFromToml(string memory adminField1, string memory adminField2, address removedAdmin) internal { // Load current deployment config to check which field contains the removed admin DeploymentConfig memory deploymentConfig = ConfigLoader.loadDeploymentConfig(string.concat(vm.projectRoot(), "/", CONFIG)); diff --git a/contracts/scripts/ManageVerifier.s.sol b/contracts/scripts/ManageVerifier.s.sol new file mode 100644 index 000000000..8216a4809 --- /dev/null +++ b/contracts/scripts/ManageVerifier.s.sol @@ -0,0 +1,957 @@ +// Copyright 2025 Boundless Foundation, Inc. +// +// Use of this source code is governed by the Business Source License +// as found in the LICENSE-BSL file. + +pragma solidity ^0.8.9; + +import {Script} from "forge-std/Script.sol"; +import {console2} from "forge-std/console2.sol"; +import {Strings} from "openzeppelin/contracts/utils/Strings.sol"; +import {TimelockController} from "openzeppelin/contracts/governance/TimelockController.sol"; +import {RiscZeroVerifierRouter} from "../src/verifier/RiscZeroVerifierRouter.sol"; +import {VerifierLayeredRouter} from "../src/verifier/VerifierLayeredRouter.sol"; +import {RiscZeroVerifierEmergencyStop} from "risc0/RiscZeroVerifierEmergencyStop.sol"; +import {IRiscZeroVerifier} from "risc0/IRiscZeroVerifier.sol"; +import {IRiscZeroSelectable} from "risc0/IRiscZeroSelectable.sol"; +import {Blake3Groth16Verifier} from "../src/blake3-groth16/Blake3Groth16Verifier.sol"; +import {ControlID} from "../src/blake3-groth16/ControlID.sol"; +import {ConfigLoader, Deployment, DeploymentLib} from "../src/config/VerifierConfig.sol"; + +// Default salt used with CREATE2 for deterministic deployment addresses. +// NOTE: It kind of spelled risc0 in 1337. +bytes32 constant CREATE2_SALT = hex"1215c0"; + +/// @notice Compare strings for equality. +function stringEq(string memory a, string memory b) pure returns (bool) { + return (keccak256(abi.encodePacked((a))) == keccak256(abi.encodePacked((b)))); +} + +/// @notice Return the role code for the given named role +function timelockControllerRole(TimelockController timelockController, string memory roleStr) view returns (bytes32) { + if (stringEq(roleStr, "proposer")) { + return timelockController.PROPOSER_ROLE(); + } else if (stringEq(roleStr, "executor")) { + return timelockController.EXECUTOR_ROLE(); + } else if (stringEq(roleStr, "canceller")) { + return timelockController.CANCELLER_ROLE(); + } else { + revert(); + } +} + +/// @notice Base contract for the scripts below, providing common context and functions. +contract RiscZeroManagementScript is Script { + using DeploymentLib for Deployment; + + Deployment internal deployment; + TimelockController internal _timelockController; + VerifierLayeredRouter internal _verifierRouter; + RiscZeroVerifierRouter internal _parentRouter; + RiscZeroVerifierEmergencyStop internal _verifierEstop; + IRiscZeroVerifier internal _verifier; + + function loadConfig() internal { + string memory configPath = + vm.envOr("DEPLOYMENT_CONFIG", string.concat(vm.projectRoot(), "/", "contracts/deployment_verifier.toml")); + console2.log("Loading deployment config from %s", configPath); + ConfigLoader.loadDeploymentConfig(configPath).copyTo(deployment); + + // Wrap the control addresses with their respective contract implementations. + // NOTE: These addresses may be zero, so this does not guarantee contracts are deployed. + _timelockController = TimelockController(payable(deployment.timelockController)); + _verifierRouter = VerifierLayeredRouter(deployment.router); + _parentRouter = RiscZeroVerifierRouter(deployment.parentRouter); + } + + modifier withConfig() { + loadConfig(); + _; + } + + /// @notice Returns the address of the deployer, set in the DEPLOYER_ADDRESS env var. + function deployerAddress() internal returns (address) { + address deployer = vm.envAddress("DEPLOYER_ADDRESS"); + uint256 deployerKey = vm.envOr("DEPLOYER_PRIVATE_KEY", uint256(0)); + if (deployerKey != 0) { + require(vm.addr(deployerKey) == deployer, "DEPLOYER_ADDRESS and DEPLOYER_PRIVATE_KEY are inconsistent"); + vm.rememberKey(deployerKey); + } + return deployer; + } + + /// @notice Returns the address of the contract admin, set in the ADMIN_ADDRESS env var. + /// @dev This admin address will be set as the owner of the estop contracts, and the proposer + /// of for the timelock controller. Note that it is not the "admin" on the timelock. + function adminAddress() internal view returns (address) { + return vm.envOr("ADMIN_ADDRESS", deployment.admin); + } + + /// @notice Returns the timelock-delay, set in the MIN_DELAY env var. + function timelockDelay() internal view returns (uint256) { + return vm.envOr("MIN_DELAY", deployment.timelockDelay); + } + + /// @notice Determines the contract address of TimelockController from the environment. + /// @dev Uses the TIMELOCK_CONTROLLER environment variable. + function timelockController() internal returns (TimelockController) { + if (address(_timelockController) != address(0)) { + return _timelockController; + } + _timelockController = TimelockController(payable(vm.envAddress("TIMELOCK_CONTROLLER"))); + console2.log("Using TimelockController at address", address(_timelockController)); + return _timelockController; + } + + /// @notice Determines the contract address of VerifierLayeredRouter from the environment. + /// @dev Uses the VERIFIER_ROUTER environment variable. + function verifierRouter() internal returns (VerifierLayeredRouter) { + if (address(_verifierRouter) != address(0)) { + return _verifierRouter; + } + _verifierRouter = VerifierLayeredRouter(vm.envAddress("VERIFIER_ROUTER")); + console2.log("Using VerifierLayeredRouter at address", address(_verifierRouter)); + return _verifierRouter; + } + + function parentRouter() internal returns (RiscZeroVerifierRouter) { + if (address(_parentRouter) != address(0)) { + return _parentRouter; + } + _parentRouter = RiscZeroVerifierRouter(vm.envAddress("PARENT_VERIFIER_ROUTER")); + console2.log("Using Parent RiscZeroVerifierRouter at address", address(_parentRouter)); + return _parentRouter; + } + + /// @notice Determines the contract address of RiscZeroVerifierRouter from the environment. + /// @dev Uses the VERIFIER_ESTOP environment variable. + function verifierEstop() internal returns (RiscZeroVerifierEmergencyStop) { + if (address(_verifierEstop) != address(0)) { + return _verifierEstop; + } + // Use the address set in the VERIFIER_ESTOP environment variable if it is set. + _verifierEstop = RiscZeroVerifierEmergencyStop(vm.envOr("VERIFIER_ESTOP", address(0))); + if (address(_verifierEstop) != address(0)) { + console2.log("Using RiscZeroVerifierEmergencyStop at address", address(_verifierEstop)); + return _verifierEstop; + } + bytes4 selector = bytes4(vm.envBytes("VERIFIER_SELECTOR")); + for (uint256 i = 0; i < deployment.verifiers.length; i++) { + if (deployment.verifiers[i].selector == selector) { + _verifierEstop = RiscZeroVerifierEmergencyStop(deployment.verifiers[i].estop); + break; + } + } + console2.log( + "Using RiscZeroVerifierEmergencyStop at address %s and selector %x", + address(_verifierEstop), + uint256(bytes32(selector)) + ); + return _verifierEstop; + } + + /// @notice Determines the contract address of IRiscZeroVerifier from the environment. + /// @dev Uses the VERIFIER_ESTOP environment variable, and gets the proxied verifier. + function verifier() internal returns (IRiscZeroVerifier) { + if (address(_verifier) != address(0)) { + return _verifier; + } + _verifier = verifierEstop().verifier(); + console2.log("Using IRiscZeroVerifier at address", address(_verifier)); + return _verifier; + } + + /// @notice Determines the contract address of IRiscZeroSelectable from the environment. + /// @dev Uses the VERIFIER_ESTOP environment variable, and gets the proxied selectable. + function selectable() internal returns (IRiscZeroSelectable) { + return IRiscZeroSelectable(address(verifier())); + } + + /// @notice Simulates a call to check if it will succeed, given the current EVM state. + function simulate(address dest, bytes memory data) internal { + console2.log("Simulating call to", dest); + console2.logBytes(data); + uint256 snapshot = vm.snapshot(); + vm.prank(address(timelockController())); + (bool success,) = dest.call(data); + require(success, "simulation of transaction to schedule failed"); + vm.revertTo(snapshot); + console2.log("Simulation successful"); + } +} + +/// @notice Deployment script for the timelocked router. +/// @dev Use the following environment variable to control the deployment: +/// * MIN_DELAY minimum delay in seconds for operations +/// * PROPOSER address of proposer +/// * EXECUTOR address of executor +/// +/// See the Foundry documentation for more information about Solidity scripts. +/// https://book.getfoundry.sh/guides/scripting-with-solidity +contract DeployTimelockRouter is RiscZeroManagementScript { + function run() external withConfig { + // initial minimum delay in seconds for operations + uint256 minDelay = timelockDelay(); + console2.log("minDelay:", minDelay); + + // accounts to be granted proposer and canceller roles + address[] memory proposers = new address[](1); + proposers[0] = vm.envOr("PROPOSER", adminAddress()); + console2.log("proposers:", proposers[0]); + + // accounts to be granted executor role + address[] memory executors = new address[](1); + executors[0] = vm.envOr("EXECUTOR", adminAddress()); + console2.log("executors:", executors[0]); + + // NOTE: This functionality is unused in our process. The admin is not subject to the timelock + // delay, which is useful e.g. for initial setup, but should not be used in production. + // + // optional account to be granted admin role; disable with zero address + // When the admin is unset, the contract is self-administered. + //address admin = vm.envOr("ADMIN", address(0)); + //console2.log("admin:", admin); + + // Deploy new contracts + vm.broadcast(deployerAddress()); + _timelockController = new TimelockController{salt: CREATE2_SALT}(minDelay, proposers, executors, address(0)); + console2.log("Deployed TimelockController to", address(timelockController())); + + vm.broadcast(deployerAddress()); + _verifierRouter = new VerifierLayeredRouter{salt: CREATE2_SALT}(address(timelockController()), parentRouter()); + console2.log("Deployed VerifierLayeredRouter to", address(verifierRouter())); + } +} + +/// @notice Deployment script for the RISC Zero verifier with Emergency Stop mechanism. +/// @dev Use the following environment variable to control the deployment: +/// * CHAIN_KEY key of the target chain +/// * VERIFIER_ESTOP_OWNER owner of the emergency stop contract +/// +/// See the Foundry documentation for more information about Solidity scripts. +/// https://book.getfoundry.sh/guides/scripting-with-solidity +contract DeployEstopBlake3Groth16Verifier is RiscZeroManagementScript { + function run() external withConfig { + string memory chainKey = vm.envString("CHAIN_KEY"); + console2.log("chainKey:", chainKey); + address verifierEstopOwner = vm.envOr("VERIFIER_ESTOP_OWNER", adminAddress()); + console2.log("verifierEstopOwner:", verifierEstopOwner); + + // Deploy new contracts + vm.broadcast(deployerAddress()); + Blake3Groth16Verifier blake3Groth16Verifier = + new Blake3Groth16Verifier{salt: CREATE2_SALT}(ControlID.CONTROL_ROOT, ControlID.BN254_CONTROL_ID); + _verifier = blake3Groth16Verifier; + + vm.broadcast(deployerAddress()); + _verifierEstop = + new RiscZeroVerifierEmergencyStop{salt: CREATE2_SALT}(blake3Groth16Verifier, verifierEstopOwner); + + // Print in TOML format + console2.log(""); + console2.log("[[chains.%s.verifiers]]", chainKey); + console2.log("name = \"Blake3Groth16Verifier\""); + console2.log("version = \"%s\"", blake3Groth16Verifier.VERSION()); + console2.log("selector = \"%s\"", Strings.toHexString(uint256(uint32(blake3Groth16Verifier.SELECTOR())), 4)); + console2.log("verifier = \"%s\"", address(verifier())); + console2.log("estop = \"%s\"", address(verifierEstop())); + console2.log("unroutable = true # remove when added to the router"); + } +} + +/// @notice Schedule addition of verifier to router. +/// @dev Use the following environment variable to control the deployment: +/// * SCHEDULE_DELAY (optional) minimum delay in seconds for the scheduled action +/// * TIMELOCK_CONTROLLER contract address of TimelockController +/// * VERIFIER_ROUTER contract address of RiscZeroVerifierRouter +/// * VERIFIER_ESTOP contract address of RiscZeroVerifierEmergencyStop +/// +/// See the Foundry documentation for more information about Solidity scripts. +/// https://book.getfoundry.sh/guides/scripting-with-solidity +contract ScheduleAddVerifier is RiscZeroManagementScript { + function run() external withConfig { + // Check for deployment mode flags + bool gnosisExecute = vm.envOr("GNOSIS_EXECUTE", false); + // Schedule the 'addVerifier()' request + bytes4 selector = selectable().SELECTOR(); + console2.log("Selector: ", Strings.toHexString(uint256(uint32(selector)))); + + uint256 scheduleDelay = vm.envOr("SCHEDULE_DELAY", timelockController().getMinDelay()); + console2.log("scheduleDelay: ", scheduleDelay); + + bytes memory data = abi.encodeCall(verifierRouter().addVerifier, (selector, verifierEstop())); + address dest = address(verifierRouter()); + simulate(dest, data); + + if (gnosisExecute) { + _printGnosisSafeInfo(address(timelockController()), dest, selector, data, scheduleDelay); + return; + } + vm.broadcast(adminAddress()); + timelockController().schedule(dest, 0, data, 0, 0, scheduleDelay); + } + + /// @notice Print Gnosis Safe transaction information for manual submissions + /// @param timelockAddress The timelock controller address (target for Gnosis Safe) + /// @param dest The destination address for the scheduled operation + /// @param selector The verifier selector being added + /// @param data The calldata for the scheduled operation + /// @param scheduleDelay The minimum delay in seconds for the scheduled action + function _printGnosisSafeInfo( + address timelockAddress, + address dest, + bytes4 selector, + bytes memory data, + uint256 scheduleDelay + ) internal pure { + console2.log("================================"); + console2.log("================================"); + console2.log("=== GNOSIS SAFE SCHEDULE ADD VERIFIER INFO ==="); + console2.log("Target Timelock Controller Address (To): ", timelockAddress); + console2.log("Verifier Router Address (dest): ", dest); + console2.log("Selector: ", Strings.toHexString(uint256(uint32(selector)))); + console2.log("scheduleDelay: ", scheduleDelay); + + bytes memory callData = abi.encodeWithSignature( + "schedule(address,uint256,bytes,bytes32,bytes32,uint256)", dest, 0, data, 0, 0, scheduleDelay + ); + console2.log("Function: schedule(address,uint256,bytes,bytes32,bytes32,uint256)"); + console2.log("Calldata:"); + console2.logBytes(callData); + console2.log(""); + console2.log("================================"); + } +} + +/// @notice Finish addition of verifier to router. +/// @dev Use the following environment variable to control the deployment: +/// * TIMELOCK_CONTROLLER contract address of TimelockController +/// * VERIFIER_ROUTER contract address of RiscZeroVerifierRouter +/// * VERIFIER_ESTOP contract address of RiscZeroVerifierEmergencyStop +/// +/// See the Foundry documentation for more information about Solidity scripts. +/// https://book.getfoundry.sh/guides/scripting-with-solidity +contract FinishAddVerifier is RiscZeroManagementScript { + function run() external withConfig { + // Check for deployment mode flags + bool gnosisExecute = vm.envOr("GNOSIS_EXECUTE", false); + // Execute the 'addVerifier()' request + bytes4 selector = selectable().SELECTOR(); + console2.log("selector:"); + console2.logBytes4(selector); + + bytes memory data = abi.encodeCall(verifierRouter().addVerifier, (selector, verifierEstop())); + + if (gnosisExecute) { + _printGnosisSafeInfo(address(timelockController()), address(verifierRouter()), selector, data); + return; + } + + vm.broadcast(adminAddress()); + timelockController().execute(address(verifierRouter()), 0, data, 0, 0); + } + + /// @notice Print Gnosis Safe transaction information for manual submissions + /// @param timelockAddress The timelock controller address (target for Gnosis Safe) + /// @param dest The destination address for the scheduled operation + /// @param selector The verifier selector being added + /// @param data The calldata for the scheduled operation + function _printGnosisSafeInfo(address timelockAddress, address dest, bytes4 selector, bytes memory data) + internal + pure + { + console2.log("================================"); + console2.log("================================"); + console2.log("=== GNOSIS SAFE EXECUTE ADD VERIFIER INFO ==="); + console2.log("Target Timelock Controller Address (To): ", timelockAddress); + console2.log("Verifier Router Address (dest): ", dest); + console2.log("Selector: ", Strings.toHexString(uint256(uint32(selector)))); + + bytes memory callData = + abi.encodeWithSignature("execute(address,uint256,bytes,bytes32,bytes32)", dest, 0, data, 0, 0); + console2.log("Function: execute(address,uint256,bytes,bytes32,bytes32)"); + console2.log("Calldata:"); + console2.logBytes(callData); + console2.log(""); + console2.log("================================"); + } +} + +/// @notice Schedule removal of a verifier from the router. +/// @dev Use the following environment variable to control the deployment: +/// * VERIFIER_SELECTOR the selector associated with this verifier +/// * SCHEDULE_DELAY (optional) minimum delay in seconds for the scheduled action +/// * TIMELOCK_CONTROLLER contract address of TimelockController +/// * VERIFIER_ROUTER contract address of RiscZeroVerifierRouter +/// +/// See the Foundry documentation for more information about Solidity scripts. +/// https://book.getfoundry.sh/guides/scripting-with-solidity +contract ScheduleRemoveVerifier is RiscZeroManagementScript { + function run() external withConfig { + // Check for deployment mode flags + bool gnosisExecute = vm.envOr("GNOSIS_EXECUTE", false); + bytes4 selector = bytes4(vm.envBytes("VERIFIER_SELECTOR")); + console2.log("selector:"); + console2.logBytes4(selector); + + // Schedule the 'removeVerifier()' request + uint256 scheduleDelay = vm.envOr("SCHEDULE_DELAY", timelockController().getMinDelay()); + console2.log("scheduleDelay:", scheduleDelay); + + bytes memory data = abi.encodeCall(verifierRouter().removeVerifier, selector); + address dest = address(verifierRouter()); + simulate(dest, data); + + if (gnosisExecute) { + _printGnosisSafeInfo(address(timelockController()), dest, selector, data, scheduleDelay); + return; + } + + vm.broadcast(adminAddress()); + timelockController().schedule(dest, 0, data, 0, 0, scheduleDelay); + } + + /// @notice Print Gnosis Safe transaction information for manual submissions + /// @param timelockAddress The timelock controller address (target for Gnosis Safe) + /// @param dest The destination address for the scheduled operation + /// @param selector The verifier selector being removed + /// @param data The calldata for the scheduled operation + /// @param scheduleDelay The minimum delay in seconds for the scheduled action + function _printGnosisSafeInfo( + address timelockAddress, + address dest, + bytes4 selector, + bytes memory data, + uint256 scheduleDelay + ) internal pure { + console2.log("================================"); + console2.log("================================"); + console2.log("=== GNOSIS SAFE SCHEDULE REMOVE VERIFIER INFO ==="); + console2.log("Target Timelock Controller Address (To): ", timelockAddress); + console2.log("Verifier Router Address (dest): ", dest); + console2.log("Selector: ", Strings.toHexString(uint256(uint32(selector)))); + console2.log("scheduleDelay: ", scheduleDelay); + + bytes memory callData = abi.encodeWithSignature( + "schedule(address,uint256,bytes,bytes32,bytes32,uint256)", dest, 0, data, 0, 0, scheduleDelay + ); + console2.log("Function: schedule(address,uint256,bytes,bytes32,bytes32,uint256)"); + console2.log("Calldata:"); + console2.logBytes(callData); + console2.log(""); + console2.log("================================"); + } +} + +/// @notice Finish removal of a verifier from the router. +/// @dev Use the following environment variable to control the deployment: +/// * VERIFIER_SELECTOR the selector associated with this verifier +/// * TIMELOCK_CONTROLLER contract address of TimelockController +/// * VERIFIER_ROUTER contract address of RiscZeroVerifierRouter +/// +/// See the Foundry documentation for more information about Solidity scripts. +/// https://book.getfoundry.sh/guides/scripting-with-solidity +contract FinishRemoveVerifier is RiscZeroManagementScript { + function run() external withConfig { + // Check for deployment mode flags + bool gnosisExecute = vm.envOr("GNOSIS_EXECUTE", false); + bytes4 selector = bytes4(vm.envBytes("VERIFIER_SELECTOR")); + console2.log("selector:"); + console2.logBytes4(selector); + + // Execute the 'removeVerifier()' request + bytes memory data = abi.encodeCall(verifierRouter().removeVerifier, selector); + + if (gnosisExecute) { + _printGnosisSafeInfo(address(timelockController()), address(verifierRouter()), selector, data); + return; + } + + vm.broadcast(adminAddress()); + timelockController().execute(address(verifierRouter()), 0, data, 0, 0); + } + + /// @notice Print Gnosis Safe transaction information for manual submissions + /// @param timelockAddress The timelock controller address (target for Gnosis Safe) + /// @param dest The destination address for the scheduled operation + /// @param selector The verifier selector being removed + /// @param data The calldata for the scheduled operation + function _printGnosisSafeInfo(address timelockAddress, address dest, bytes4 selector, bytes memory data) + internal + pure + { + console2.log("================================"); + console2.log("================================"); + console2.log("=== GNOSIS SAFE EXECUTE REMOVE VERIFIER INFO ==="); + console2.log("Target Timelock Controller Address (To): ", timelockAddress); + console2.log("Verifier Router Address (dest): ", dest); + console2.log("Selector: ", Strings.toHexString(uint256(uint32(selector)))); + + bytes memory callData = + abi.encodeWithSignature("execute(address,uint256,bytes,bytes32,bytes32)", dest, 0, data, 0, 0); + console2.log("Function: execute(address,uint256,bytes,bytes32,bytes32)"); + console2.log("Calldata:"); + console2.logBytes(callData); + console2.log(""); + console2.log("================================"); + } +} + +/// @notice Schedule an update of the minimum timelock delay. +/// @dev Use the following environment variable to control the deployment: +/// * MIN_DELAY minimum delay in seconds for operations +/// * SCHEDULE_DELAY (optional) minimum delay in seconds for the scheduled action +/// * TIMELOCK_CONTROLLER contract address of TimelockController +/// +/// See the Foundry documentation for more information about Solidity scripts. +/// https://book.getfoundry.sh/guides/scripting-with-solidity +contract ScheduleUpdateDelay is RiscZeroManagementScript { + function run() external withConfig { + // Check for deployment mode flags + bool gnosisExecute = vm.envOr("GNOSIS_EXECUTE", false); + uint256 minDelay = vm.envUint("MIN_DELAY"); + console2.log("minDelay:", minDelay); + + // Schedule the 'updateDelay()' request + uint256 scheduleDelay = vm.envOr("SCHEDULE_DELAY", timelockController().getMinDelay()); + console2.log("scheduleDelay:", scheduleDelay); + + bytes memory data = abi.encodeCall(timelockController().updateDelay, minDelay); + address dest = address(timelockController()); + simulate(dest, data); + + if (gnosisExecute) { + _printGnosisSafeInfo(address(timelockController()), minDelay, data, scheduleDelay); + return; + } + + vm.broadcast(adminAddress()); + timelockController().schedule(dest, 0, data, 0, 0, scheduleDelay); + } + + /// @notice Print Gnosis Safe transaction information for manual submissions + /// @param timelockAddress The timelock controller address (target for Gnosis Safe) + /// @param minDelay The new minimum delay + /// @param data The calldata for the scheduled operation + /// @param scheduleDelay The minimum delay in seconds for the scheduled action + function _printGnosisSafeInfo(address timelockAddress, uint256 minDelay, bytes memory data, uint256 scheduleDelay) + internal + pure + { + console2.log("================================"); + console2.log("================================"); + console2.log("=== GNOSIS SAFE SCHEDULE MIN DELAY INFO ==="); + console2.log("Target Timelock Controller Address (To): ", timelockAddress); + console2.log("New min delay: ", minDelay); + console2.log("scheduleDelay: ", scheduleDelay); + + bytes memory callData = abi.encodeWithSignature( + "schedule(address,uint256,bytes,bytes32,bytes32,uint256)", timelockAddress, 0, data, 0, 0, scheduleDelay + ); + console2.log("Function: schedule(address,uint256,bytes,bytes32,bytes32,uint256)"); + console2.log("Calldata:"); + console2.logBytes(callData); + console2.log(""); + console2.log("================================"); + } +} + +/// @notice Finish an update of the minimum timelock delay. +/// @dev Use the following environment variable to control the deployment: +/// * MIN_DELAY minimum delay in seconds for operations +/// * TIMELOCK_CONTROLLER contract address of TimelockController +/// +/// See the Foundry documentation for more information about Solidity scripts. +/// https://book.getfoundry.sh/guides/scripting-with-solidity +contract FinishUpdateDelay is RiscZeroManagementScript { + function run() external withConfig { + // Check for deployment mode flags + bool gnosisExecute = vm.envOr("GNOSIS_EXECUTE", false); + uint256 minDelay = vm.envUint("MIN_DELAY"); + console2.log("minDelay:", minDelay); + + // Execute the 'updateDelay()' request + bytes memory data = abi.encodeCall(timelockController().updateDelay, minDelay); + + if (gnosisExecute) { + _printGnosisSafeInfo(address(timelockController()), minDelay, data); + return; + } + + vm.broadcast(adminAddress()); + timelockController().execute(address(timelockController()), 0, data, 0, 0); + } + + /// @notice Print Gnosis Safe transaction information for manual submissions + /// @param timelockAddress The timelock controller address (target for Gnosis Safe) + /// @param minDelay The new minimum delay + /// @param data The calldata for the scheduled operation + function _printGnosisSafeInfo(address timelockAddress, uint256 minDelay, bytes memory data) internal pure { + console2.log("================================"); + console2.log("================================"); + console2.log("=== GNOSIS SAFE EXECUTE MIN DELAY INFO ==="); + console2.log("Target Timelock Controller Address (To): ", timelockAddress); + console2.log("New min delay: ", minDelay); + + bytes memory callData = + abi.encodeWithSignature("execute(address,uint256,bytes,bytes32,bytes32)", timelockAddress, 0, data, 0, 0); + console2.log("Function: execute(address,uint256,bytes,bytes32,bytes32)"); + console2.log("Calldata:"); + console2.logBytes(callData); + console2.log(""); + console2.log("================================"); + } +} + +// TODO: Add this command to the README.md +/// @notice Cancel a pending operation on the timelock controller +/// @dev Use the following environment variable to control the script: +/// * TIMELOCK_CONTROLLER contract address of TimelockController +/// * OPERATION_ID identifier for the operation to cancel +/// +/// See the Foundry documentation for more information about Solidity scripts. +/// https://book.getfoundry.sh/guides/scripting-with-solidity +contract CancelOperation is RiscZeroManagementScript { + function run() external withConfig { + // Check for deployment mode flags + bool gnosisExecute = vm.envOr("GNOSIS_EXECUTE", false); + bytes32 operationId = vm.envBytes32("OPERATION_ID"); + console2.log("operationId:", uint256(operationId)); + + if (gnosisExecute) { + _printGnosisSafeInfo(address(timelockController()), operationId); + return; + } + + // Execute the 'cancel()' request + vm.broadcast(adminAddress()); + timelockController().cancel(operationId); + } + + /// @notice Print Gnosis Safe transaction information for manual submissions + function _printGnosisSafeInfo(address timelockAddress, bytes32 operationId) internal pure { + console2.log("================================"); + console2.log("================================"); + console2.log("=== GNOSIS SAFE CANCEL OPERATION INFO ==="); + console2.log("Target Timelock Controller Address (To): ", timelockAddress); + console2.log("Operation ID: ", uint256(operationId)); + + bytes memory callData = abi.encodeWithSignature("cancel(bytes32)", operationId); + console2.log("Function: cancel(bytes32)"); + console2.log("Calldata:"); + console2.logBytes(callData); + console2.log(""); + console2.log("================================"); + } +} + +/// @notice Schedule grant role. +/// @dev Use the following environment variable to control the deployment: +/// * ROLE the role to be granted +/// * ACCOUNT the account to be granted the role +/// * SCHEDULE_DELAY (optional) minimum delay in seconds for the scheduled action +/// * TIMELOCK_CONTROLLER contract address of TimelockController +/// +/// See the Foundry documentation for more information about Solidity scripts. +/// https://book.getfoundry.sh/guides/scripting-with-solidity +contract ScheduleGrantRole is RiscZeroManagementScript { + function run() external withConfig { + // Check for deployment mode flags + bool gnosisExecute = vm.envOr("GNOSIS_EXECUTE", false); + string memory roleStr = vm.envString("ROLE"); + console2.log("roleStr:", roleStr); + + address account = vm.envAddress("ACCOUNT"); + console2.log("account:", account); + + // Schedule the 'grantRole()' request + bytes32 role = timelockControllerRole(timelockController(), roleStr); + console2.log("role: "); + console2.logBytes32(role); + + uint256 scheduleDelay = vm.envOr("SCHEDULE_DELAY", timelockController().getMinDelay()); + console2.log("scheduleDelay:", scheduleDelay); + + bytes memory data = abi.encodeCall(timelockController().grantRole, (role, account)); + address dest = address(timelockController()); + simulate(dest, data); + + if (gnosisExecute) { + _printGnosisSafeInfo(address(timelockController()), role, account, data, scheduleDelay); + return; + } + + vm.broadcast(adminAddress()); + timelockController().schedule(dest, 0, data, 0, 0, scheduleDelay); + } + + /// @notice Print Gnosis Safe transaction information for manual submissions + function _printGnosisSafeInfo( + address timelockAddress, + bytes32 role, + address account, + bytes memory data, + uint256 scheduleDelay + ) internal pure { + console2.log("================================"); + console2.log("================================"); + console2.log("=== GNOSIS SAFE SCHEDULE GRANT ROLE INFO ==="); + console2.log("Target Timelock Controller Address (To): ", timelockAddress); + console2.log("Role: ", uint256(role)); + console2.log("Account: ", account); + console2.log("scheduleDelay: ", scheduleDelay); + + bytes memory callData = abi.encodeWithSignature( + "schedule(address,uint256,bytes,bytes32,bytes32,uint256)", timelockAddress, 0, data, 0, 0, scheduleDelay + ); + console2.log("Function: schedule(address,uint256,bytes,bytes32,bytes32,uint256)"); + console2.log("Calldata:"); + console2.logBytes(callData); + console2.log(""); + console2.log("================================"); + } +} + +/// @notice Finish grant role. +/// @dev Use the following environment variable to control the deployment: +/// * ROLE the role to be granted +/// * ACCOUNT the account to be granted the role +/// * TIMELOCK_CONTROLLER contract address of TimelockController +/// +/// See the Foundry documentation for more information about Solidity scripts. +/// https://book.getfoundry.sh/guides/scripting-with-solidity +contract FinishGrantRole is RiscZeroManagementScript { + function run() external withConfig { + // Check for deployment mode flags + bool gnosisExecute = vm.envOr("GNOSIS_EXECUTE", false); + string memory roleStr = vm.envString("ROLE"); + console2.log("roleStr:", roleStr); + + address account = vm.envAddress("ACCOUNT"); + console2.log("account:", account); + + // Execute the 'grantRole()' request + bytes32 role = timelockControllerRole(timelockController(), roleStr); + console2.log("role: "); + console2.logBytes32(role); + + bytes memory data = abi.encodeCall(timelockController().grantRole, (role, account)); + + if (gnosisExecute) { + _printGnosisSafeInfo(address(timelockController()), role, account, data); + return; + } + + vm.broadcast(adminAddress()); + timelockController().execute(address(timelockController()), 0, data, 0, 0); + } + + /// @notice Print Gnosis Safe transaction information for manual submissions + function _printGnosisSafeInfo(address timelockAddress, bytes32 role, address account, bytes memory data) + internal + pure + { + console2.log("================================"); + console2.log("================================"); + console2.log("=== GNOSIS SAFE EXECUTE GRANT ROLE INFO ==="); + console2.log("Target Timelock Controller Address (To): ", timelockAddress); + console2.log("Role: ", uint256(role)); + console2.log("Account: ", account); + + bytes memory callData = + abi.encodeWithSignature("execute(address,uint256,bytes,bytes32,bytes32)", timelockAddress, 0, data, 0, 0); + console2.log("Function: execute(address,uint256,bytes,bytes32,bytes32)"); + console2.log("Calldata:"); + console2.logBytes(callData); + console2.log(""); + console2.log("================================"); + } +} + +/// @notice Schedule revoke role. +/// @dev Use the following environment variable to control the deployment: +/// * ROLE the role to be revoked +/// * ACCOUNT the account to be revoked of the role +/// * SCHEDULE_DELAY (optional) minimum delay in seconds for the scheduled action +/// * TIMELOCK_CONTROLLER contract address of TimelockController +/// +/// See the Foundry documentation for more information about Solidity scripts. +/// https://book.getfoundry.sh/guides/scripting-with-solidity +contract ScheduleRevokeRole is RiscZeroManagementScript { + function run() external withConfig { + // Check for deployment mode flags + bool gnosisExecute = vm.envOr("GNOSIS_EXECUTE", false); + string memory roleStr = vm.envString("ROLE"); + console2.log("roleStr:", roleStr); + + address account = vm.envAddress("ACCOUNT"); + console2.log("account:", account); + + // Schedule the 'grantRole()' request + bytes32 role = timelockControllerRole(timelockController(), roleStr); + console2.log("role: "); + console2.logBytes32(role); + + uint256 scheduleDelay = vm.envOr("SCHEDULE_DELAY", timelockController().getMinDelay()); + console2.log("scheduleDelay:", scheduleDelay); + + bytes memory data = abi.encodeCall(timelockController().revokeRole, (role, account)); + address dest = address(timelockController()); + simulate(dest, data); + + if (gnosisExecute) { + _printGnosisSafeInfo(address(timelockController()), role, account, data, scheduleDelay); + return; + } + + vm.broadcast(adminAddress()); + timelockController().schedule(dest, 0, data, 0, 0, scheduleDelay); + } + + /// @notice Print Gnosis Safe transaction information for manual submissions + function _printGnosisSafeInfo( + address timelockAddress, + bytes32 role, + address account, + bytes memory data, + uint256 scheduleDelay + ) internal pure { + console2.log("================================"); + console2.log("================================"); + console2.log("=== GNOSIS SAFE SCHEDULE REVOKE ROLE INFO ==="); + console2.log("Target Timelock Controller Address (To): ", timelockAddress); + console2.log("Role: ", uint256(role)); + console2.log("Account: ", account); + console2.log("scheduleDelay: ", scheduleDelay); + + bytes memory callData = abi.encodeWithSignature( + "schedule(address,uint256,bytes,bytes32,bytes32,uint256)", timelockAddress, 0, data, 0, 0, scheduleDelay + ); + console2.log("Function: schedule(address,uint256,bytes,bytes32,bytes32,uint256)"); + console2.log("Calldata:"); + console2.logBytes(callData); + console2.log(""); + console2.log("================================"); + } +} + +/// @notice Finish revoke role. +/// @dev Use the following environment variable to control the deployment: +/// * ROLE the role to be revoked +/// * ACCOUNT the account to be revoked of the role +/// * TIMELOCK_CONTROLLER contract address of TimelockController +/// +/// See the Foundry documentation for more information about Solidity scripts. +/// https://book.getfoundry.sh/guides/scripting-with-solidity +contract FinishRevokeRole is RiscZeroManagementScript { + function run() external withConfig { + // Check for deployment mode flags + bool gnosisExecute = vm.envOr("GNOSIS_EXECUTE", false); + string memory roleStr = vm.envString("ROLE"); + console2.log("roleStr:", roleStr); + + address account = vm.envAddress("ACCOUNT"); + console2.log("account:", account); + + // Execute the 'grantRole()' request + bytes32 role = timelockControllerRole(timelockController(), roleStr); + console2.log("role: "); + console2.logBytes32(role); + + bytes memory data = abi.encodeCall(timelockController().revokeRole, (role, account)); + + if (gnosisExecute) { + _printGnosisSafeInfo(address(timelockController()), role, account, data); + return; + } + + vm.broadcast(adminAddress()); + timelockController().execute(address(timelockController()), 0, data, 0, 0); + } + + /// @notice Print Gnosis Safe transaction information for manual submissions + function _printGnosisSafeInfo(address timelockAddress, bytes32 role, address account, bytes memory data) + internal + pure + { + console2.log("================================"); + console2.log("================================"); + console2.log("=== GNOSIS SAFE EXECUTE REVOKE ROLE INFO ==="); + console2.log("Target Timelock Controller Address (To): ", timelockAddress); + console2.log("Role: ", uint256(role)); + console2.log("Account: ", account); + + bytes memory callData = + abi.encodeWithSignature("execute(address,uint256,bytes,bytes32,bytes32)", timelockAddress, 0, data, 0, 0); + console2.log("Function: execute(address,uint256,bytes,bytes32,bytes32)"); + console2.log("Calldata:"); + console2.logBytes(callData); + console2.log(""); + console2.log("================================"); + } +} + +/// @notice Renounce role. +/// @dev Use the following environment variable to control the deployment: +/// * RENOUNCE_ADDRESS the address to send the renounce transaction +/// * RENOUNCE_ROLE the role to be renounced +/// * TIMELOCK_CONTROLLER contract address of TimelockController +/// +/// See the Foundry documentation for more information about Solidity scripts. +/// https://book.getfoundry.sh/guides/scripting-with-solidity +contract RenounceRole is RiscZeroManagementScript { + function run() external withConfig { + address renouncer = vm.envAddress("RENOUNCE_ADDRESS"); + string memory roleStr = vm.envString("RENOUNCE_ROLE"); + console2.log("renouncer:", renouncer); + console2.log("roleStr:", roleStr); + + console2.log("msg.sender:", msg.sender); + + // Renounce the role + bytes32 role = timelockControllerRole(timelockController(), roleStr); + console2.log("role: "); + console2.logBytes32(role); + + vm.broadcast(renouncer); + timelockController().renounceRole(role, msg.sender); + } +} + +/// @notice Activate an Emergency Stop mechanism. +/// @dev Use the following environment variable to control the deployment: +/// * VERIFIER_ESTOP contract address of RiscZeroVerifierEmergencyStop +/// +/// See the Foundry documentation for more information about Solidity scripts. +/// https://book.getfoundry.sh/guides/scripting-with-solidity +contract ActivateEstop is RiscZeroManagementScript { + function run() external withConfig { + // Check for deployment mode flags + bool gnosisExecute = vm.envOr("GNOSIS_EXECUTE", false); + // Locate contracts + console2.log("Using RiscZeroVerifierEmergencyStop at address", address(verifierEstop())); + + if (gnosisExecute) { + _printGnosisSafeInfo(address(verifierEstop())); + return; + } + // Activate the emergency stop + vm.broadcast(adminAddress()); + verifierEstop().estop(); + require(verifierEstop().paused(), "verifier is not stopped after calling estop"); + } + + // @notice Print Gnosis Safe transaction information for manual submissions + function _printGnosisSafeInfo(address estopAddress) internal pure { + console2.log("================================"); + console2.log("================================"); + console2.log("=== GNOSIS SAFE ACTIVATE EMERGENCY STOP INFO ==="); + console2.log("RiscZeroVerifierEmergencyStop Address (To): ", estopAddress); + bytes memory callData = abi.encodeWithSignature("estop()"); + console2.log("Function: estop()"); + console2.log("Calldata:"); + console2.logBytes(callData); + console2.log(""); + console2.log("================================"); + } +} diff --git a/contracts/scripts/VERIFIER_DEPLOYMENT.md b/contracts/scripts/VERIFIER_DEPLOYMENT.md new file mode 100644 index 000000000..8da9e3667 --- /dev/null +++ b/contracts/scripts/VERIFIER_DEPLOYMENT.md @@ -0,0 +1,433 @@ +# Contract Operations Guide + +An operations guide for the Boundless verifier contracts. + +> [!NOTE] +> All the commands in this guide assume your current working directory is the root of the repo. + +## Dependencies + +Requires [Foundry](https://book.getfoundry.sh/getting-started/installation). + +> [!NOTE] +> Running the `manage-verifier` commands will run in simulation mode (i.e. will not send transactions) unless the `--broadcast` flag is passed. +> When setting `GNOSIS_EXECUTE=true` all the transactions calldata will be printed so that they can be copied over to the Safe web app. + +Commands in this guide use `yq` to parse the TOML config files. + +You can install `yq` by following the [directions on GitHub][yq-install], or using `go install`. + +```sh +go install github.com/mikefarah/yq/v4@latest +``` + +## Configuration + +Configurations and deployment state information is stored in `deployment_verifier.toml`. +It contains information about each chain (e.g. name, ID, Etherscan URL), and addresses for the timelock, router, and verifier contracts on each chain. + +Accompanying the `deployment_verifier.toml` file is a `deployment_secrets.toml` file with the following schema. +It is used to store somewhat sensitive API keys for RPC services and Etherscan. +Note that it does not contain private keys or API keys for Fireblocks. +It should never be committed to `git`, and the API keys should be rotated if this occurs. + +```toml +[chains.$CHAIN_KEY] +rpc-url = "..." +etherscan-api-key = "..." +``` + +## Environment + +### Public Networks (Testnet or Mainnet) + +Set the chain you are operating on by the key from the `deployment_verifier.toml` file. +An example chain key is "ethereum-sepolia", and you can look at `deployment_verifier.toml` for the full list. + +```sh +export CHAIN_KEY="xxx-testnet" +``` + +**Based on the chain key, the `manage-verifier` script will automatically load environment variables from deployment_verifier.toml and deployment_secrets.toml** + +If the chain you are deploying to is not in `deployment_secrets.toml`, set your RPC URL, public and private key, and Etherscan API key: + +```sh +export RPC_URL=$(yq eval -e ".chains[\"${CHAIN_KEY:?}\"].rpc-url" contracts/deployment_secrets.toml | tee /dev/stderr) +export ETHERSCAN_URL=$(yq eval -e ".chains[\"${CHAIN_KEY:?}\"].etherscan-url" contracts/deployment_verifier.toml | tee /dev/stderr) +export ETHERSCAN_API_KEY=$(yq eval -e ".chains[\"${CHAIN_KEY:?}\"].etherscan-api-key" contracts/deployment_secrets.toml | tee /dev/stderr) +``` + +> [!TIP] +> Foundry has a [config full of information about each chain][alloy-chains], mapped from chain ID. +> It includes the Etherscan compatible API URL, which is how only specifying the API key works. +> You can find this list in the following source file: + +Example RPC URLs: + +- `https://eth-sepolia.g.alchemy.com/v2/YOUR_API_KEY` +- `https://sepolia.infura.io/v3/YOUR_API_KEY` + +## Deploy the timelocked router + +1. Dry run the contract deployment: + + > [!IMPORTANT] + > Adjust the `MIN_DELAY` (or `timelock-delay` in the toml) to a value appropriate for the environment (e.g. 0 second for testnet and 259200 seconds (3 days) for mainnet). + + ```sh + contracts/scripts/manage-verifier DeployTimelockRouter + + ... + + == Logs == + minDelay: 1 + proposers: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 + executors: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 + admin: 0x0000000000000000000000000000000000000000 + Deployed TimelockController to 0x5FbDB2315678afecb367f032d93F642f64180aa3 + Deployed VerifierLayeredRouter to 0x918063A3fa14C59b390B18db8b1A565780E8b933 + ``` + +2. Run the command again with `--broadcast`. + + This will result in two transactions sent from the deployer address. + +3. Test the deployment. + + ```console + FOUNDRY_PROFILE=deployment-test forge test --match-contract=VerifierDeploymentTest -vv --fork-url=${RPC_URL:?} + ``` + +## Deploy a Blae3Groth16 verifier with emergency stop mechanism + +This is a two-step process, guarded by the `TimelockController`. + +### Deploy the verifier + +1. Dry run deployment of BlakeGroth16 verifier and estop: + + ```sh + contracts/scripts/manage-verifier DeployEstopBlake3Groth16Verifier + ``` + + > [!IMPORTANT] + > Check the logs from this dry run to verify the estop owner is the expected address. + > It should be equal to the admin address on the given chain. + > Note that it should not be the `TimelockController`. + > Also check the chain ID to ensure you are deploying to the chain you expect. + > And check the selector to make sure it matches what you expect. + +2. Send deployment transactions for verifier and estop by running the command again with `--broadcast`. + + This will result in two transactions sent from the deployer address. + +3. Add the addresses for the newly deployed contract to the `deployment_verifier.toml` file. + +4. Test the deployment. + + ```sh + FOUNDRY_PROFILE=deployment-test forge test --match-contract=VerifierDeploymentTest -vv --fork-url=${RPC_URL:?} + ``` + +5. Print the operation to schedule the operation to add the verifier to the router. + + ```sh + GNOSIS_EXECUTE=true VERIFIER_SELECTOR="0x..." bash contracts/scripts/manage-verifier ScheduleAddVerifier + ``` + +6. Send the transaction for the scheduled update via Safe. + +### Finish the update + +After the delay on the timelock controller has passed, the operation to add the new verifier to the router can be executed. + +1. Print the transaction calldata to execute the add verifier operation: + + ```sh + GNOSIS_EXECUTE=true VERIFIER_SELECTOR="0x..." bash contracts/scripts/manage-verifier FinishAddVerifier + ``` + +2. Send the transaction for execution via Safe + +3. Remove the `unroutable` field from the selected verifier. + +4. Test the deployment. + + ```console + FOUNDRY_PROFILE=deployment-test forge test --match-contract=VerifierDeploymentTest -vv --fork-url=${RPC_URL:?} + ``` + +## Remove a verifier + +This is a two-step process, guarded by the `TimelockController`. + +### Schedule the update + +1. Print the transaction to schedule the remove verifier operation: + + ```sh + GNOSIS_EXECUTE=true VERIFIER_SELECTOR="0x..." contracts/scripts/manage-verifier ScheduleRemoveVerifier + ``` + +2. Send the transaction for execution via Safe + +### Finish the update + +1. Print the transaction to execute the remove verifier operation: + + ```sh + GNOSIS_EXECUTE=true VERIFIER_SELECTOR="0x..." contracts/scripts/manage-verifier FinishRemoveVerifier + ``` + +2. Send the transaction for execution via Safe + +3. Update `deployment_verifier.toml` and set `unroutable = true` on the removed verifier. + +4. Test the deployment. + + ```console + FOUNDRY_PROFILE=deployment-test forge test --match-contract=VerifierDeploymentTest -vv --fork-url=${RPC_URL:?} + ``` + +## Update the TimelockController minimum delay + +This is a two-step process, guarded by the `TimelockController`. + +The minimum delay (`MIN_DELAY`) on the timelock controller is denominated in seconds. + +### Schedule the update + +1. Print the transaction calldata: + + ```sh + GNOSIS_EXECUTE=true MIN_DELAY=10 contracts/scripts/manage-verifier ScheduleUpdateDelay + ``` + +2. Send the transaction for execution via Safe + +### Finish the update + +Execute the action: + +1. Print the transaction calldata: + + ```sh + GNOSIS_EXECUTE=true MIN_DELAY=10 contracts/scripts/manage-verifier FinishUpdateDelay + ``` + +2. Send the transaction for execution via Safe + +3. Test the deployment. + + ```console + FOUNDRY_PROFILE=deployment-test forge test --match-contract=VerifierDeploymentTest -vv --fork-url=${RPC_URL:?} + ``` + +## Cancel a scheduled timelock operation + +Use the following steps to cancel an operation that is pending on the `TimelockController`. + +1. Identify the operation ID and set the environment variable. + + > TIP: One way to get the operation ID is to open the contract in Etherscan and look at the events. + > On the `CallScheduled` event, the ID is labeled as `[topic1]`. + > + > ```sh + > open ${ETHERSCAN_URL:?}/address/${TIMELOCK_CONTROLLER:?}#events + > ``` + + ```sh + export OPERATION_ID="0x..." \ + ``` + +2. Print the transaction calldata to cancel the operation. + + ```sh + GNOSIS_EXECUTE=true contracts/scripts/manage-verifier CancelOperation + ``` + +3. Send the transaction for execution via Safe + +## Grant access to the TimelockController + +This is a two-step process, guarded by the `TimelockController`. + +Three roles are supported: + +- `proposer` +- `executor` +- `canceller` + +### Schedule the update + +1. Print the transaction calldata: + + ```sh + GNOSIS_EXECUTE=true \ + ROLE="executor" \ + ACCOUNT="0x00000000000000aabbccddeeff00000000000000" \ + bash contracts/scripts/manage-verifier ScheduleGrantRole + ``` + +2. Send the transaction for execution via Safe + +### Finish the update + +1. Print the transaction calldata: + + ```sh + GNOSIS_EXECUTE=true \ + ROLE="executor" \ + ACCOUNT="0x00000000000000aabbccddeeff00000000000000" \ + bash contracts/scripts/manage-verifier FinishGrantRole + ``` + +2. Send the transaction for execution via Safe. + +3. Confirm the update: + + ```sh + # Query the role code. + cast call --rpc-url ${RPC_URL:?} \ + ${TIMELOCK_CONTROLLER:?} \ + 'EXECUTOR_ROLE()(bytes32)' + 0xd8aa0f3194971a2a116679f7c2090f6939c8d4e01a2a8d7e41d55e5351469e63 + + # Check that the account now has that role. + cast call --rpc-url ${RPC_URL:?} \ + ${TIMELOCK_CONTROLLER:?} \ + 'hasRole(bytes32, address)(bool)' \ + 0xd8aa0f3194971a2a116679f7c2090f6939c8d4e01a2a8d7e41d55e5351469e63 \ + 0x00000000000000aabbccddeeff00000000000000 + true + ``` + +## Revoke access to the TimelockController + +This is a two-step process, guarded by the `TimelockController`. + +Three roles are supported: + +- `proposer` +- `executor` +- `canceller` + +### Schedule the update + +1. Print the transaction calldata: + + ```sh + GNOSIS_EXECUTE=true \ + ROLE="executor" \ + ACCOUNT="0x00000000000000aabbccddeeff00000000000000" \ + bash contracts/scripts/manage-verifier ScheduleRevokeRole + ``` + +2. Send the transaction for execution via Safe + +Confirm the role code: + +```sh +cast call --rpc-url ${RPC_URL:?} \ + ${TIMELOCK_CONTROLLER:?} \ + 'EXECUTOR_ROLE()(bytes32)' +0xd8aa0f3194971a2a116679f7c2090f6939c8d4e01a2a8d7e41d55e5351469e63 +``` + +### Finish the update + +1. Print the transaction calldata: + + ```sh + GNOSIS_EXECUTE=true \ + ROLE="executor" \ + ACCOUNT="0x00000000000000aabbccddeeff00000000000000" \ + bash contracts/scripts/manage-verifier FinishRevokeRole + ``` + +2. Send the transaction for execution via Safe + +3. Confirm the update: + + ```sh + # Query the role code. + cast call --rpc-url ${RPC_URL:?} \ + ${TIMELOCK_CONTROLLER:?} \ + 'EXECUTOR_ROLE()(bytes32)' + 0xd8aa0f3194971a2a116679f7c2090f6939c8d4e01a2a8d7e41d55e5351469e63 + + # Check that the account no longer has that role. + cast call --rpc-url ${RPC_URL:?} \ + ${TIMELOCK_CONTROLLER:?} \ + 'hasRole(bytes32, address)(bool)' \ + 0xd8aa0f3194971a2a116679f7c2090f6939c8d4e01a2a8d7e41d55e5351469e63 \ + 0x00000000000000aabbccddeeff00000000000000 + false + ``` + +## Renounce access to the TimelockController + +If your private key is compromised, you can renounce your role(s) without waiting for the time delay. Repeat this action for any of the roles you might have, such as: + +- proposer +- executor +- canceller + +> ![WARNING] +> Renouncing authorization on the timelock controller may make it permanently inoperable. + +1. Print the transaction calldata: + + ```sh + GNOSIS_EXECUTE=true \ + RENOUNCE_ROLE="executor" \ + RENOUNCE_ADDRESS="0x00000000000000aabbccddeeff00000000000000" \ + bash contracts/scripts/manage-verifier RenounceRole + ``` + +2. Send the transaction for execution via Safe + +3. Confirm: + + ```sh + cast call --rpc-url ${RPC_URL:?} \ + ${TIMELOCK_CONTROLLER:?} \ + 'hasRole(bytes32, address)(bool)' \ + 0xd8aa0f3194971a2a116679f7c2090f6939c8d4e01a2a8d7e41d55e5351469e63 \ + ${RENOUNCE_ADDRESS:?} + false + ``` + +## Activate the emergency stop + +Activate the emergency stop: + +> ![WARNING] +> Activating the emergency stop will make that verifier permanently inoperable. + +> ![NOTE] +> In order to send a transaction to the estop contract in Fireblocks, the addresses need to be added to the allow-list. +> If this has not already been done, do this as a pre-step. + +1. Print the transaction calldata: + + ```sh + GNOSIS_EXECUTE=true \ + VERIFIER_SELCTOR="0x..." \ + bash contracts/scripts/manage-verifier ActivateEstop + ``` + +2. Send the transaction for execution via Safe + +3. Test the activation: + + ```sh + cast call --rpc-url ${RPC_URL:?} \ + ${VERIFIER_ESTOP:?} \ + 'paused()(bool)' + true + ``` + +[yq-install]: https://github.com/mikefarah/yq?tab=readme-ov-file#install +[alloy-chains]: https://github.com/alloy-rs/chains/blob/main/src/named.rs diff --git a/contracts/scripts/manage-verifier b/contracts/scripts/manage-verifier new file mode 100755 index 000000000..cb8e8dc75 --- /dev/null +++ b/contracts/scripts/manage-verifier @@ -0,0 +1,201 @@ +#!/bin/bash + +set -eo pipefail + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +SCRIPT_FILE="${SCRIPT_DIR}/ManageVerifier.s.sol" +REPO_ROOT_DIR="${SCRIPT_DIR:?}/../.." +FIREBLOCKS=0 +export FOUNDRY_OUT=${FOUNDRY_OUT:-"contracts/out"} + +# # Check for python3, required for updating the deployment toml +# if ! command -v python3 >/dev/null 2>&1; then +# echo "❌ python3 is not installed" +# exit 1 +# fi + +# # Check for tomlkit (Python package), required for updating the deployment toml +# if ! python3 -c "import tomlkit" >/dev/null 2>&1; then +# echo "❌ tomlkit is not installed for python3" +# echo "To install: python3 -m pip install tomlkit" +# exit 1 +# fi + +# Check for yq +if ! command -v yq >/dev/null 2>&1; then + echo "❌ yq is not installed" + echo "Install yq v4+ from: https://github.com/mikefarah/yq" + exit 1 +fi + +POSITIONAL_ARGS=() +FORGE_SCRIPT_FLAGS=() + +while [[ $# -gt 0 ]]; do + case $1 in + -f|--fireblocks) + FIREBLOCKS=1 + shift # past argument + ;; + --broadcast|--verify) + FORGE_SCRIPT_FLAGS+=("$1") + shift + ;; + -*|--*) + echo "Unknown option $1" + exit 1 + ;; + *) + POSITIONAL_ARGS+=("$1") # save positional arg + shift # past argument + ;; + esac +done + +set -- "${POSITIONAL_ARGS[@]}" # restore positional parameters + +# HINT: deployment_secrets.toml contains API keys. You can write it yourself, or ask a friend. +load_env_var() { + local var_name="$1" + local config_key="$2" + local config_file="$3" + + # Get current value of the variable + local current_value=$(eval echo \$$var_name) + + if [ -z "$current_value" ]; then + echo "$var_name from $config_file: " > /dev/stderr + local new_value=$(yq eval -e "$config_key" "$REPO_ROOT_DIR/contracts/$config_file") + [ -n "$new_value" ] && [[ "$new_value" != "null" ]] || exit 1 + export $var_name="$new_value" + else + echo "$var_name from env $current_value" + fi +} + +# Run a Forge script with support for Fireblocks with options set automatically +forge_script() { + # Set our function. If the function is "help", or if the function is + # unspecified, then print some help. + local script_function="${1:-help}" + shift + + if [ "${script_function:?}" == "help" ]; then + cat << EOF +🔧 Verifiers Management Script +================================== + +Usage: $0 [options] + +Commands: + DeployTimelockRouter Deploy the TimelockController and Verifier Router contracts + DeployEstopBlake3Groth16Verifier Deploy the Estop and Blake3 Groth16 Verifier contracts + ScheduleAddVerifier Schedule adding a verifier to the Verifier Router contract + FinishAddVerifier Finish adding a verifier to the Verifier Router contract + ScheduleRemoveVerifier Schedule removing a verifier from the Verifier Router contract + FinishRemoveVerifier Finish removing a verifier from the Verifier Router contract + ScheduleUpdateDelay Schedule updating the timelock delay on the TimelockController contract + FinishUpdateDelay Finish updating the timelock delay on the TimelockController contract + CancelOperation Cancel a scheduled operation on the TimelockController contract + ScheduleGrantRole Schedule granting a role to an account on the TimelockController contract + FinishGrantRole Finish granting a role to an account on the TimelockController contract + ScheduleRevokeRole Schedule revoking a role from an account on the TimelockController contract + FinishRevokeRole Finish revoking a role from an account on the TimelockController contract + RenounceRole Renounce a role on the TimelockController contract + ActivateEstop Activate the emergency stop on the Verifier Router contract +Options: + -f, --fireblocks Use Fireblocks for transaction signing + --broadcast Broadcast transactions to network + --verify Verify contracts on Etherscan + -h, --help Show this help message + +Environment Variables: + CHAIN_KEY Required. Deployment environment key (anvil, ethereum-mainnet, ethereum-sepolia, ethereum-sepolia-staging) + STACK_TAG Optional. Stack tag for multi-deployment environments + DEPLOYER_PRIVATE_KEY Required. Private key for transaction signing (0x...) + DEPLOYER_ADDRESS Optional. Address for transaction signing + ADMIN_ADDRESS Optional. Address to use as admin for deployed contracts + VERIFIER_ESTOP_OWNER Optional. Address to set as estop owner for deployed verifiers (defaults to ADMIN_ADDRESS) + GNOSIS_EXECUTE Optional. If true, generate Gnosis Safe calldata for admin operations + VERIFIER_SELECTOR Required for verifier management commands. Verifier selector to add/remove (string) + SCHEDULE_DELAY Optional. Delay (in seconds) for scheduling operations (defaults to timelock delay) + MIN_DELAY Minimum delay (in seconds) for updating the timelock controller + OPERATION_ID Required for CancelOperation command. Operation ID to cancel (bytes32 string) + RENOUNCE_ADDRESS Optional. Address to renounce role from (defaults to DEPLOYER_PRIVATE_KEY address) + RENOUNCE_ROLE Required for RenounceRole command. Role to renounce (bytes32 string) + ACCOUNT Required for role management commands. Account to grant/revoke role to/from + ROLE Required for role management commands. Role to grant/revoke (bytes32 string) + +Examples: + # Deploy TimelockController and Verifier Router + CHAIN_KEY=ethereum-sepolia DEPLOYER_PRIVATE_KEY=0x... $0 DeployTimelockRouter --broadcast + + # Deploy Estop and Blake3 Groth16 Verifier + CHAIN_KEY=ethereum-sepolia DEPLOYER_PRIVATE_KEY=0x... $0 DeployEstopBlake3Groth16Verifier --broadcast + +Notes: + - Network configuration is loaded from deployment_verifier.toml and deployment_secrets.toml + - Private keys must be provided via DEPLOYER_PRIVATE_KEY environment variable + - Admin operations support GNOSIS_EXECUTE=true for Gnosis Safe calldata generation +EOF + exit 0 + fi + + # Load environment variables only when running actual commands + if [ -n "$STACK_TAG" ]; then + DEPLOY_KEY=${CHAIN_KEY:?}-${STACK_TAG:?} + else + DEPLOY_KEY=${CHAIN_KEY:?} + fi + + echo "Loading environment variables from deployment_verifier TOML files" + load_env_var "RPC_URL" ".chains[\"${CHAIN_KEY:?}\"].rpc-url" "deployment_secrets.toml" + load_env_var "ETHERSCAN_API_KEY" ".chains[\"${CHAIN_KEY:?}\"].etherscan-api-key" "deployment_secrets.toml" + load_env_var "CHAIN_ID" ".chains[\"${CHAIN_KEY:?}\"].id" "deployment_verifier.toml" + + # Check if we're on the correct network + CONNECTED_CHAIN_ID=$(cast chain-id --rpc-url ${RPC_URL:?}) + if [[ "${CONNECTED_CHAIN_ID:?}" != "${CHAIN_ID:?}" ]]; then + echo -e "${RED}Error: connected chain id and configured chain id do not match: ${CONNECTED_CHAIN_ID:?} != ${CHAIN_ID:?} ${NC}" + echo ${RPC_URL:?} + + exit 1 + fi + + local target="${SCRIPT_FILE:?}:${script_function:?}" + echo "Running forge script $target" + + if [ $FIREBLOCKS -gt 0 ]; then + # Check for fireblocks + if ! command -v fireblocks-json-rpc &> /dev/null + then + echo "fireblocks-json-rpc not found" + echo "can be installed with npm install -g @fireblocks/fireblocks-json-rpc" + exit 1 + fi + + # Run forge via fireblocks + fireblocks-json-rpc --verbose --rpcUrl ${RPC_URL:?} --http --apiKey ${FIREBLOCKS_API_KEY:?} -- \ + forge script ${FORGE_SCRIPT_FLAGS} \ + --slow --unlocked \ + --etherscan-api-key=${ETHERSCAN_API_KEY:?} \ + --rpc-url {} \ + "$target" "$@" + else + # Run forge + forge script ${FORGE_SCRIPT_FLAGS} \ + --private-key=${DEPLOYER_PRIVATE_KEY:?} \ + --etherscan-api-key=${ETHERSCAN_API_KEY:?} \ + --rpc-url ${RPC_URL:?} \ + "$target" "$@" + fi +} + +# Run from the repo root for consistency. +cd ${REPO_ROOT_DIR:?} + +# Get current git commit hash for deployment tracking +CURRENT_COMMIT=$(git rev-parse --short HEAD) +export CURRENT_COMMIT + +forge_script "$@" \ No newline at end of file diff --git a/contracts/src/BoundlessMarket.sol b/contracts/src/BoundlessMarket.sol index 9ef04261b..34e11be82 100644 --- a/contracts/src/BoundlessMarket.sol +++ b/contracts/src/BoundlessMarket.sol @@ -16,7 +16,11 @@ import {ERC20} from "solmate/tokens/ERC20.sol"; import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; import { - IRiscZeroVerifier, Receipt, ReceiptClaim, ReceiptClaimLib, VerificationFailed + IRiscZeroVerifier, + Receipt, + ReceiptClaim, + ReceiptClaimLib, + VerificationFailed } from "risc0/IRiscZeroVerifier.sol"; import {IRiscZeroSetVerifier} from "risc0/IRiscZeroSetVerifier.sol"; diff --git a/contracts/src/blake3-groth16/Blake3Groth16Verifier.sol b/contracts/src/blake3-groth16/Blake3Groth16Verifier.sol new file mode 100644 index 000000000..63427ea6e --- /dev/null +++ b/contracts/src/blake3-groth16/Blake3Groth16Verifier.sol @@ -0,0 +1,165 @@ +// Copyright 2025 Boundless Foundation, Inc. +// +// Use of this source code is governed by the Business Source License +// as found in the LICENSE-BSL file. +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.9; + +import {SafeCast} from "openzeppelin/contracts/utils/math/SafeCast.sol"; + +import {Groth16Verifier} from "./Groth16Verifier.sol"; +import { + IRiscZeroVerifier, + Output, + OutputLib, + Receipt, + ReceiptClaim, + ReceiptClaimLib, + VerificationFailed +} from "risc0/IRiscZeroVerifier.sol"; +import {StructHash} from "risc0/StructHash.sol"; +import {reverseByteOrderUint256} from "risc0/Util.sol"; +import {IRiscZeroSelectable} from "risc0/IRiscZeroSelectable.sol"; + +/// @notice A Groth16 seal over the claimed receipt claim. +struct Seal { + uint256[2] a; + uint256[2][2] b; + uint256[2] c; +} + +/// @notice Error raised when this verifier receives a receipt with a selector that does not match +/// its own. The selector value is calculated from the verifier parameters, and so this +/// usually indicates a mismatch between the version of the prover and this verifier. +error SelectorMismatch(bytes4 received, bytes4 expected); + +/// @notice Blake3Groth16 verifier contract for RISC Zero receipts of execution. +contract Blake3Groth16Verifier is IRiscZeroVerifier, IRiscZeroSelectable, Groth16Verifier { + using ReceiptClaimLib for ReceiptClaim; + using OutputLib for Output; + using SafeCast for uint256; + + /// @notice Semantic version of the RISC Zero system of which this contract is part. + /// @dev This is set to be equal to the version of the risc0-zkvm crate. + string public constant VERSION = "0.0.1"; + + /// @notice Control root hash binding the set of circuits in the RISC Zero system. + /// @dev This value controls what set of recursion programs (e.g. lift, join, resolve), and + /// therefore what version of the zkVM circuit, will be accepted by this contract. Each + /// instance of this verifier contract will accept a single release of the RISC Zero circuits. + /// + /// New releases of RISC Zero's zkVM require updating these values. These values can be + /// calculated from the [risc0 monorepo][1] using: `cargo xtask bootstrap`. + /// + /// [1]: https://github.com/risc0/risc0 + bytes16 public immutable CONTROL_ROOT_0; + bytes16 public immutable CONTROL_ROOT_1; + bytes32 public immutable BN254_CONTROL_ID; + + /// @notice A short key attached to the seal to select the correct verifier implementation. + /// @dev The selector is taken from the hash of the verifier parameters including the Groth16 + /// verification key and the control IDs that commit to the RISC Zero circuits. If two + /// receipts have different selectors (i.e. different verifier parameters), then it can + /// generally be assumed that they need distinct verifier implementations. This is used as + /// part of the RISC Zero versioning mechanism. + /// + /// A selector is not intended to be collision resistant, in that it is possible to find + /// two preimages that result in the same selector. This is acceptable since it's purpose + /// to a route a request among a set of trusted verifiers, and to make errors of sending a + /// receipt to a mismatching verifiers easier to debug. It is analogous to the ABI + /// function selectors. + bytes4 public immutable SELECTOR; + + /// @notice Identifier for the Groth16 verification key encoded into the base contract. + /// @dev This value is computed at compile time. + function verifierKeyDigest() internal pure returns (bytes32) { + bytes32[] memory icDigests = new bytes32[](2); + icDigests[0] = sha256(abi.encodePacked(IC0x, IC0y)); + icDigests[1] = sha256(abi.encodePacked(IC1x, IC1y)); + + return sha256( + abi.encodePacked( + // tag + sha256("risc0_groth16.VerifyingKey"), + // down + sha256(abi.encodePacked(alphax, alphay)), + sha256(abi.encodePacked(betax1, betax2, betay1, betay2)), + sha256(abi.encodePacked(gammax1, gammax2, gammay1, gammay2)), + sha256(abi.encodePacked(deltax1, deltax2, deltay1, deltay2)), + StructHash.taggedList(sha256("risc0_groth16.VerifyingKey.IC"), icDigests), + // down length + uint16(5) << 8 + ) + ); + } + + constructor(bytes32 controlRoot, bytes32 bn254ControlId) { + (CONTROL_ROOT_0, CONTROL_ROOT_1) = splitDigest(controlRoot); + BN254_CONTROL_ID = bn254ControlId; + + SELECTOR = bytes4( + sha256( + abi.encodePacked( + // tag + sha256("risc0.Groth16ReceiptVerifierParameters"), + // down + controlRoot, + reverseByteOrderUint256(uint256(bn254ControlId)), + verifierKeyDigest(), + // down length + uint16(3) << 8 + ) + ) + ); + } + + /// @notice splits a digest into two 128-bit halves to use as public signal inputs. + /// @dev RISC Zero's Circom verifier circuit takes each of two hash digests in two 128-bit + /// chunks. These values can be derived from the digest by splitting the digest in half and + /// then reversing the bytes of each. + function splitDigest(bytes32 digest) internal pure returns (bytes16, bytes16) { + uint256 reversed = reverseByteOrderUint256(uint256(digest)); + return (bytes16(uint128(reversed)), bytes16(uint128(reversed >> 128))); + } + + /// @inheritdoc IRiscZeroVerifier + function verify(bytes calldata seal, bytes32 imageId, bytes32 journalDigest) external pure { + seal; + imageId; + journalDigest; + revert("Use verifyIntegrity"); + } + + /// @inheritdoc IRiscZeroVerifier + function verifyIntegrity(Receipt calldata receipt) external view { + return _verifyIntegrity(receipt.seal, receipt.claimDigest); + } + + /// @notice internal implementation of verifyIntegrity, factored to avoid copying calldata bytes to memory. + function _verifyIntegrity(bytes calldata seal, bytes32 claimDigest) internal view { + // Check that the seal has a matching selector. Mismatch generally indicates that the + // prover and this verifier are using different parameters, and so the verification + // will not succeed. + if (SELECTOR != bytes4(seal[:4])) { + revert SelectorMismatch({received: bytes4(seal[:4]), expected: SELECTOR}); + } + + // Run the Groth16 verify procedure. + Seal memory decodedSeal = abi.decode(seal[4:], (Seal)); + bool verified = this.verifyProof( + decodedSeal.a, + decodedSeal.b, + decodedSeal.c, + [ + /// Blake3(Sha256(control_root, pre_state_digest, post_state_digest, id_bn254_fr), journal)[:31] + uint256(claimDigest) + ] + ); + + // Revert is verification failed. + if (!verified) { + revert VerificationFailed(); + } + } +} diff --git a/contracts/src/blake3-groth16/ControlID.sol b/contracts/src/blake3-groth16/ControlID.sol new file mode 100644 index 000000000..bbf34db11 --- /dev/null +++ b/contracts/src/blake3-groth16/ControlID.sol @@ -0,0 +1,16 @@ +// Copyright 2025 Boundless Foundation, Inc. +// +// Use of this source code is governed by the Business Source License +// as found in the LICENSE-BSL file. +// SPDX-License-Identifier: BUSL-1.1 + +// This file is automatically generated by: +// cargo xtask bootstrap-groth16 + +pragma solidity ^0.8.9; + +library ControlID { + bytes32 public constant CONTROL_ROOT = hex"a54dc85ac99f851c92d7c96d7318af41dbe7c0194edfcc37eb4d422a998c1f56"; + // NOTE: This has the opposite byte order to the value in the risc0 repository. + bytes32 public constant BN254_CONTROL_ID = hex"04446e66d300eb7fb45c9726bb53c793dda407a62e9601618bb43c5c14657ac0"; +} diff --git a/contracts/src/blake3-groth16/Groth16Verifier.sol b/contracts/src/blake3-groth16/Groth16Verifier.sol new file mode 100644 index 000000000..9baf9d805 --- /dev/null +++ b/contracts/src/blake3-groth16/Groth16Verifier.sol @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: GPL-3.0 +/* + Copyright 2021 0KIMS association. + + This file is generated with [snarkJS](https://github.com/iden3/snarkjs). + + snarkJS is a free software: you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + snarkJS is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public + License for more details. + + You should have received a copy of the GNU General Public License + along with snarkJS. If not, see . +*/ + +pragma solidity >=0.7.0 <0.9.0; + +contract Groth16Verifier { + // Scalar field size + uint256 constant r = 21888242871839275222246405745257275088548364400416034343698204186575808495617; + // Base field size + uint256 constant q = 21888242871839275222246405745257275088696311157297823662689037894645226208583; + + // Verification Key data + uint256 constant alphax = 16428432848801857252194528405604668803277877773566238944394625302971855135431; + uint256 constant alphay = 16846502678714586896801519656441059708016666274385668027902869494772365009666; + uint256 constant betax1 = 3182164110458002340215786955198810119980427837186618912744689678939861918171; + uint256 constant betax2 = 16348171800823588416173124589066524623406261996681292662100840445103873053252; + uint256 constant betay1 = 4920802715848186258981584729175884379674325733638798907835771393452862684714; + uint256 constant betay2 = 19687132236965066906216944365591810874384658708175106803089633851114028275753; + uint256 constant gammax1 = 11559732032986387107991004021392285783925812861821192530917403151452391805634; + uint256 constant gammax2 = 10857046999023057135944570762232829481370756359578518086990519993285655852781; + uint256 constant gammay1 = 4082367875863433681332203403145435568316851327593401208105741076214120093531; + uint256 constant gammay2 = 8495653923123431417604973247489272438418190587263600148770280649306958101930; + uint256 constant deltax1 = 18786665442134809547367793008388252094276956707083189371748822844215202271178; + uint256 constant deltax2 = 17296777349791701671871010047490559682924748762983962242018229225890177681165; + uint256 constant deltay1 = 21546884238630900902634517213362010321565339505810557359182294051078510536811; + uint256 constant deltay2 = 7214627676570978956115414107903354102221009447018809863680303520130992055423; + + + uint256 constant IC0x = 1396989810128049774239906514097458055670219613079348950494410066757721605523; + uint256 constant IC0y = 20069629286434534534516684991063672335613842540347999544849171590987775766961; + + uint256 constant IC1x = 19282603452922066135228857769519044667044696173320493211119861249451600114594; + uint256 constant IC1y = 11966256187809052800087108088094647243345273965264062329687482664981607072161; + + + // Memory data + uint16 constant pVk = 0; + uint16 constant pPairing = 128; + + uint16 constant pLastMem = 896; + + function verifyProof(uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[1] calldata _pubSignals) public view returns (bool) { + assembly { + function checkField(v) { + if iszero(lt(v, r)) { + mstore(0, 0) + return(0, 0x20) + } + } + + // G1 function to multiply a G1 value(x,y) to value in an address + function g1_mulAccC(pR, x, y, s) { + let success + let mIn := mload(0x40) + mstore(mIn, x) + mstore(add(mIn, 32), y) + mstore(add(mIn, 64), s) + + success := staticcall(sub(gas(), 2000), 7, mIn, 96, mIn, 64) + + if iszero(success) { + mstore(0, 0) + return(0, 0x20) + } + + mstore(add(mIn, 64), mload(pR)) + mstore(add(mIn, 96), mload(add(pR, 32))) + + success := staticcall(sub(gas(), 2000), 6, mIn, 128, pR, 64) + + if iszero(success) { + mstore(0, 0) + return(0, 0x20) + } + } + + function checkPairing(pA, pB, pC, pubSignals, pMem) -> isOk { + let _pPairing := add(pMem, pPairing) + let _pVk := add(pMem, pVk) + + mstore(_pVk, IC0x) + mstore(add(_pVk, 32), IC0y) + + // Compute the linear combination vk_x + + g1_mulAccC(_pVk, IC1x, IC1y, calldataload(add(pubSignals, 0))) + + + // -A + mstore(_pPairing, calldataload(pA)) + mstore(add(_pPairing, 32), mod(sub(q, calldataload(add(pA, 32))), q)) + + // B + mstore(add(_pPairing, 64), calldataload(pB)) + mstore(add(_pPairing, 96), calldataload(add(pB, 32))) + mstore(add(_pPairing, 128), calldataload(add(pB, 64))) + mstore(add(_pPairing, 160), calldataload(add(pB, 96))) + + // alpha1 + mstore(add(_pPairing, 192), alphax) + mstore(add(_pPairing, 224), alphay) + + // beta2 + mstore(add(_pPairing, 256), betax1) + mstore(add(_pPairing, 288), betax2) + mstore(add(_pPairing, 320), betay1) + mstore(add(_pPairing, 352), betay2) + + // vk_x + mstore(add(_pPairing, 384), mload(add(pMem, pVk))) + mstore(add(_pPairing, 416), mload(add(pMem, add(pVk, 32)))) + + + // gamma2 + mstore(add(_pPairing, 448), gammax1) + mstore(add(_pPairing, 480), gammax2) + mstore(add(_pPairing, 512), gammay1) + mstore(add(_pPairing, 544), gammay2) + + // C + mstore(add(_pPairing, 576), calldataload(pC)) + mstore(add(_pPairing, 608), calldataload(add(pC, 32))) + + // delta2 + mstore(add(_pPairing, 640), deltax1) + mstore(add(_pPairing, 672), deltax2) + mstore(add(_pPairing, 704), deltay1) + mstore(add(_pPairing, 736), deltay2) + + + let success := staticcall(sub(gas(), 2000), 8, _pPairing, 768, _pPairing, 0x20) + + isOk := and(success, mload(_pPairing)) + } + + let pMem := mload(0x40) + mstore(0x40, add(pMem, pLastMem)) + + // Validate that all evaluations ∈ F + + checkField(calldataload(add(_pubSignals, 0))) + + + // Validate all evaluations + let isValid := checkPairing(_pA, _pB, _pC, _pubSignals, pMem) + + mstore(0, isValid) + return(0, 0x20) + } + } +} \ No newline at end of file diff --git a/contracts/src/config/VerifierConfig.sol b/contracts/src/config/VerifierConfig.sol new file mode 100644 index 000000000..9bc0e85f4 --- /dev/null +++ b/contracts/src/config/VerifierConfig.sol @@ -0,0 +1,172 @@ +// Copyright 2025 Boundless Foundation, Inc. +// +// Use of this source code is governed by the Business Source License +// as found in the LICENSE-BSL file. +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.20; + +import {Vm} from "forge-std/Vm.sol"; +import {console2} from "forge-std/console2.sol"; +import {stdToml} from "forge-std/StdToml.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + +/// Deployment of a single verifier. +/// +/// Many verifiers may be part of a deployment, with the router serving the purpose of making them +/// all accessible at a single address. +struct VerifierDeployment { + string name; + string version; + bytes4 selector; + address verifier; + address estop; + /// Specifies that this verifier is not deployed to the verifier router. + /// Default is false since most of the verifiers in the config are intended to be routable. + bool unroutable; + /// Flag set when the verifier has had its estop activated. Once activated, + /// the estop verifier is permanently inoperable. + bool stopped; +} + +/// Deployment of the verifier contracts on a particular chain. +/// +/// The deployment.toml file contains a number of deployments. Each is indexed by a "chain key", +/// such as "ethereum-mainnet". This struct represents the values in one of those deployments. +struct Deployment { + /// A friendly name for the network, such as "Ethereum Mainnet". + string name; + /// Chain ID for the network. + uint256 chainId; + /// Admin address for emergency stop contracts on this network, as well as the proposer for the + /// timelock controller that acts as the admin for the router. + address admin; + /// Address of the verifier router in this deployment. + address router; + /// Address of the parent verifier router in this deployment. + address parentRouter; + /// Address of the timelock control in this deployment, which is set as the admin of the router. + address timelockController; + /// Min delay configured on the timelock controller. + uint256 timelockDelay; + /// Deployed verifier implementations. + VerifierDeployment[] verifiers; +} + +library DeploymentLib { + /// Copy the deployment from memory to storage. + /// Solidity does not allow this to be done via the assignment operator. + function copyTo(Deployment memory mem, Deployment storage stor) internal { + stor.name = mem.name; + stor.chainId = mem.chainId; + stor.admin = mem.admin; + stor.router = mem.router; + stor.parentRouter = mem.parentRouter; + stor.timelockController = mem.timelockController; + stor.timelockDelay = mem.timelockDelay; + delete stor.verifiers; + for (uint256 i = 0; i < mem.verifiers.length; i++) { + stor.verifiers.push(mem.verifiers[i]); + } + } +} + +/// @notice Loader for the deployment config from a given deployment.toml file. +/// @dev This library uses Forge cheat code and can only be used in Forge script or test environments. +library ConfigLoader { + /// Reference the vm address without needing to inherit from Script. + Vm private constant VM = Vm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + + /// Given the contents of the deployment.toml file, determine the active chain key. + /// This function first checks the "CHAIN_KEY" environment variable and uses the value if set. + /// If not set, this function looks for a deployment in the given TOML with a matching chainId + /// field and returns the first matching result. + function determineChainKey(string memory configToml) internal view returns (string memory) { + // Get the config profile from the environment variable, or leave it empty + string memory chainKey = VM.envOr("CHAIN_KEY", string("")); + + if (bytes(chainKey).length != 0) { + console2.log("Using chain key %s set via environment variable", chainKey); + } else { + // Since no chain key is set, select the default one based on the chainId + console2.log("Determining chain key from chain ID %d", block.chainid); + string[] memory chainKeys = VM.parseTomlKeys(configToml, ".chains"); + for (uint256 i = 0; i < chainKeys.length; i++) { + if (stdToml.readUint(configToml, string.concat(".chains.", chainKeys[i], ".id")) == block.chainid) { + chainKey = chainKeys[i]; + console2.log("Using chain key %s from the config for chain ID %d", chainKey, block.chainid); + break; + } + } + } + require(bytes(chainKey).length != 0, "failed to determine the chain key in config TOML"); + + // Double check that there chain-key and connected chain ID match. TODO: Is this too restrictive? + uint256 chainId = stdToml.readUint(configToml, string.concat(".chains.", chainKey, ".id")); + require( + chainId == block.chainid, "chosen chain key is associated with chain ID that does not match connected chain" + ); + + return chainKey; + } + + function loadDeploymentConfig(string memory configFilePath) internal view returns (Deployment memory) { + string memory configToml = VM.readFile(configFilePath); + string memory chainKey = determineChainKey(configToml); + return ConfigParser.parseConfig(configToml, chainKey); + } +} + +/// @notice Parser for the deployment config given a TOML string. +/// @dev This library uses Forge cheat code and can only be used in Forge script or test environments. +library ConfigParser { + using SafeCast for uint256; + + /// Reference the vm address without needing to inherit from Script. + Vm private constant VM = Vm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + + function parseConfig(string memory config, string memory chainKey) internal view returns (Deployment memory) { + string memory chain = string.concat(".chains.", chainKey); + + Deployment memory deploymentConfig; + deploymentConfig.name = stdToml.readString(config, string.concat(chain, ".name")); + deploymentConfig.chainId = stdToml.readUint(config, string.concat(chain, ".id")); + deploymentConfig.admin = stdToml.readAddressOr(config, string.concat(chain, ".admin"), address(0)); + deploymentConfig.router = stdToml.readAddressOr(config, string.concat(chain, ".router"), address(0)); + deploymentConfig.parentRouter = stdToml.readAddress(config, string.concat(chain, ".parent-router")); + deploymentConfig.timelockController = + stdToml.readAddressOr(config, string.concat(chain, ".timelock-controller"), address(0)); + deploymentConfig.timelockDelay = stdToml.readUint(config, string.concat(chain, ".timelock-delay")); + + // Iterate over the verifier struct entries to get the length; + // NOTE: We do this because Solidity doesn't support dynamic arrays in memory :| + uint256 verifiersLength = 0; + string memory verifierKey = string.concat(chain, ".verifiers[", VM.toString(verifiersLength), "]"); + while (stdToml.keyExists(config, verifierKey)) { + verifiersLength++; + verifierKey = string.concat(chain, ".verifiers[", VM.toString(verifiersLength), "]"); + } + deploymentConfig.verifiers = new VerifierDeployment[](verifiersLength); + + // Iterate over the verifier struct entries and parse them. + uint256 verifierIndex = 0; + verifierKey = string.concat(chain, ".verifiers[", VM.toString(verifierIndex), "]"); + while (stdToml.keyExists(config, verifierKey)) { + VerifierDeployment memory verifier; + verifier.name = stdToml.readStringOr(config, string.concat(verifierKey, ".name"), ""); + verifier.version = stdToml.readStringOr(config, string.concat(verifierKey, ".version"), ""); + verifier.selector = bytes4(stdToml.readUint(config, string.concat(verifierKey, ".selector")).toUint32()); + verifier.verifier = stdToml.readAddress(config, string.concat(verifierKey, ".verifier")); + verifier.estop = stdToml.readAddress(config, string.concat(verifierKey, ".estop")); + verifier.unroutable = stdToml.readBoolOr(config, string.concat(verifierKey, ".unroutable"), false); + verifier.stopped = stdToml.readBoolOr(config, string.concat(verifierKey, ".stopped"), false); + + deploymentConfig.verifiers[verifierIndex] = verifier; + + verifierIndex++; + verifierKey = string.concat(chain, ".verifiers[", VM.toString(verifierIndex), "]"); + } + + return deploymentConfig; + } +} diff --git a/contracts/src/povw/PovwMint.sol b/contracts/src/povw/PovwMint.sol index f44564b02..3040d47f5 100644 --- a/contracts/src/povw/PovwMint.sol +++ b/contracts/src/povw/PovwMint.sol @@ -92,15 +92,16 @@ contract PovwMint is IPovwMint, Initializable, OwnableUpgradeable, UUPSUpgradeab } if (journal.povwAccountingAddress != address(ACCOUNTING)) { revert IncorrectSteelContractAddress({ - expected: address(ACCOUNTING), - received: journal.povwAccountingAddress + expected: address(ACCOUNTING), received: journal.povwAccountingAddress }); } if (journal.zkcAddress != address(TOKEN)) { revert IncorrectSteelContractAddress({expected: address(TOKEN), received: journal.zkcAddress}); } if (journal.zkcRewardsAddress != address(TOKEN_REWARDS)) { - revert IncorrectSteelContractAddress({expected: address(TOKEN_REWARDS), received: journal.zkcRewardsAddress}); + revert IncorrectSteelContractAddress({ + expected: address(TOKEN_REWARDS), received: journal.zkcRewardsAddress + }); } // Ensure the initial commit for each update is correct and update the final commit. diff --git a/contracts/src/types/Account.sol b/contracts/src/types/Account.sol index 9005d5e25..9ff69b3b4 100644 --- a/contracts/src/types/Account.sol +++ b/contracts/src/types/Account.sol @@ -37,10 +37,10 @@ library AccountLibrary { // forge-lint: disable-next-item(incorrect-shift) function requestFlags(Account storage self, uint32 idx) internal view returns (bool locked, bool fulfilled) { if (idx < REQUEST_FLAGS_INITIAL_BITS / REQUEST_FLAGS_BITWIDTH) { - uint64 masked = ( - self.requestFlagsInitial - & (uint64((1 << REQUEST_FLAGS_BITWIDTH) - 1) << uint64(idx * REQUEST_FLAGS_BITWIDTH)) - ) >> (idx * REQUEST_FLAGS_BITWIDTH); + uint64 masked = + (self.requestFlagsInitial + & (uint64((1 << REQUEST_FLAGS_BITWIDTH) - 1) << uint64(idx * REQUEST_FLAGS_BITWIDTH))) + >> (idx * REQUEST_FLAGS_BITWIDTH); return (masked & uint64(1) != 0, masked & uint64(2) != 0); } else { uint256 idxShifted = idx - (REQUEST_FLAGS_INITIAL_BITS / REQUEST_FLAGS_BITWIDTH); diff --git a/contracts/src/types/FulfillmentContext.sol b/contracts/src/types/FulfillmentContext.sol index f6a06cafd..727c2d6a5 100644 --- a/contracts/src/types/FulfillmentContext.sol +++ b/contracts/src/types/FulfillmentContext.sol @@ -38,9 +38,7 @@ library FulfillmentContextLibrary { /// @return The unpacked FulfillmentContext struct function unpack(uint256 packed) internal pure returns (FulfillmentContext memory) { return FulfillmentContext({ - valid: (packed & VALID_MASK) != 0, - expired: (packed & EXPIRED_MASK) != 0, - price: uint96(packed & PRICE_MASK) + valid: (packed & VALID_MASK) != 0, expired: (packed & EXPIRED_MASK) != 0, price: uint96(packed & PRICE_MASK) }); } diff --git a/contracts/src/types/Predicate.sol b/contracts/src/types/Predicate.sol index efe25f20f..407019d10 100644 --- a/contracts/src/types/Predicate.sol +++ b/contracts/src/types/Predicate.sol @@ -42,11 +42,7 @@ library PredicateLibrary { /// @notice Creates a prefix match predicate. /// @param prefix The prefix to match. /// @return A Predicate struct with type PrefixMatch and the provided prefix. - function createPrefixMatchPredicate(bytes32 imageId, bytes memory prefix) - internal - pure - returns (Predicate memory) - { + function createPrefixMatchPredicate(bytes32 imageId, bytes memory prefix) internal pure returns (Predicate memory) { return Predicate({predicateType: PredicateType.PrefixMatch, data: abi.encodePacked(imageId, prefix)}); } diff --git a/contracts/src/verifier/RiscZeroVerifierRouter.sol b/contracts/src/verifier/RiscZeroVerifierRouter.sol new file mode 100644 index 000000000..0e9f9c84b --- /dev/null +++ b/contracts/src/verifier/RiscZeroVerifierRouter.sol @@ -0,0 +1,105 @@ +// Copyright 2025 RISC Zero, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.9; + +import {Ownable, Ownable2Step} from "openzeppelin/contracts/access/Ownable2Step.sol"; + +import {IRiscZeroVerifier, Receipt} from "risc0/IRiscZeroVerifier.sol"; + +/// @notice Router for IRiscZeroVerifier, allowing multiple implementations to be accessible behind a single address. +contract RiscZeroVerifierRouter is IRiscZeroVerifier, Ownable2Step { + /// @notice Mapping from 4-byte verifier selector to verifier contracts. + /// Used to route receipts to verifiers that are able to check the receipt. + mapping(bytes4 => IRiscZeroVerifier) public verifiers; + + /// @notice Value of an entry that has never been set. + IRiscZeroVerifier internal constant UNSET = IRiscZeroVerifier(address(0)); + /// @notice A "tombstone" value used to mark verifier entries that have been removed from the mapping. + IRiscZeroVerifier internal constant TOMBSTONE = IRiscZeroVerifier(address(1)); + + /// @notice Error raised when attempting to verify a receipt with a selector that is not + /// registered on this router. Generally, this indicates a version mismatch where the + /// prover generated a receipt with version of the zkVM that does not match any + /// registered version on this router contract. + error SelectorUnknown(bytes4 selector); + /// @notice Error raised when attempting to add a verifier for a selector that is already registered. + error SelectorInUse(bytes4 selector); + /// @notice Error raised when attempting to verify a receipt with a selector that has been + /// removed, or attempting to add a new verifier with a selector that was previously + /// registered and then removed. + error SelectorRemoved(bytes4 selector); + /// @notice Error raised when attempting to add a verifier with a zero address. + error VerifierAddressZero(); + + constructor(address admin) Ownable(admin) {} + + /// @notice Adds a verifier to the router, such that it can receive receipt verification calls. + function addVerifier(bytes4 selector, IRiscZeroVerifier verifier) external virtual onlyOwner { + if (verifiers[selector] == TOMBSTONE) { + revert SelectorRemoved({selector: selector}); + } + if (verifiers[selector] != UNSET) { + revert SelectorInUse({selector: selector}); + } + if (address(verifier) == address(0)) { + revert VerifierAddressZero(); + } + verifiers[selector] = verifier; + } + + /// @notice Removes verifier from the router, such that it can not receive verification calls. + /// Removing a selector sets it to the tombstone value. It can never be set to any + /// other value, and can never be reused for a new verifier, in order to enforce the + /// property that each selector maps to at most one implementation across time. + function removeVerifier(bytes4 selector) external virtual onlyOwner { + // Simple check to reduce the chance of accidents. + // NOTE: If there ever _is_ a reason to remove a selector that has never been set, the owner + // can call addVerifier with the tombstone address. + if (verifiers[selector] == UNSET) { + revert SelectorUnknown({selector: selector}); + } + verifiers[selector] = TOMBSTONE; + } + + /// @notice Get the associated verifier, reverting if the selector is unknown or removed. + function getVerifier(bytes4 selector) public view virtual returns (IRiscZeroVerifier) { + IRiscZeroVerifier verifier = verifiers[selector]; + if (verifier == UNSET) { + revert SelectorUnknown({selector: selector}); + } + if (verifier == TOMBSTONE) { + revert SelectorRemoved({selector: selector}); + } + return verifier; + } + + /// @notice Get the associated verifier, reverting if the selector is unknown or removed. + function getVerifier(bytes calldata seal) public view returns (IRiscZeroVerifier) { + // Use the first 4 bytes of the seal at the selector to look up in the mapping. + return getVerifier(bytes4(seal[0:4])); + } + + /// @inheritdoc IRiscZeroVerifier + function verify(bytes calldata seal, bytes32 imageId, bytes32 journalDigest) external view virtual { + getVerifier(seal).verify(seal, imageId, journalDigest); + } + + /// @inheritdoc IRiscZeroVerifier + function verifyIntegrity(Receipt calldata receipt) external view virtual { + getVerifier(receipt.seal).verifyIntegrity(receipt); + } +} diff --git a/contracts/src/verifier/VerifierLayeredRouter.sol b/contracts/src/verifier/VerifierLayeredRouter.sol new file mode 100644 index 000000000..18b5d59df --- /dev/null +++ b/contracts/src/verifier/VerifierLayeredRouter.sol @@ -0,0 +1,103 @@ +// Copyright 2025 Boundless Foundation, Inc. +// +// Use of this source code is governed by the Business Source License +// as found in the LICENSE-BSL file. +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.9; + +import {IRiscZeroVerifier, Receipt} from "risc0/IRiscZeroVerifier.sol"; +import {RiscZeroVerifierRouter} from "./RiscZeroVerifierRouter.sol"; + +/// @notice A layered router enabling additional verifier implementations to be registered on top of a +/// parent router, while delegating unknown selectors to the parent. +/// @dev Resolution checks this router first and falls back to the parent router when unset. +contract VerifierLayeredRouter is RiscZeroVerifierRouter { + /// @notice The parent RISC Zero verifier router used as fallback. + RiscZeroVerifierRouter public immutable parentRouter; + + constructor(address owner, RiscZeroVerifierRouter _parentRouter) RiscZeroVerifierRouter(owner) { + require(address(_parentRouter) != address(0), "Parent router address cannot be zero"); + parentRouter = _parentRouter; + } + + /// @notice Gets the parent RISC Zero verifier router. + function getParentRouter() external view returns (RiscZeroVerifierRouter) { + return parentRouter; + } + + /// @notice Adds a verifier to the router, such that it can receive receipt verification calls. + /// @dev Ensures that the selector is not already registered or removed in either this router or the parent router. + function addVerifier(bytes4 selector, IRiscZeroVerifier verifier) external override onlyOwner { + // Ensure the selector is not removed from the parent router. + if (parentRouter.verifiers(selector) == TOMBSTONE) { + revert SelectorRemoved({selector: selector}); + } + // Ensure the selector is not already in use in the parent router. + if (parentRouter.verifiers(selector) != UNSET) { + revert SelectorInUse({selector: selector}); + } + // Ensure the selector is not removed from this router. + if (verifiers[selector] == TOMBSTONE) { + revert SelectorRemoved({selector: selector}); + } + // Ensure the selector is not already in use in this router. + if (verifiers[selector] != UNSET) { + revert SelectorInUse({selector: selector}); + } + // Ensure the verifier address is not zero. + if (address(verifier) == address(0)) { + revert VerifierAddressZero(); + } + verifiers[selector] = verifier; + } + + /// @inheritdoc RiscZeroVerifierRouter + function removeVerifier(bytes4 selector) external override onlyOwner { + verifiers[selector] = TOMBSTONE; + } + + /// @notice Get the associated verifier, falling back to the parent router if unset. + function getVerifier(bytes4 selector) public view override returns (IRiscZeroVerifier) { + IRiscZeroVerifier verifier = verifiers[selector]; + // If the verifier is unset, fall back to the parent router. + if (verifier == UNSET) { + return parentRouter.getVerifier(selector); + } + if (verifier == TOMBSTONE) { + revert SelectorRemoved({selector: selector}); + } + return verifier; + } + + /// @inheritdoc IRiscZeroVerifier + function verify(bytes calldata seal, bytes32 imageId, bytes32 journalDigest) external view override { + bytes4 selector = bytes4(seal[0:4]); + IRiscZeroVerifier v = verifiers[selector]; + + if (v == UNSET) { + // Single external call to parent (it resolves + forwards) + parentRouter.verify(seal, imageId, journalDigest); + return; + } + if (v == TOMBSTONE) { + revert SelectorRemoved({selector: selector}); + } + v.verify(seal, imageId, journalDigest); + } + + /// @inheritdoc IRiscZeroVerifier + function verifyIntegrity(Receipt calldata receipt) external view override { + bytes4 selector = bytes4(receipt.seal[0:4]); + IRiscZeroVerifier v = verifiers[selector]; + + if (v == UNSET) { + parentRouter.verifyIntegrity(receipt); + return; + } + if (v == TOMBSTONE) { + revert SelectorRemoved({selector: selector}); + } + v.verifyIntegrity(receipt); + } +} diff --git a/contracts/test/Blake3Groth16Verifier.t.sol b/contracts/test/Blake3Groth16Verifier.t.sol new file mode 100644 index 000000000..f3ac23b5a --- /dev/null +++ b/contracts/test/Blake3Groth16Verifier.t.sol @@ -0,0 +1,70 @@ +// Copyright 2025 Boundless Foundation, Inc. +// +// Use of this source code is governed by the Business Source License +// as found in the LICENSE-BSL file. +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.13; + +import {Test} from "forge-std/Test.sol"; + +import { + Output, + OutputLib, + + // Receipt needs to be renamed due to collision with type on the Test contract. + Receipt as RiscZeroReceipt, + ReceiptClaim, + ReceiptClaimLib, + SystemState, + SystemStateLib, + VerificationFailed +} from "risc0/IRiscZeroVerifier.sol"; +import {ControlID} from "../src/blake3-groth16/ControlID.sol"; +import {Blake3Groth16Verifier} from "../src/blake3-groth16/Blake3Groth16Verifier.sol"; +import {TestReceipt} from "./receipts/Blake3Groth16TestReceipt.sol"; + +contract Blake3Groth16VerifierTest is Test { + using OutputLib for Output; + using ReceiptClaimLib for ReceiptClaim; + using SystemStateLib for SystemState; + + RiscZeroReceipt internal receipt = RiscZeroReceipt(TestReceipt.SEAL, TestReceipt.CLAIM_DIGEST); + + Blake3Groth16Verifier internal verifier; + + function setUp() external { + verifier = new Blake3Groth16Verifier(ControlID.CONTROL_ROOT, ControlID.BN254_CONTROL_ID); + } + + function testConsistentSystemStateZeroDigest() external pure { + require( + ReceiptClaimLib.SYSTEM_STATE_ZERO_DIGEST + == sha256( + abi.encodePacked( + SystemStateLib.TAG_DIGEST, + // down + bytes32(0), + // data + uint32(0), + // down.length + uint16(1) << 8 + ) + ) + ); + } + + function testVerifyKnownGoodReceipt() external view { + verifier.verifyIntegrity(receipt); + } + + function expectVerificationFailure(bytes memory seal, ReceiptClaim memory claim) internal { + bytes32 claimDigest = claim.digest(); + vm.expectRevert(VerificationFailed.selector); + verifier.verifyIntegrity(RiscZeroReceipt(seal, claimDigest)); + } + + function testSelectorIsStable() external view { + require(verifier.SELECTOR() == hex"62f049f6"); + } +} diff --git a/contracts/test/BoundlessMarket.t.sol b/contracts/test/BoundlessMarket.t.sol index bca3fc0d5..f16b608e0 100644 --- a/contracts/test/BoundlessMarket.t.sol +++ b/contracts/test/BoundlessMarket.t.sol @@ -346,7 +346,8 @@ contract BoundlessMarketTest is Test { root, verifier.mockProve( SET_BUILDER_IMAGE_ID, sha256(abi.encodePacked(SET_BUILDER_IMAGE_ID, uint256(1 << 255), root)) - ).seal + ) + .seal ); } @@ -507,8 +508,9 @@ contract BoundlessMarketTest is Test { view returns (Fulfillment[] memory fills, AssessorReceipt memory assessorReceipt, bytes32 root) { - (fills, assessorReceipt, root) = - createFills(requests, journals, prover, FulfillmentDataType.ImageIdAndJournal, DEPRECATED_ASSESSOR_IMAGE_ID); + (fills, assessorReceipt, root) = createFills( + requests, journals, prover, FulfillmentDataType.ImageIdAndJournal, DEPRECATED_ASSESSOR_IMAGE_ID + ); } function newBatch(uint256 batchSize) internal returns (ProofRequest[] memory requests, bytes[] memory journals) { @@ -1408,9 +1410,11 @@ contract BoundlessMarketBasicTest is BoundlessMarketTest { (Fulfillment[] memory fills, AssessorReceipt memory assessorReceipt, bytes32 root) = createFills(requests, journals, testProverAddress); - bytes memory seal = verifier.mockProve( + bytes memory seal = + verifier.mockProve( SET_BUILDER_IMAGE_ID, sha256(abi.encodePacked(SET_BUILDER_IMAGE_ID, uint256(1 << 255), root)) - ).seal; + ) + .seal; if (lockinMethod == LockRequestMethod.None) { // Annoying boilerplate for creating singleton lists. @@ -1480,9 +1484,11 @@ contract BoundlessMarketBasicTest is BoundlessMarketTest { (Fulfillment[] memory fills, AssessorReceipt memory assessorReceipt, bytes32 root) = createFills(requests, journals, testProverAddress); - bytes memory seal = verifier.mockProve( + bytes memory seal = + verifier.mockProve( SET_BUILDER_IMAGE_ID, sha256(abi.encodePacked(SET_BUILDER_IMAGE_ID, uint256(1 << 255), root)) - ).seal; + ) + .seal; uint256 initialBalance = boundlessMarket.balanceOf(testProverAddress) + testProverAddress.balance; @@ -1634,9 +1640,9 @@ contract BoundlessMarketBasicTest is BoundlessMarketTest { fills[0] = fill; vm.expectEmit(true, true, true, true); - emit IBoundlessMarket.PaymentRequirementsFailed( - abi.encodeWithSelector(IBoundlessMarket.RequestIsLocked.selector, request.id) - ); + emit IBoundlessMarket.PaymentRequirementsFailed(abi.encodeWithSelector( + IBoundlessMarket.RequestIsLocked.selector, request.id + )); boundlessMarket.fulfill(fills, assessorReceipt); vm.snapshotGasLastCall("fulfill: another prover fulfills without payment"); @@ -2294,9 +2300,9 @@ contract BoundlessMarketBasicTest is BoundlessMarketTest { boundlessMarket.priceAndFulfill(requests, clientSignatures, fills, assessorReceipt); vm.expectEmit(true, true, true, true); - emit IBoundlessMarket.PaymentRequirementsFailed( - abi.encodeWithSelector(IBoundlessMarket.RequestIsFulfilled.selector, request.id) - ); + emit IBoundlessMarket.PaymentRequirementsFailed(abi.encodeWithSelector( + IBoundlessMarket.RequestIsFulfilled.selector, request.id + )); boundlessMarket.priceAndFulfill(requests, clientSignatures, fills, assessorReceipt); vm.snapshotGasLastCall("priceAndFulfill: fulfill already fulfilled was locked request"); @@ -2343,9 +2349,9 @@ contract BoundlessMarketBasicTest is BoundlessMarketTest { // But its already been fulfilled by the other prover. vm.expectEmit(true, true, true, true); - emit IBoundlessMarket.PaymentRequirementsFailed( - abi.encodeWithSelector(IBoundlessMarket.RequestIsFulfilled.selector, request.id) - ); + emit IBoundlessMarket.PaymentRequirementsFailed(abi.encodeWithSelector( + IBoundlessMarket.RequestIsFulfilled.selector, request.id + )); // The proof should still be delivered. vm.expectEmit(true, true, true, false); @@ -2396,9 +2402,9 @@ contract BoundlessMarketBasicTest is BoundlessMarketTest { // In this case the request has fully expired, so the proof should NOT be delivered, // however we should not revert (as this allows partial fulfillment of other requests in the batch) vm.expectEmit(true, true, true, false); - emit IBoundlessMarket.PaymentRequirementsFailed( - abi.encodeWithSelector(IBoundlessMarket.RequestIsExpired.selector, request.id) - ); + emit IBoundlessMarket.PaymentRequirementsFailed(abi.encodeWithSelector( + IBoundlessMarket.RequestIsExpired.selector, request.id + )); // The fulfillment should not revert, as we support multiple proofs being delivered for a single request. bytes[] memory paymentErrors = @@ -2586,9 +2592,9 @@ contract BoundlessMarketBasicTest is BoundlessMarketTest { // expect emit of payment requirement failed vm.expectEmit(true, true, true, true); - emit IBoundlessMarket.PaymentRequirementsFailed( - abi.encodeWithSelector(IBoundlessMarket.InsufficientBalance.selector, clientAddress) - ); + emit IBoundlessMarket.PaymentRequirementsFailed(abi.encodeWithSelector( + IBoundlessMarket.InsufficientBalance.selector, clientAddress + )); vm.prank(clientAddress); boundlessMarket.priceAndFulfill(requests, clientSignatures, fills, assessorReceipt); expectRequestFulfilled(fill.id); @@ -2967,9 +2973,11 @@ contract BoundlessMarketBasicTest is BoundlessMarketTest { (Fulfillment[] memory fills, AssessorReceipt memory assessorReceipt, bytes32 root) = createFills(requests, journals, testProverAddress); - bytes memory seal = verifier.mockProve( + bytes memory seal = + verifier.mockProve( SET_BUILDER_IMAGE_ID, sha256(abi.encodePacked(SET_BUILDER_IMAGE_ID, uint256(1 << 255), root)) - ).seal; + ) + .seal; bytes[] memory clientSignatures = new bytes[](1); clientSignatures[0] = client.sign(requests[0]); @@ -3222,9 +3230,11 @@ contract BoundlessMarketBasicTest is BoundlessMarketTest { (Fulfillment[] memory fills, AssessorReceipt memory assessorReceipt, bytes32 root) = createFills(requests, journals, testProverAddress); - bytes memory seal = verifier.mockProve( + bytes memory seal = + verifier.mockProve( SET_BUILDER_IMAGE_ID, sha256(abi.encodePacked(SET_BUILDER_IMAGE_ID, uint256(1 << 255), root)) - ).seal; + ) + .seal; boundlessMarket.submitRootAndFulfill(address(setVerifier), root, seal, fills, assessorReceipt); vm.snapshotGasLastCall("submitRootAndFulfill: a batch of 2 requests"); @@ -3528,9 +3538,9 @@ contract BoundlessMarketBasicTest is BoundlessMarketTest { // In this case the request has fully expired, so the proof should NOT be delivered, // however we should not revert (as this allows partial fulfillment of other requests in the batch) vm.expectEmit(true, true, true, false); - emit IBoundlessMarket.PaymentRequirementsFailed( - abi.encodeWithSelector(IBoundlessMarket.RequestIsExpired.selector, request.id) - ); + emit IBoundlessMarket.PaymentRequirementsFailed(abi.encodeWithSelector( + IBoundlessMarket.RequestIsExpired.selector, request.id + )); // The fulfillment should not revert, as we support multiple proofs being delivered for a single request. bytes[] memory paymentErrors = @@ -3799,9 +3809,9 @@ contract BoundlessMarketBasicTest is BoundlessMarketTest { vm.expectEmit(true, true, true, true); emit IBoundlessMarket.RequestFulfilled(request.id, otherProverAddress, fill.requestDigest); vm.expectEmit(true, true, true, true); - emit IBoundlessMarket.PaymentRequirementsFailed( - abi.encodeWithSelector(IBoundlessMarket.RequestIsLocked.selector, request.id) - ); + emit IBoundlessMarket.PaymentRequirementsFailed(abi.encodeWithSelector( + IBoundlessMarket.RequestIsLocked.selector, request.id + )); vm.expectEmit(true, true, true, false); emit IBoundlessMarket.ProofDelivered(request.id, otherProverAddress, fill); vm.expectEmit(true, true, true, true); @@ -3846,9 +3856,9 @@ contract BoundlessMarketBasicTest is BoundlessMarketTest { vm.expectEmit(true, true, true, true); emit IBoundlessMarket.RequestFulfilled(request.id, otherProverAddress, fill.requestDigest); vm.expectEmit(true, true, true, true); - emit IBoundlessMarket.PaymentRequirementsFailed( - abi.encodeWithSelector(IBoundlessMarket.RequestIsLocked.selector, request.id) - ); + emit IBoundlessMarket.PaymentRequirementsFailed(abi.encodeWithSelector( + IBoundlessMarket.RequestIsLocked.selector, request.id + )); vm.expectEmit(true, true, true, false); emit IBoundlessMarket.ProofDelivered(request.id, otherProverAddress, fill); vm.expectEmit(true, true, true, true); diff --git a/contracts/test/BoundlessMarketCallback.t.sol b/contracts/test/BoundlessMarketCallback.t.sol index 4ce08bc32..fdc635973 100644 --- a/contracts/test/BoundlessMarketCallback.t.sol +++ b/contracts/test/BoundlessMarketCallback.t.sol @@ -6,7 +6,10 @@ pragma solidity ^0.8.26; import {Test} from "forge-std/Test.sol"; import { - IRiscZeroVerifier, Receipt as RiscZeroReceipt, ReceiptClaim, ReceiptClaimLib + IRiscZeroVerifier, + Receipt as RiscZeroReceipt, + ReceiptClaim, + ReceiptClaimLib } from "risc0/IRiscZeroVerifier.sol"; import {BoundlessMarketCallback} from "../src/BoundlessMarketCallback.sol"; diff --git a/contracts/test/TestUtils.sol b/contracts/test/TestUtils.sol index e9b9169ec..8c0ee7ae3 100644 --- a/contracts/test/TestUtils.sol +++ b/contracts/test/TestUtils.sol @@ -30,8 +30,8 @@ library TestUtils { for (uint256 i = 0; i < fills.length; i++) { leaves[i] = AssessorCommitment( - i, fills[i].id, fills[i].requestDigest, fills[i].claimDigest, fills[i].fulfillmentDataDigest() - ).eip712Digest(); + i, fills[i].id, fills[i].requestDigest, fills[i].claimDigest, fills[i].fulfillmentDataDigest() + ).eip712Digest(); } bytes32 root = MerkleProofish.processTree(leaves); diff --git a/contracts/test/VerifierLayeredRouter.t.sol b/contracts/test/VerifierLayeredRouter.t.sol new file mode 100644 index 000000000..890cb7f2d --- /dev/null +++ b/contracts/test/VerifierLayeredRouter.t.sol @@ -0,0 +1,403 @@ +// Copyright 2025 Boundless Foundation, Inc. +// +// Use of this source code is governed by the Business Source License +// as found in the LICENSE-BSL file. +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.13; + +import {Test} from "forge-std/Test.sol"; +import {console2} from "forge-std/console2.sol"; +import {Ownable} from "openzeppelin/contracts/access/Ownable.sol"; + +import { + IRiscZeroVerifier, + Output, + OutputLib, + + // Receipt needs to be renamed due to collision with type on the Test contract. + Receipt as RiscZeroReceipt, + ReceiptClaim, + ReceiptClaimLib, + ExitCode, + SystemExitCode, + VerificationFailed +} from "risc0/IRiscZeroVerifier.sol"; +import {RiscZeroMockVerifier} from "risc0/test/RiscZeroMockVerifier.sol"; +import {RiscZeroVerifierRouter} from "risc0/RiscZeroVerifierRouter.sol"; +import {RiscZeroVerifierRouter as BoundlessVerifierRouter} from "../src/verifier/RiscZeroVerifierRouter.sol"; +import {VerifierLayeredRouter} from "../src/verifier/VerifierLayeredRouter.sol"; + +library TestReceipt { + bytes public constant SEAL = + hex"7f3d01021e2cc73fcbc78acba09144eef4ee7a3bdeeacff3f50d801d6e62423a5ce863072df236b96ac4a8c91af0947d56e34560a7ba3a6fae79ca79bd70c30e2934cb842b72ad3493f70fd5b51a2a8eeead852563d8e8aae05afeeec8aa3ab719d3e42d0f6982e2de87cb6b2ccebab138c09c8a12674ae17d6b0ac0ffeee240af7ca39c00aab7f9aefb5d936bcb2d92c99a44548518130e1bcccdbccf84793862846c2422539985417029729b42c2254be325d915509c74269e4ad5bcc1c243a957a8c504b2162c32c72a3eb4a61becc8512f35603c0758bbbbd6b712efc49e6a3e68c52b42ef79a6f348875913f38da1e1dca85fc43b31097a2938a39480eec05700ea"; + bytes public constant JOURNAL = hex"6a75737420612073696d706c652072656365697074"; + bytes32 public constant IMAGE_ID = hex"be5ee8a820e3f10d48576fcefce4570c08f876b2d8a12a7dcd586b5901c7ab3d"; + bytes32 public constant USER_ID = hex"be5ee8a820e3f10d48576fcefce4570c08f876b2d8a12a7dcd586b5901c7ab3d"; +} + +contract RiscZeroVerifierLayeredRouterTest is Test { + using OutputLib for Output; + using ReceiptClaimLib for ReceiptClaim; + + bytes32 internal TEST_JOURNAL_DIGEST = sha256(TestReceipt.JOURNAL); + ReceiptClaim internal TEST_RECEIPT_CLAIM = ReceiptClaimLib.ok(TestReceipt.IMAGE_ID, TEST_JOURNAL_DIGEST); + RiscZeroReceipt internal TEST_RECEIPT_A; + RiscZeroReceipt internal TEST_RECEIPT_B; + RiscZeroReceipt internal TEST_MANGLED_RECEIPT_A; + RiscZeroReceipt internal TEST_MANGLED_RECEIPT_B; + bytes4 internal SELECTOR_A; + bytes4 internal SELECTOR_B; + + RiscZeroMockVerifier internal verifierMockA; + RiscZeroMockVerifier internal verifierMockB; + RiscZeroVerifierRouter internal parentRouter; + VerifierLayeredRouter internal layeredRouter; + + function setUp() external { + parentRouter = new RiscZeroVerifierRouter(address(this)); + layeredRouter = new VerifierLayeredRouter(address(this), BoundlessVerifierRouter(address(parentRouter))); + + verifierMockA = new RiscZeroMockVerifier(bytes4(0xFFFFFFFF)); + verifierMockB = new RiscZeroMockVerifier(bytes4(uint32(1))); + + TEST_RECEIPT_A = verifierMockA.mockProve(TEST_RECEIPT_CLAIM.digest()); + TEST_RECEIPT_B = verifierMockB.mockProve(TEST_RECEIPT_CLAIM.digest()); + + TEST_MANGLED_RECEIPT_A = TEST_RECEIPT_A; + TEST_MANGLED_RECEIPT_A.seal[4] ^= bytes1(uint8(1)); + TEST_MANGLED_RECEIPT_B = TEST_RECEIPT_B; + TEST_MANGLED_RECEIPT_B.seal[4] ^= bytes1(uint8(1)); + + SELECTOR_A = verifierMockA.SELECTOR(); + SELECTOR_B = verifierMockB.SELECTOR(); + } + + function test_LayeredRouterGet() external view { + assertEq(address(layeredRouter.getParentRouter()), address(parentRouter)); + } + + function test_AddSelectorExistsInParentRouter() external { + parentRouter.addVerifier(SELECTOR_A, verifierMockA); + + vm.expectRevert(abi.encodeWithSelector(RiscZeroVerifierRouter.SelectorInUse.selector, SELECTOR_A)); + layeredRouter.addVerifier(SELECTOR_A, verifierMockB); + } + + function test_AddRemovedSelectorInParentRouter() external { + parentRouter.addVerifier(SELECTOR_A, verifierMockA); + parentRouter.removeVerifier(SELECTOR_A); + + vm.expectRevert(abi.encodeWithSelector(RiscZeroVerifierRouter.SelectorRemoved.selector, SELECTOR_A)); + layeredRouter.addVerifier(SELECTOR_A, verifierMockB); + } + + function test_AddSelector() external { + layeredRouter.addVerifier(SELECTOR_A, verifierMockA); + IRiscZeroVerifier verifier = layeredRouter.getVerifier(SELECTOR_A); + assertEq(address(verifier), address(verifierMockA)); + } + + function test_LayeredRouterVerifyIntegrity() external { + parentRouter.addVerifier(SELECTOR_A, verifierMockA); + layeredRouter.addVerifier(SELECTOR_B, verifierMockB); + // Expect exactly 2 calls, to verifier A/B with TEST_RECEIPT_x and TEST_MANGLED_RECEIPT_x. + vm.expectCall(address(verifierMockA), new bytes(0), 2); + vm.expectCall(address(verifierMockA), abi.encodeCall(IRiscZeroVerifier.verifyIntegrity, TEST_RECEIPT_A), 1); + vm.expectCall( + address(verifierMockA), abi.encodeCall(IRiscZeroVerifier.verifyIntegrity, TEST_MANGLED_RECEIPT_A), 1 + ); + vm.expectCall(address(verifierMockB), new bytes(0), 2); + vm.expectCall(address(verifierMockB), abi.encodeCall(IRiscZeroVerifier.verifyIntegrity, TEST_RECEIPT_B), 1); + vm.expectCall( + address(verifierMockB), abi.encodeCall(IRiscZeroVerifier.verifyIntegrity, TEST_MANGLED_RECEIPT_B), 1 + ); + layeredRouter.verifyIntegrity(TEST_RECEIPT_A); + vm.expectRevert(VerificationFailed.selector); + layeredRouter.verifyIntegrity(TEST_MANGLED_RECEIPT_A); + + layeredRouter.verifyIntegrity(TEST_RECEIPT_B); + vm.expectRevert(VerificationFailed.selector); + layeredRouter.verifyIntegrity(TEST_MANGLED_RECEIPT_B); + } + + function test_EmptyRouterVerifyIntegrity() external { + // Expect no calls to be made to the verifier controlled. + vm.expectCall(address(verifierMockA), new bytes(0), 0); + vm.expectCall(address(verifierMockB), new bytes(0), 0); + + // Empty router should always revert with selector unknown. + vm.expectRevert(abi.encodeWithSelector(RiscZeroVerifierRouter.SelectorUnknown.selector, SELECTOR_A)); + layeredRouter.verifyIntegrity(TEST_RECEIPT_A); + + vm.expectRevert(abi.encodeWithSelector(RiscZeroVerifierRouter.SelectorUnknown.selector, SELECTOR_B)); + layeredRouter.verifyIntegrity(TEST_RECEIPT_B); + } + + function test_SingleVerifierVerifyIntegrity() external { + // Expect exactly 2 calls, to verifier A with TEST_RECEIPT_A and TEST_MANGLED_RECEIPT_A. + vm.expectCall(address(verifierMockA), new bytes(0), 2); + vm.expectCall(address(verifierMockA), abi.encodeCall(IRiscZeroVerifier.verifyIntegrity, TEST_RECEIPT_A), 1); + vm.expectCall( + address(verifierMockA), abi.encodeCall(IRiscZeroVerifier.verifyIntegrity, TEST_MANGLED_RECEIPT_A), 1 + ); + vm.expectCall(address(verifierMockB), new bytes(0), 0); + + layeredRouter.addVerifier(SELECTOR_A, verifierMockA); + + layeredRouter.verifyIntegrity(TEST_RECEIPT_A); + vm.expectRevert(VerificationFailed.selector); + layeredRouter.verifyIntegrity(TEST_MANGLED_RECEIPT_A); + + vm.expectRevert(abi.encodeWithSelector(RiscZeroVerifierRouter.SelectorUnknown.selector, SELECTOR_B)); + layeredRouter.verifyIntegrity(TEST_RECEIPT_B); + } + + function test_TwoVerifiersVerifyIntegrity() external { + // Expect exactly 2 calls, to verifier A/B with TEST_RECEIPT_x and TEST_MANGLED_RECEIPT_x. + vm.expectCall(address(verifierMockA), new bytes(0), 2); + vm.expectCall(address(verifierMockA), abi.encodeCall(IRiscZeroVerifier.verifyIntegrity, TEST_RECEIPT_A), 1); + vm.expectCall( + address(verifierMockA), abi.encodeCall(IRiscZeroVerifier.verifyIntegrity, TEST_MANGLED_RECEIPT_A), 1 + ); + vm.expectCall(address(verifierMockB), new bytes(0), 2); + vm.expectCall(address(verifierMockB), abi.encodeCall(IRiscZeroVerifier.verifyIntegrity, TEST_RECEIPT_B), 1); + vm.expectCall( + address(verifierMockB), abi.encodeCall(IRiscZeroVerifier.verifyIntegrity, TEST_MANGLED_RECEIPT_B), 1 + ); + + layeredRouter.addVerifier(SELECTOR_A, verifierMockA); + layeredRouter.addVerifier(SELECTOR_B, verifierMockB); + + layeredRouter.verifyIntegrity(TEST_RECEIPT_A); + vm.expectRevert(VerificationFailed.selector); + layeredRouter.verifyIntegrity(TEST_MANGLED_RECEIPT_A); + + layeredRouter.verifyIntegrity(TEST_RECEIPT_B); + vm.expectRevert(VerificationFailed.selector); + layeredRouter.verifyIntegrity(TEST_MANGLED_RECEIPT_B); + } + + function test_RemoveVerifierVerifyIntegrity() external { + // Expect exactly 4 calls to verifier A with TEST_RECEIPT_A and TEST_MANGLED_RECEIPT_A. + // Expect exactly 2 calls to verifier B with TEST_RECEIPT_B and TEST_MANGLED_RECEIPT_B. + vm.expectCall(address(verifierMockA), new bytes(0), 4); + vm.expectCall(address(verifierMockA), abi.encodeCall(IRiscZeroVerifier.verifyIntegrity, TEST_RECEIPT_A), 2); + vm.expectCall( + address(verifierMockA), abi.encodeCall(IRiscZeroVerifier.verifyIntegrity, TEST_MANGLED_RECEIPT_A), 2 + ); + vm.expectCall(address(verifierMockB), new bytes(0), 2); + vm.expectCall(address(verifierMockB), abi.encodeCall(IRiscZeroVerifier.verifyIntegrity, TEST_RECEIPT_B), 1); + vm.expectCall( + address(verifierMockB), abi.encodeCall(IRiscZeroVerifier.verifyIntegrity, TEST_MANGLED_RECEIPT_B), 1 + ); + + layeredRouter.addVerifier(SELECTOR_A, verifierMockA); + layeredRouter.addVerifier(SELECTOR_B, verifierMockB); + + layeredRouter.verifyIntegrity(TEST_RECEIPT_A); + vm.expectRevert(VerificationFailed.selector); + layeredRouter.verifyIntegrity(TEST_MANGLED_RECEIPT_A); + + layeredRouter.verifyIntegrity(TEST_RECEIPT_B); + vm.expectRevert(VerificationFailed.selector); + layeredRouter.verifyIntegrity(TEST_MANGLED_RECEIPT_B); + + layeredRouter.removeVerifier(SELECTOR_B); + + layeredRouter.verifyIntegrity(TEST_RECEIPT_A); + vm.expectRevert(VerificationFailed.selector); + layeredRouter.verifyIntegrity(TEST_MANGLED_RECEIPT_A); + + vm.expectRevert(abi.encodeWithSelector(RiscZeroVerifierRouter.SelectorRemoved.selector, SELECTOR_B)); + layeredRouter.verifyIntegrity(TEST_RECEIPT_B); + } + + function test_EmptyRouterVerify() external { + // Expect no calls to be made to the verifier controlled. + vm.expectCall(address(verifierMockA), new bytes(0), 0); + vm.expectCall(address(verifierMockB), new bytes(0), 0); + + // Empty router should always revert with selector unknown. + vm.expectRevert(abi.encodeWithSelector(RiscZeroVerifierRouter.SelectorUnknown.selector, SELECTOR_A)); + layeredRouter.verify(TEST_RECEIPT_A.seal, TestReceipt.IMAGE_ID, TEST_JOURNAL_DIGEST); + + vm.expectRevert(abi.encodeWithSelector(RiscZeroVerifierRouter.SelectorUnknown.selector, SELECTOR_B)); + layeredRouter.verify(TEST_RECEIPT_B.seal, TestReceipt.IMAGE_ID, TEST_JOURNAL_DIGEST); + } + + function test_SingleVerifierVerify() external { + // Expect exactly 2 calls, to verifier A with TEST_RECEIPT_A and TEST_MANGLED_RECEIPT_A. + vm.expectCall(address(verifierMockA), new bytes(0), 2); + vm.expectCall( + address(verifierMockA), + abi.encodeCall(IRiscZeroVerifier.verify, (TEST_RECEIPT_A.seal, TestReceipt.IMAGE_ID, TEST_JOURNAL_DIGEST)), + 1 + ); + vm.expectCall( + address(verifierMockA), + abi.encodeCall( + IRiscZeroVerifier.verify, (TEST_MANGLED_RECEIPT_A.seal, TestReceipt.IMAGE_ID, TEST_JOURNAL_DIGEST) + ), + 1 + ); + vm.expectCall(address(verifierMockB), new bytes(0), 0); + + layeredRouter.addVerifier(SELECTOR_A, verifierMockA); + + layeredRouter.verify(TEST_RECEIPT_A.seal, TestReceipt.IMAGE_ID, TEST_JOURNAL_DIGEST); + vm.expectRevert(VerificationFailed.selector); + layeredRouter.verify(TEST_MANGLED_RECEIPT_A.seal, TestReceipt.IMAGE_ID, TEST_JOURNAL_DIGEST); + + vm.expectRevert(abi.encodeWithSelector(RiscZeroVerifierRouter.SelectorUnknown.selector, SELECTOR_B)); + layeredRouter.verify(TEST_RECEIPT_B.seal, TestReceipt.IMAGE_ID, TEST_JOURNAL_DIGEST); + } + + function test_TwoVerifiersVerify() external { + // Expect exactly 2 calls, to verifier A/B with TEST_RECEIPT_x and TEST_MANGLED_RECEIPT_x. + vm.expectCall(address(verifierMockA), new bytes(0), 2); + vm.expectCall( + address(verifierMockA), + abi.encodeCall(IRiscZeroVerifier.verify, (TEST_RECEIPT_A.seal, TestReceipt.IMAGE_ID, TEST_JOURNAL_DIGEST)), + 1 + ); + vm.expectCall( + address(verifierMockA), + abi.encodeCall( + IRiscZeroVerifier.verify, (TEST_MANGLED_RECEIPT_A.seal, TestReceipt.IMAGE_ID, TEST_JOURNAL_DIGEST) + ), + 1 + ); + vm.expectCall(address(verifierMockB), new bytes(0), 2); + vm.expectCall( + address(verifierMockB), + abi.encodeCall(IRiscZeroVerifier.verify, (TEST_RECEIPT_B.seal, TestReceipt.IMAGE_ID, TEST_JOURNAL_DIGEST)), + 1 + ); + vm.expectCall( + address(verifierMockB), + abi.encodeCall( + IRiscZeroVerifier.verify, (TEST_MANGLED_RECEIPT_B.seal, TestReceipt.IMAGE_ID, TEST_JOURNAL_DIGEST) + ), + 1 + ); + + layeredRouter.addVerifier(SELECTOR_A, verifierMockA); + layeredRouter.addVerifier(SELECTOR_B, verifierMockB); + + layeredRouter.verify(TEST_RECEIPT_A.seal, TestReceipt.IMAGE_ID, TEST_JOURNAL_DIGEST); + vm.expectRevert(VerificationFailed.selector); + layeredRouter.verify(TEST_MANGLED_RECEIPT_A.seal, TestReceipt.IMAGE_ID, TEST_JOURNAL_DIGEST); + + layeredRouter.verify(TEST_RECEIPT_B.seal, TestReceipt.IMAGE_ID, TEST_JOURNAL_DIGEST); + vm.expectRevert(VerificationFailed.selector); + layeredRouter.verify(TEST_MANGLED_RECEIPT_B.seal, TestReceipt.IMAGE_ID, TEST_JOURNAL_DIGEST); + } + + function test_RemoveVerifierVerify() external { + // Expect exactly 4 calls to verifier A with TEST_RECEIPT_A and TEST_MANGLED_RECEIPT_A. + // Expect exactly 2 calls to verifier B with TEST_RECEIPT_B and TEST_MANGLED_RECEIPT_B. + vm.expectCall(address(verifierMockA), new bytes(0), 4); + vm.expectCall( + address(verifierMockA), + abi.encodeCall(IRiscZeroVerifier.verify, (TEST_RECEIPT_A.seal, TestReceipt.IMAGE_ID, TEST_JOURNAL_DIGEST)), + 2 + ); + vm.expectCall( + address(verifierMockA), + abi.encodeCall( + IRiscZeroVerifier.verify, (TEST_MANGLED_RECEIPT_A.seal, TestReceipt.IMAGE_ID, TEST_JOURNAL_DIGEST) + ), + 2 + ); + vm.expectCall(address(verifierMockB), new bytes(0), 2); + vm.expectCall( + address(verifierMockB), + abi.encodeCall(IRiscZeroVerifier.verify, (TEST_RECEIPT_B.seal, TestReceipt.IMAGE_ID, TEST_JOURNAL_DIGEST)), + 1 + ); + vm.expectCall( + address(verifierMockB), + abi.encodeCall( + IRiscZeroVerifier.verify, (TEST_MANGLED_RECEIPT_B.seal, TestReceipt.IMAGE_ID, TEST_JOURNAL_DIGEST) + ), + 1 + ); + + layeredRouter.addVerifier(SELECTOR_A, verifierMockA); + layeredRouter.addVerifier(SELECTOR_B, verifierMockB); + + layeredRouter.verify(TEST_RECEIPT_A.seal, TestReceipt.IMAGE_ID, TEST_JOURNAL_DIGEST); + vm.expectRevert(VerificationFailed.selector); + layeredRouter.verify(TEST_MANGLED_RECEIPT_A.seal, TestReceipt.IMAGE_ID, TEST_JOURNAL_DIGEST); + + layeredRouter.verify(TEST_RECEIPT_B.seal, TestReceipt.IMAGE_ID, TEST_JOURNAL_DIGEST); + vm.expectRevert(VerificationFailed.selector); + layeredRouter.verify(TEST_MANGLED_RECEIPT_B.seal, TestReceipt.IMAGE_ID, TEST_JOURNAL_DIGEST); + + layeredRouter.removeVerifier(SELECTOR_B); + + layeredRouter.verify(TEST_RECEIPT_A.seal, TestReceipt.IMAGE_ID, TEST_JOURNAL_DIGEST); + vm.expectRevert(VerificationFailed.selector); + layeredRouter.verify(TEST_MANGLED_RECEIPT_A.seal, TestReceipt.IMAGE_ID, TEST_JOURNAL_DIGEST); + + vm.expectRevert(abi.encodeWithSelector(RiscZeroVerifierRouter.SelectorRemoved.selector, SELECTOR_B)); + layeredRouter.verify(TEST_RECEIPT_B.seal, TestReceipt.IMAGE_ID, TEST_JOURNAL_DIGEST); + } + + function test_OnlyOwnerCanAddVerifier() external { + layeredRouter.addVerifier(SELECTOR_A, verifierMockA); + + layeredRouter.renounceOwnership(); + + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, address(this))); + layeredRouter.addVerifier(SELECTOR_B, verifierMockB); + } + + function test_OnlyOwnerCanRemoveVerifier() external { + layeredRouter.addVerifier(SELECTOR_A, verifierMockA); + + layeredRouter.renounceOwnership(); + + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, address(this))); + layeredRouter.removeVerifier(SELECTOR_A); + } + + function test_VerifierCanOnlyBeAddedOnce() external { + layeredRouter.addVerifier(SELECTOR_A, verifierMockA); + + vm.expectRevert(abi.encodeWithSelector(RiscZeroVerifierRouter.SelectorInUse.selector, SELECTOR_A)); + layeredRouter.addVerifier(SELECTOR_A, verifierMockA); + } + + function test_VerifierCannotBeAddedAfterRemove() external { + layeredRouter.addVerifier(SELECTOR_A, verifierMockA); + layeredRouter.removeVerifier(SELECTOR_A); + + vm.expectRevert(abi.encodeWithSelector(RiscZeroVerifierRouter.SelectorRemoved.selector, SELECTOR_A)); + layeredRouter.addVerifier(SELECTOR_A, verifierMockA); + } + + function test_UnsetVerifierCanBeRemoved() external { + layeredRouter.removeVerifier(SELECTOR_A); + } + + function test_TransferRouterOwnership() external { + address newOwner = address(0xc0ffee); + + layeredRouter.transferOwnership(newOwner); + assertEq(layeredRouter.pendingOwner(), newOwner); + assertEq(layeredRouter.owner(), address(this)); + + vm.startPrank(newOwner); + layeredRouter.acceptOwnership(); + vm.stopPrank(); + + assertEq(layeredRouter.owner(), newOwner); + } + + function test_CannotAddZeroAddressVerifier() external { + vm.expectRevert(abi.encodeWithSelector(RiscZeroVerifierRouter.VerifierAddressZero.selector)); + layeredRouter.addVerifier(SELECTOR_A, IRiscZeroVerifier(address(0))); + } +} diff --git a/contracts/test/receipts/Blake3Groth16TestReceipt.sol b/contracts/test/receipts/Blake3Groth16TestReceipt.sol new file mode 100644 index 000000000..45074b894 --- /dev/null +++ b/contracts/test/receipts/Blake3Groth16TestReceipt.sol @@ -0,0 +1,16 @@ +// Copyright 2025 Boundless Foundation, Inc. +// +// Use of this source code is governed by the Business Source License +// as found in the LICENSE-BSL file. +// SPDX-License-Identifier: BUSL-1.1 + +// This file is automatically generated by: +// cargo xtask bootstrap-groth16 + +pragma solidity ^0.8.13; + +library TestReceipt { + bytes public constant SEAL = + hex"62f049f609e5e571a5daab1c3a3c4e02e4e3f3104218cb0bc39a1067859858d09eefd9102809fbb55dd140c09cf131dae9c271d7d386b021389e929f791e7aa4ffdb90741b8f4159cc9d57fce7c4bb5a7da795cb15cd35da3f1067218e1359fe2430af2b24b5ac63cdaf40cdb240039372942207e1432496eb7efa4dec0cee4bef90cece1ae0027bc89807ac1293ce8d9c8ce3dd999f6926ea0b5904acd0182c85a203b325b504efe9bcef7d9dca9cbf7f2d1b312bd189352e7a50a969b2175dc3a3079615a8fa448aba2fc439af0a5f01949a47507210c5f3088a485ecbe6c4343e7227145675020b043e2b1211be05c0dbab20d5e5423b5b53d193b7a8cb91455dca86"; + bytes32 public constant CLAIM_DIGEST = hex"00518e3981d8f63a944afd3d1d2b5c23ba7968488981875b7659e1beb2a95a63"; +} diff --git a/contracts/test/receipts/Groth16TestReceiptV3_0.sol b/contracts/test/receipts/Groth16TestReceiptV3_0.sol new file mode 100644 index 000000000..859bdbf12 --- /dev/null +++ b/contracts/test/receipts/Groth16TestReceiptV3_0.sol @@ -0,0 +1,15 @@ +// Copyright 2025 Boundless Foundation, Inc. +// +// Use of this source code is governed by the Business Source License +// as found in the LICENSE-BSL file. +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.13; + +library TestReceipt { + bytes public constant SEAL = + hex"73c457ba2ccb718fd9092cc11546eeded62a44d3ed274076dd3ec154fae8739f3432050b2005be2c5dbe6c08bfd04b30601a462540962bc26a2f38c5cfc0a4d76d8f1b8015e690a1b230081234867edeedb2f98bcdf33d0471c2aa5e8db63b72333f871527eb5d1fcf0a7af50fb8f42e8699e2c4eda3cd93f4e2a930096ae78e38bea4020c5c3d963dc453b4b302170e47c0cf53382255143c8fcef474d8b6eaaa8daaaf092c2f650809a3afbd122ef128cb882c2de7a6ccddd2e544b645fa3fedf6bcc92e09be04876a07778231fd5b93305d35fd8af23f040a11682a8c64130370804f28f07a76fa538755276e42c04b5f7eb97b04b68b65fa50e3181a0452069a3667"; + bytes public constant JOURNAL = hex"6a75737420612073696d706c652072656365697074"; + bytes32 public constant IMAGE_ID = hex"11d264ed8dfdee222b820f0278e4d7f55d4b69a5472253a471c102265a91ea1a"; + bytes32 public constant USER_ID = hex"11d264ed8dfdee222b820f0278e4d7f55d4b69a5472253a471c102265a91ea1a"; +} diff --git a/contracts/test/receipts/SetInclusionTestReceiptV0_9.sol b/contracts/test/receipts/SetInclusionTestReceiptV0_9.sol new file mode 100644 index 000000000..64a0acbea --- /dev/null +++ b/contracts/test/receipts/SetInclusionTestReceiptV0_9.sol @@ -0,0 +1,17 @@ +// Copyright 2025 Boundless Foundation, Inc. +// +// Use of this source code is governed by the Business Source License +// as found in the LICENSE-BSL file. +// SPDX-License-Identifier: BUSL-1.1 + +// This file is automatically generated by: +// cargo run --bin set-inclusion-test-receipt -- --path [set-builder-elf-path] + +pragma solidity ^0.8.13; + +library TestSetInclusionReceipt { + bytes public constant SEAL = + hex"242f9d5b0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010473c457ba15c3c7fdb99bd1c573b7d69dd1a89c853b41f1d70367b2e912e0c3bf37c402fa2c54688484cc19db6aa6be1ad74c32b099a174bdd0a104048b3306b7bb69f28106e6fab490b2203cb8fe31cf65d540d9c2f8acbd926e542dcda59e39762b3d3f221f1a3223d8f6ccc540adc0c5780c597a814948fc6df6a344ab3b9c591e1daf0cbddc4bf49c4a29502895ec34ee618fce7e3106182957754058f2e97d7a8aa12756726c5616593b99a9bfc22effc123472ffa41b4a386399d62c612a858861c1c8ec04df3786dcc678ec8c330e2c814e4e30bfd1bd1db73c32937ac87d076942688845682345c812472026c6eb2271d125e001928ac2eeb4582b3c5e5fedec700000000000000000000000000000000000000000000000000000000"; + bytes public constant JOURNAL = hex"6500000063000000680000006f0000005f00000074000000650000007300000074000000"; + bytes32 public constant IMAGE_ID = hex"93795eafc980e752ecb2ba6ecb8203b2ff82794c5dc1d419847fea4300d76c8f"; +} diff --git a/crates/boundless-market/src/contracts/artifacts/Account.sol b/crates/boundless-market/src/contracts/artifacts/Account.sol index 9005d5e25..9ff69b3b4 100644 --- a/crates/boundless-market/src/contracts/artifacts/Account.sol +++ b/crates/boundless-market/src/contracts/artifacts/Account.sol @@ -37,10 +37,10 @@ library AccountLibrary { // forge-lint: disable-next-item(incorrect-shift) function requestFlags(Account storage self, uint32 idx) internal view returns (bool locked, bool fulfilled) { if (idx < REQUEST_FLAGS_INITIAL_BITS / REQUEST_FLAGS_BITWIDTH) { - uint64 masked = ( - self.requestFlagsInitial - & (uint64((1 << REQUEST_FLAGS_BITWIDTH) - 1) << uint64(idx * REQUEST_FLAGS_BITWIDTH)) - ) >> (idx * REQUEST_FLAGS_BITWIDTH); + uint64 masked = + (self.requestFlagsInitial + & (uint64((1 << REQUEST_FLAGS_BITWIDTH) - 1) << uint64(idx * REQUEST_FLAGS_BITWIDTH))) + >> (idx * REQUEST_FLAGS_BITWIDTH); return (masked & uint64(1) != 0, masked & uint64(2) != 0); } else { uint256 idxShifted = idx - (REQUEST_FLAGS_INITIAL_BITS / REQUEST_FLAGS_BITWIDTH); diff --git a/crates/boundless-market/src/contracts/artifacts/FulfillmentContext.sol b/crates/boundless-market/src/contracts/artifacts/FulfillmentContext.sol index f6a06cafd..727c2d6a5 100644 --- a/crates/boundless-market/src/contracts/artifacts/FulfillmentContext.sol +++ b/crates/boundless-market/src/contracts/artifacts/FulfillmentContext.sol @@ -38,9 +38,7 @@ library FulfillmentContextLibrary { /// @return The unpacked FulfillmentContext struct function unpack(uint256 packed) internal pure returns (FulfillmentContext memory) { return FulfillmentContext({ - valid: (packed & VALID_MASK) != 0, - expired: (packed & EXPIRED_MASK) != 0, - price: uint96(packed & PRICE_MASK) + valid: (packed & VALID_MASK) != 0, expired: (packed & EXPIRED_MASK) != 0, price: uint96(packed & PRICE_MASK) }); } diff --git a/crates/boundless-market/src/contracts/artifacts/Predicate.sol b/crates/boundless-market/src/contracts/artifacts/Predicate.sol index efe25f20f..407019d10 100644 --- a/crates/boundless-market/src/contracts/artifacts/Predicate.sol +++ b/crates/boundless-market/src/contracts/artifacts/Predicate.sol @@ -42,11 +42,7 @@ library PredicateLibrary { /// @notice Creates a prefix match predicate. /// @param prefix The prefix to match. /// @return A Predicate struct with type PrefixMatch and the provided prefix. - function createPrefixMatchPredicate(bytes32 imageId, bytes memory prefix) - internal - pure - returns (Predicate memory) - { + function createPrefixMatchPredicate(bytes32 imageId, bytes memory prefix) internal pure returns (Predicate memory) { return Predicate({predicateType: PredicateType.PrefixMatch, data: abi.encodePacked(imageId, prefix)}); } diff --git a/deployments-check.py b/deployments-check.py new file mode 100644 index 000000000..f290ebeb7 --- /dev/null +++ b/deployments-check.py @@ -0,0 +1,174 @@ +import tomllib +import re +import sys +from pathlib import Path +from typing import Dict, List + + +def extract_rs_addresses(rs_content: str, network: str) -> Dict[str, str]: + """ + Parse a block like: + + pub const MAINNET: Deployment = Deployment { + chain_id: Some(NamedChain::Mainnet as u64), + boundless_market_address: address!("0x..."), + verifier_router_address: Some(address!("0x...")), + set_verifier_address: address!("0x..."), + collateral_token_address: Some(address!("0x...")), + }; + + Returns a dict mapping *_address field names to lowercase 0x addresses. + If a field is None or not present, returns ''. + """ + block_pat = rf'pub const {re.escape(network.upper())}\s*:\s*Deployment\s*=\s*Deployment\s*\{{(.*?)\}};' + m = re.search(block_pat, rs_content, re.DOTALL) + addresses: Dict[str, str] = {} + if not m: + return addresses + + block = m.group(1) + + for m_field in re.finditer(r'(\w+_address)\s*:\s*([^,]+),', block): + field = m_field.group(1) + val = m_field.group(2).strip() + + if val == "None": + addresses[field] = "" + continue + + m_addr = re.search(r'address!\(\s*"(?P0x[a-fA-F0-9]{40})"\s*\)', val) + if m_addr: + addresses[field] = m_addr.group("addr").lower() + else: + addresses[field] = "" + + return addresses + + +def toml_section(toml_data: dict, network_key: str) -> dict: + return (toml_data.get("deployment") or {}).get(network_key, {}) or {} + + +def main(): + with open("contracts/deployment.toml", "rb") as f: + toml_data = tomllib.load(f) + + rs_content = Path("crates/boundless-market/src/deployments.rs").read_text() + zkc_rs_content = Path("crates/zkc/src/deployments.rs").read_text() + povw_rs_content = Path("crates/povw/src/deployments.rs").read_text() + + errors = 0 + + # RS const names + boundless_rs_network_keys = { + "base-mainnet": "BASE", + "base-sepolia": "BASE_SEPOLIA", + "ethereum-sepolia": "SEPOLIA", + } + + zkc_rs_network_keys = { + "ethereum-mainnet": "MAINNET", + "ethereum-sepolia": "SEPOLIA", + } + + # ---- Boundless Market + SetVerifier + Router + CollateralToken ---- + for net_key in boundless_rs_network_keys: + toml_net = toml_section(toml_data, net_key) + rs_addrs = extract_rs_addresses(rs_content, boundless_rs_network_keys[net_key]) + + mapping = { + "boundless-market": "boundless_market_address", + "verifier": "verifier_router_address", + "set-verifier": "set_verifier_address", + "collateral-token": "collateral_token_address", + } + + for toml_field, addr_field in mapping.items(): + toml_addr = str(toml_net.get(toml_field, "") or "").lower() + rs_addr = str(rs_addrs.get(addr_field, "") or "").lower() + + # Presence checks + if not toml_addr: + print(f"❌ Missing [deployment.{net_key}] {toml_field} in deployment.toml") + errors += 1 + if not rs_addr: + print( + f"❌ Missing [{net_key}] {addr_field} in crates/boundless-market/src/deployments.rs" + ) + errors += 1 + + if toml_addr and rs_addr and toml_addr != rs_addr: + print(f"❌ Mismatch [{net_key}] {toml_field} between TOML and RS:") + print(f" TOML: {toml_addr}") + print(f" RS : {rs_addr}") + errors += 1 + + # ---- ZKC + veZKC ---- + for net_key in zkc_rs_network_keys: + toml_net = toml_section(toml_data, net_key) + rs_addrs = extract_rs_addresses(zkc_rs_content, zkc_rs_network_keys[net_key]) + + mapping = { + "zkc": "zkc_address", + "vezkc": "vezkc_address", + } + + for toml_field, addr_field in mapping.items(): + toml_addr = str(toml_net.get(toml_field, "") or "").lower() + rs_addr = str(rs_addrs.get(addr_field, "") or "").lower() + + if not toml_addr: + print(f"❌ Missing [deployment.{net_key}] {toml_field} in deployment.toml") + errors += 1 + if not rs_addr: + print( + f"❌ Missing [{net_key}] {addr_field} in crates/zkc/src/deployments.rs" + ) + errors += 1 + + if toml_addr and rs_addr and toml_addr != rs_addr: + print(f"❌ Mismatch [{net_key}] {toml_field} between TOML and RS:") + print(f" TOML: {toml_addr}") + print(f" RS : {rs_addr}") + errors += 1 + + # ---- POVW ---- + for net_key in zkc_rs_network_keys: + toml_net = toml_section(toml_data, net_key) + rs_addrs = extract_rs_addresses(povw_rs_content, zkc_rs_network_keys[net_key]) + + mapping = { + "zkc": "zkc_address", + "vezkc": "vezkc_address", + "povw-accounting": "povw_accounting_address", + "povw-mint": "povw_mint_address", + } + + for toml_field, addr_field in mapping.items(): + toml_addr = str(toml_net.get(toml_field, "") or "").lower() + rs_addr = str(rs_addrs.get(addr_field, "") or "").lower() + + if not toml_addr: + print(f"❌ Missing [deployment.{net_key}] {toml_field} in deployment.toml") + errors += 1 + if not rs_addr: + print( + f"❌ Missing [{net_key}] {addr_field} in crates/povw/src/deployments.rs" + ) + errors += 1 + + if toml_addr and rs_addr and toml_addr != rs_addr: + print(f"❌ Mismatch [{net_key}] {toml_field} between TOML and RS:") + print(f" TOML: {toml_addr}") + print(f" RS : {rs_addr}") + errors += 1 + + if errors == 0: + print("✅ All deployment addresses match between deployment.toml and deployments.rs.") + else: + print(f"\n❌ Found {errors} issues. Please fix inconsistencies.") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/foundry.toml b/foundry.toml index 79fd82daf..1234595de 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,7 +3,12 @@ src = "./contracts/src" # OZ Upgrades requires the out dir to be `./out`, or for `FOUNDRY_OUT` to be set # https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades/blob/cfd861bc18ef4737e82eae6ec75304e27af699ef/src/internal/Utils.sol#L139-L147 out = "./out" -fs_permissions = [{ access = "read", path = "./out" }, { access = "read", path = "./contracts/out" }, { access = "read-write", path = "./contracts/deployment.toml" }] +fs_permissions = [ + { access = "read", path = "./out" }, + { access = "read", path = "./contracts/out" }, + { access = "read-write", path = "./contracts/deployment.toml" }, + { access = "read", path = "./contracts/deployment_verifier.toml" }, +] libs = ["./lib"] script = "./contracts/scripts" test = "./contracts/test" @@ -31,6 +36,7 @@ isolate = true line_length = 120 tab_width = 4 quote_style = "double" +ignore = ["contracts/src/blake3-groth16/Groth16Verifier.sol"] [lint] exclude_lints = ["asm-keccak256", "incorrect-shift"] @@ -40,7 +46,10 @@ exclude_lints = ["asm-keccak256", "incorrect-shift"] [profile.deployment-test] test = "contracts/deployment-test" #match_path = "contracts/deployment-test/*" -fs_permissions = [{ access = "read", path = "contracts/deployment.toml" }] +fs_permissions = [ + { access = "read", path = "contracts/deployment.toml" }, + { access = "read", path = "contracts/deployment_verifier.toml" }, +] isolate = true # Profile used for deploying PoVW contracts with higher optimization @@ -48,7 +57,11 @@ isolate = true [profile.povw-deploy] src = "./contracts/src" out = "./out" -fs_permissions = [{ access = "read", path = "./out" }, { access = "read", path = "./contracts/out" }, { access = "read-write", path = "./contracts/deployment.toml" }] +fs_permissions = [ + { access = "read", path = "./out" }, + { access = "read", path = "./contracts/out" }, + { access = "read-write", path = "./contracts/deployment.toml" }, +] libs = ["./lib"] script = "./contracts/scripts" test = "./contracts/test" @@ -76,4 +89,6 @@ ffi = true ast = true build_info = true extra_output = ["storageLayout"] -fs_permissions = [{ access = "read", path = "contracts/reference-contract/out" }] +fs_permissions = [ + { access = "read", path = "contracts/reference-contract/out" }, +] diff --git a/license-check.py b/license-check.py index 1a5851b72..b794c05f9 100755 --- a/license-check.py +++ b/license-check.py @@ -40,6 +40,8 @@ str(Path.cwd()) + "/contracts/src/SetBuilderImageID.sol", str(Path.cwd()) + "/contracts/src/libraries/AssessorImageID.sol", str(Path.cwd()) + "/contracts/src/libraries/UtilImageID.sol", + str(Path.cwd()) + "/contracts/src/verifier/RiscZeroVerifierRouter.sol", + str(Path.cwd()) + "/contracts/src/blake3-groth16/Groth16Verifier.sol", str(Path.cwd()) + "/crates/boundless-market/src/contracts/artifacts", str(Path.cwd()) + "/crates/boundless-market/src/contracts/bytecode.rs", str(Path.cwd()) + "/crates/povw/src/contracts/artifacts",