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/ManageVerifier.s.sol b/contracts/scripts/ManageVerifier.s.sol new file mode 100644 index 000000000..4e51af978 --- /dev/null +++ b/contracts/scripts/ManageVerifier.s.sol @@ -0,0 +1,902 @@ +// 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..8d688c9df --- /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 + +5. 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/blake3-groth16/Blake3Groth16Verifier.sol b/contracts/src/blake3-groth16/Blake3Groth16Verifier.sol new file mode 100644 index 000000000..c2bada8fd --- /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(); + } + } +} \ No newline at end of file diff --git a/contracts/src/blake3-groth16/ControlID.sol b/contracts/src/blake3-groth16/ControlID.sol new file mode 100644 index 000000000..1c16ac491 --- /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"; +} \ No newline at end of file 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/test/Blake3Groth16Verifier.t.sol b/contracts/test/Blake3Groth16Verifier.t.sol new file mode 100644 index 000000000..1b6606a57 --- /dev/null +++ b/contracts/test/Blake3Groth16Verifier.t.sol @@ -0,0 +1,69 @@ +// 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/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..21b0e01af --- /dev/null +++ b/contracts/test/receipts/SetInclusionTestReceiptV0_9.sol @@ -0,0 +1,27 @@ +// 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 + +// 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/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" }, +]