Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions contracts/src/verifier/RiscZeroVerifierRouter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright 2025 RISC Zero, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0

pragma solidity ^0.8.9;

import {Ownable, Ownable2Step} from "openzeppelin/contracts/access/Ownable2Step.sol";

import {IRiscZeroVerifier, Receipt} from "risc0/IRiscZeroVerifier.sol";

/// @notice Router for IRiscZeroVerifier, allowing multiple implementations to be accessible behind a single address.
contract RiscZeroVerifierRouter is IRiscZeroVerifier, Ownable2Step {
/// @notice Mapping from 4-byte verifier selector to verifier contracts.
/// Used to route receipts to verifiers that are able to check the receipt.
mapping(bytes4 => IRiscZeroVerifier) public verifiers;

/// @notice Value of an entry that has never been set.
IRiscZeroVerifier internal constant UNSET = IRiscZeroVerifier(address(0));
/// @notice A "tombstone" value used to mark verifier entries that have been removed from the mapping.
IRiscZeroVerifier internal constant TOMBSTONE = IRiscZeroVerifier(address(1));

/// @notice Error raised when attempting to verify a receipt with a selector that is not
/// registered on this router. Generally, this indicates a version mismatch where the
/// prover generated a receipt with version of the zkVM that does not match any
/// registered version on this router contract.
error SelectorUnknown(bytes4 selector);
/// @notice Error raised when attempting to add a verifier for a selector that is already registered.
error SelectorInUse(bytes4 selector);
/// @notice Error raised when attempting to verify a receipt with a selector that has been
/// removed, or attempting to add a new verifier with a selector that was previously
/// registered and then removed.
error SelectorRemoved(bytes4 selector);
/// @notice Error raised when attempting to add a verifier with a zero address.
error VerifierAddressZero();

constructor(address admin) Ownable(admin) {}

/// @notice Adds a verifier to the router, such that it can receive receipt verification calls.
function addVerifier(bytes4 selector, IRiscZeroVerifier verifier) external virtual onlyOwner {
if (verifiers[selector] == TOMBSTONE) {
revert SelectorRemoved({selector: selector});
}
if (verifiers[selector] != UNSET) {
revert SelectorInUse({selector: selector});
}
if (address(verifier) == address(0)) {
revert VerifierAddressZero();
}
verifiers[selector] = verifier;
}

/// @notice Removes verifier from the router, such that it can not receive verification calls.
/// Removing a selector sets it to the tombstone value. It can never be set to any
/// other value, and can never be reused for a new verifier, in order to enforce the
/// property that each selector maps to at most one implementation across time.
function removeVerifier(bytes4 selector) external virtual onlyOwner {
// Simple check to reduce the chance of accidents.
// NOTE: If there ever _is_ a reason to remove a selector that has never been set, the owner
// can call addVerifier with the tombstone address.
if (verifiers[selector] == UNSET) {
revert SelectorUnknown({selector: selector});
}
verifiers[selector] = TOMBSTONE;
}

/// @notice Get the associated verifier, reverting if the selector is unknown or removed.
function getVerifier(bytes4 selector) public view virtual returns (IRiscZeroVerifier) {
IRiscZeroVerifier verifier = verifiers[selector];
if (verifier == UNSET) {
revert SelectorUnknown({selector: selector});
}
if (verifier == TOMBSTONE) {
revert SelectorRemoved({selector: selector});
}
return verifier;
}

/// @notice Get the associated verifier, reverting if the selector is unknown or removed.
function getVerifier(bytes calldata seal) public view returns (IRiscZeroVerifier) {
// Use the first 4 bytes of the seal at the selector to look up in the mapping.
return getVerifier(bytes4(seal[0:4]));
}

/// @inheritdoc IRiscZeroVerifier
function verify(bytes calldata seal, bytes32 imageId, bytes32 journalDigest) external view virtual {
getVerifier(seal).verify(seal, imageId, journalDigest);
}

/// @inheritdoc IRiscZeroVerifier
function verifyIntegrity(Receipt calldata receipt) external view virtual {
getVerifier(receipt.seal).verifyIntegrity(receipt);
}
}
103 changes: 103 additions & 0 deletions contracts/src/verifier/VerifierLayeredRouter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Copyright 2025 Boundless Foundation, Inc.
//
// Use of this source code is governed by the Business Source License
// as found in the LICENSE-BSL file.
// SPDX-License-Identifier: BUSL-1.1

pragma solidity ^0.8.9;

import {IRiscZeroVerifier, Receipt} from "risc0/IRiscZeroVerifier.sol";
import {RiscZeroVerifierRouter} from "./RiscZeroVerifierRouter.sol";

/// @notice A layered router enabling additional verifier implementations to be registered on top of a
/// parent router, while delegating unknown selectors to the parent.
/// @dev Resolution checks this router first and falls back to the parent router when unset.
contract VerifierLayeredRouter is RiscZeroVerifierRouter {
/// @notice The parent RISC Zero verifier router used as fallback.
RiscZeroVerifierRouter public immutable parentRouter;

constructor(address owner, RiscZeroVerifierRouter _parentRouter) RiscZeroVerifierRouter(owner) {
require(address(_parentRouter) != address(0), "Parent router address cannot be zero");
parentRouter = _parentRouter;
}

/// @notice Gets the parent RISC Zero verifier router.
function getParentRouter() external view returns (RiscZeroVerifierRouter) {
return parentRouter;
}

/// @notice Adds a verifier to the router, such that it can receive receipt verification calls.
/// @dev Ensures that the selector is not already registered or removed in either this router or the parent router.
function addVerifier(bytes4 selector, IRiscZeroVerifier verifier) external override onlyOwner {
// Ensure the selector is not removed from the parent router.
if (parentRouter.verifiers(selector) == TOMBSTONE) {
revert SelectorRemoved({selector: selector});
}
// Ensure the selector is not already in use in the parent router.
if (parentRouter.verifiers(selector) != UNSET) {
revert SelectorInUse({selector: selector});
}
// Ensure the selector is not removed from this router.
if (verifiers[selector] == TOMBSTONE) {
revert SelectorRemoved({selector: selector});
}
// Ensure the selector is not already in use in this router.
if (verifiers[selector] != UNSET) {
revert SelectorInUse({selector: selector});
}
// Ensure the verifier address is not zero.
if (address(verifier) == address(0)) {
revert VerifierAddressZero();
}
verifiers[selector] = verifier;
}

/// @inheritdoc RiscZeroVerifierRouter
function removeVerifier(bytes4 selector) external override onlyOwner {
verifiers[selector] = TOMBSTONE;
}

/// @notice Get the associated verifier, falling back to the parent router if unset.
function getVerifier(bytes4 selector) public view override returns (IRiscZeroVerifier) {
IRiscZeroVerifier verifier = verifiers[selector];
// If the verifier is unset, fall back to the parent router.
if (verifier == UNSET) {
return parentRouter.getVerifier(selector);
}
if (verifier == TOMBSTONE) {
revert SelectorRemoved({selector: selector});
}
return verifier;
}

/// @inheritdoc IRiscZeroVerifier
function verify(bytes calldata seal, bytes32 imageId, bytes32 journalDigest) external view override {
bytes4 selector = bytes4(seal[0:4]);
IRiscZeroVerifier v = verifiers[selector];

if (v == UNSET) {
// Single external call to parent (it resolves + forwards)
parentRouter.verify(seal, imageId, journalDigest);
return;
}
if (v == TOMBSTONE) {
revert SelectorRemoved({selector: selector});
}
v.verify(seal, imageId, journalDigest);
}

/// @inheritdoc IRiscZeroVerifier
function verifyIntegrity(Receipt calldata receipt) external view override {
bytes4 selector = bytes4(receipt.seal[0:4]);
IRiscZeroVerifier v = verifiers[selector];

if (v == UNSET) {
parentRouter.verifyIntegrity(receipt);
return;
}
if (v == TOMBSTONE) {
revert SelectorRemoved({selector: selector});
}
v.verifyIntegrity(receipt);
}
}
Loading
Loading