├── .env
├── .solcover.js
├── .gitignore
├── test
└── data
│ ├── proofs.db
│ ├── rendered-vk-outer-512
│ ├── rendered-vk-outer-1024
│ ├── rendered-vk-outer-16384
│ ├── rendered-vk-outer-2048
│ ├── rendered-vk-outer-32768
│ ├── rendered-vk-outer-4096
│ ├── rendered-vk-outer-65536
│ └── rendered-vk-outer-8192
├── .prettierrc
├── contracts
├── Governance.sol
├── mock
│ ├── MockERC20.sol
│ └── MockFeeDelegate.sol
├── test
│ ├── AuxRootTest.sol
│ ├── BlockHashMessengerForTesting.sol
│ ├── ZkSyncBlockHashMessengerForTesting.sol
│ ├── RelicReceiverForTesting.sol
│ └── BlockHistoryForTesting.sol
├── interfaces
│ ├── IContractURI.sol
│ ├── IRecursiveVerifier.sol
│ ├── IProxyBlockHistory.sol
│ ├── IFeeDelegate.sol
│ ├── ITokenURI.sol
│ ├── IProver.sol
│ ├── IRelicReceiver.sol
│ ├── IBatchProver.sol
│ ├── IBlockHistory.sol
│ ├── IERC5192.sol
│ ├── IBeaconBlockHistory.sol
│ ├── IEphemeralFacts.sol
│ └── IReliquary.sol
├── lib
│ ├── Facts.sol
│ ├── Propogate.sol
│ ├── AuxMerkleTree.sol
│ ├── BaseFeeStats.sol
│ ├── Proofs.sol
│ ├── Storage.sol
│ ├── Callbacks.sol
│ ├── BytesCalldata.sol
│ ├── MerkleTree.sol
│ ├── AnemoiJive.sol
│ └── SSZ.sol
├── zksync
│ ├── ZkSyncProxyBlockHistory.sol
│ └── ZkSyncProxyBeaconBlockHistory.sol
├── tokens
│ ├── MockRelic.sol
│ ├── BirthCertificateRelic.sol
│ ├── AttendanceArtifact.sol
│ └── MockURIProvider.sol
├── ZkSyncBlockHashMessenger.sol
├── optimism
│ ├── OptimismProxyBlockHistory.sol
│ ├── interfaces
│ │ └── IOptimismNativeBlockHistory.sol
│ ├── OptimismProxyBeaconBlockHistory.sol
│ └── OptimismNativeBlockHistory.sol
├── provers
│ ├── MockProver.sol
│ ├── BlockHeaderProver.sol
│ ├── LogProver.sol
│ ├── TransactionProver.sol
│ ├── Prover.sol
│ ├── AccountStorageProver.sol
│ ├── WithdrawalProver.sol
│ ├── StorageSlotProver.sol
│ ├── CachedStorageSlotProver.sol
│ ├── BatchProver.sol
│ ├── CachedMultiStorageSlotProver.sol
│ ├── MultiStorageSlotProver.sol
│ ├── BirthCertificateProver.sol
│ ├── AccountInfoProver.sol
│ └── AttendanceProver.sol
├── OptimismBlockHashMessenger.sol
├── BlockHashMessenger.sol
├── RelicTokenConfigurable.sol
├── EphemeralFacts.sol
├── RelicToken.sol
└── ProxyBeaconBlockHistory.sol
├── utils
├── slots.js
├── gas.js
├── network.js
├── blockproof.js
├── blockhistory.js
└── importL2.js
├── deploy
├── 00_reliquary.js
├── 07_ephemeral_facts.js
├── 06_log.js
├── 05_storage.js
├── 08_block.js
├── 12_withdrawal.js
├── 13_accountInfo.js
├── 14_transaction.js
├── 09_account_storage.js
├── 08_cached_storage.js
├── 10_multistorage.js
├── 11_cached_multistorage.js
├── 16_optimism_messenger.js
├── 03_birth_certificate.js
├── 15_zksync_messenger.js
├── 17_base_messenger.js
├── 18_blast_messenger.js
├── 04_attendance.js
├── 01_verifiers.js
└── 02_block_history.js
├── README.md
├── LICENSE
└── package.json
/.env:
--------------------------------------------------------------------------------
1 | MAINNET_RPC_URL=
2 |
--------------------------------------------------------------------------------
/.solcover.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | istanbulReporter: ['cobertura','html']
3 | };
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /artifacts
2 | /cache
3 | /coverage
4 | /coverage.json
5 | /node_modules
6 | [._]*.sw[a-z]
7 |
--------------------------------------------------------------------------------
/test/data/proofs.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Relic-Protocol/relic-contracts/HEAD/test/data/proofs.db
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": true,
3 | "tabWidth": 4,
4 | "printWidth": 100,
5 | "compiler": "0.8.13"
6 | }
7 |
--------------------------------------------------------------------------------
/test/data/rendered-vk-outer-512:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Relic-Protocol/relic-contracts/HEAD/test/data/rendered-vk-outer-512
--------------------------------------------------------------------------------
/test/data/rendered-vk-outer-1024:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Relic-Protocol/relic-contracts/HEAD/test/data/rendered-vk-outer-1024
--------------------------------------------------------------------------------
/test/data/rendered-vk-outer-16384:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Relic-Protocol/relic-contracts/HEAD/test/data/rendered-vk-outer-16384
--------------------------------------------------------------------------------
/test/data/rendered-vk-outer-2048:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Relic-Protocol/relic-contracts/HEAD/test/data/rendered-vk-outer-2048
--------------------------------------------------------------------------------
/test/data/rendered-vk-outer-32768:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Relic-Protocol/relic-contracts/HEAD/test/data/rendered-vk-outer-32768
--------------------------------------------------------------------------------
/test/data/rendered-vk-outer-4096:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Relic-Protocol/relic-contracts/HEAD/test/data/rendered-vk-outer-4096
--------------------------------------------------------------------------------
/test/data/rendered-vk-outer-65536:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Relic-Protocol/relic-contracts/HEAD/test/data/rendered-vk-outer-65536
--------------------------------------------------------------------------------
/test/data/rendered-vk-outer-8192:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Relic-Protocol/relic-contracts/HEAD/test/data/rendered-vk-outer-8192
--------------------------------------------------------------------------------
/contracts/Governance.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.12;
6 |
7 | import "@openzeppelin/contracts/governance/TimelockController.sol";
8 |
--------------------------------------------------------------------------------
/contracts/mock/MockERC20.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.12;
6 |
7 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
8 |
9 | contract MockERC20 is ERC20 {
10 | constructor() ERC20("Mock", "MOCK") {
11 | _mint(msg.sender, 10 ether);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/utils/slots.js:
--------------------------------------------------------------------------------
1 | const { ethers } = require("hardhat")
2 |
3 | function getMerkleRootsSlot(blockHistory) {
4 | const storage = blockHistory.storageLayout.storage.find(({ label }) => label == "merkleRoots")
5 | const merkleRootsSlot = ethers.utils.defaultAbiCoder.encode(["uint256"], [storage.slot])
6 | return merkleRootsSlot
7 | }
8 |
9 | module.exports = {
10 | getMerkleRootsSlot
11 | }
12 |
--------------------------------------------------------------------------------
/contracts/mock/MockFeeDelegate.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.12;
6 |
7 | import "../interfaces/IFeeDelegate.sol";
8 |
9 | contract MockFeeDelegate is IFeeDelegate {
10 | function checkFee(
11 | address, /* sender */
12 | bytes calldata /* data */
13 | ) external payable {
14 | require(msg.value == 1 ether);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/deploy/00_reliquary.js:
--------------------------------------------------------------------------------
1 | const { artifacts, config, ethers, network, waffle } = require("hardhat");
2 |
3 | module.exports = async ({getNamedAccounts, deployments}) => {
4 | const {deploy} = deployments;
5 | const {deployer} = await getNamedAccounts();
6 | await deploy("Reliquary", {
7 | contract: "ReliquaryWithFee",
8 | from: deployer,
9 | args: [],
10 | log: true,
11 | skipIfAlreadyDeployed: true,
12 | });
13 | };
14 | module.exports.tags = ["Reliquary"];
15 |
--------------------------------------------------------------------------------
/contracts/test/AuxRootTest.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.0;
6 |
7 | import "../lib/AuxMerkleTree.sol";
8 |
9 | /**
10 | * @title AuxRootTest
11 | * @author Theori, Inc.
12 | * @notice Test contract for Auxiliary Merkle Roots
13 | */
14 | contract AuxRootTest {
15 | function auxRoot(bytes32[] calldata inputs) external view returns (bytes32 result) {
16 | result = AuxMerkleTree.computeRoot(inputs);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/deploy/07_ephemeral_facts.js:
--------------------------------------------------------------------------------
1 | const { ethers } = require("hardhat");
2 |
3 | module.exports = async ({getNamedAccounts, deployments}) => {
4 | const {deploy} = deployments;
5 | const {deployer} = await getNamedAccounts();
6 | const reliquary = await ethers.getContract("Reliquary");
7 |
8 | await deploy("EphemeralFacts", {
9 | from: deployer,
10 | args: [reliquary.address],
11 | log: true,
12 | });
13 | };
14 |
15 | module.exports.tags = ["EphemeralFacts"];
16 | module.exports.dependencies = ["Reliquary"];
--------------------------------------------------------------------------------
/contracts/interfaces/IContractURI.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.0;
6 |
7 | /**
8 | * @title NFT Contract Metadata URI provider
9 | * @author Theori, Inc.
10 | * @notice Outsourced contractURI provider for NFT/SBT tokens
11 | */
12 | interface IContractURI {
13 | /**
14 | * @notice Get the contract metadata URI
15 | * @return the string of the URI
16 | */
17 | function contractURI() external view returns (string memory);
18 | }
19 |
--------------------------------------------------------------------------------
/utils/gas.js:
--------------------------------------------------------------------------------
1 | const { ethers } = require("hardhat");
2 |
3 | const GWEI = ethers.BigNumber.from("1000000000");
4 |
5 | const MAX_GAS_PRICE = process.env.MAX_GAS_PRICE;
6 |
7 | function getGasOptions() {
8 | if (MAX_GAS_PRICE === undefined) return {};
9 | const defaultPriority = GWEI;
10 | const maxFeePerGas = ethers.BigNumber.from(MAX_GAS_PRICE);
11 | const maxPriorityFeePerGas = maxFeePerGas.lt(defaultPriority) ? maxFeePerGas : defaultPriority;
12 | return { maxFeePerGas, maxPriorityFeePerGas };
13 | }
14 |
15 | module.exports = {
16 | getGasOptions
17 | }
18 |
--------------------------------------------------------------------------------
/deploy/06_log.js:
--------------------------------------------------------------------------------
1 | const { ethers } = require("hardhat");
2 |
3 | module.exports = async ({getNamedAccounts, deployments}) => {
4 | const {deploy} = deployments;
5 | const {deployer} = await getNamedAccounts();
6 | const reliquary = await ethers.getContract("Reliquary");
7 | const blockHistory = await ethers.getContract("BlockHistory");
8 |
9 | await deploy("LogProver", {
10 | from: deployer,
11 | args: [blockHistory.address, reliquary.address],
12 | log: true,
13 | });
14 | };
15 |
16 | module.exports.tags = ["LogProver"];
17 | module.exports.dependencies = ["Reliquary", "BlockHistory"];
18 |
--------------------------------------------------------------------------------
/deploy/05_storage.js:
--------------------------------------------------------------------------------
1 | const { ethers } = require("hardhat");
2 |
3 | module.exports = async ({getNamedAccounts, deployments}) => {
4 | const {deploy} = deployments;
5 | const {deployer} = await getNamedAccounts();
6 | const reliquary = await ethers.getContract("Reliquary");
7 | const blockHistory = await ethers.getContract("BlockHistory");
8 |
9 | await deploy("StorageSlotProver", {
10 | from: deployer,
11 | args: [blockHistory.address, reliquary.address],
12 | log: true,
13 | });
14 | };
15 |
16 | module.exports.tags = ["StorageSlotProver"];
17 | module.exports.dependencies = ["Reliquary", "BlockHistory"];
18 |
--------------------------------------------------------------------------------
/deploy/08_block.js:
--------------------------------------------------------------------------------
1 | const { ethers } = require("hardhat");
2 |
3 | module.exports = async ({getNamedAccounts, deployments}) => {
4 | const {deploy} = deployments;
5 | const {deployer} = await getNamedAccounts();
6 | const reliquary = await ethers.getContract("Reliquary");
7 | const blockHistory = await ethers.getContract("BlockHistory");
8 |
9 | await deploy("BlockHeaderProver", {
10 | from: deployer,
11 | args: [blockHistory.address, reliquary.address],
12 | log: true,
13 | });
14 | };
15 |
16 | module.exports.tags = ["BlockHeaderProver"];
17 | module.exports.dependencies = ["Reliquary", "BlockHistory"];
18 |
--------------------------------------------------------------------------------
/deploy/12_withdrawal.js:
--------------------------------------------------------------------------------
1 |
2 | const { ethers } = require("hardhat");
3 |
4 | module.exports = async ({getNamedAccounts, deployments}) => {
5 | const {deploy} = deployments;
6 | const {deployer} = await getNamedAccounts();
7 | const reliquary = await ethers.getContract("Reliquary");
8 | const blockHistory = await ethers.getContract("BlockHistory");
9 |
10 | await deploy("WithdrawalProver", {
11 | from: deployer,
12 | args: [blockHistory.address, reliquary.address],
13 | log: true,
14 | });
15 | };
16 |
17 | module.exports.tags = ["WithdrawalProver"];
18 | module.exports.dependencies = ["Reliquary", "BlockHistory"];
19 |
--------------------------------------------------------------------------------
/deploy/13_accountInfo.js:
--------------------------------------------------------------------------------
1 |
2 | const { ethers } = require("hardhat");
3 |
4 | module.exports = async ({getNamedAccounts, deployments}) => {
5 | const {deploy} = deployments;
6 | const {deployer} = await getNamedAccounts();
7 | const reliquary = await ethers.getContract("Reliquary");
8 | const blockHistory = await ethers.getContract("BlockHistory");
9 |
10 | await deploy("AccountInfoProver", {
11 | from: deployer,
12 | args: [blockHistory.address, reliquary.address],
13 | log: true,
14 | });
15 | };
16 |
17 | module.exports.tags = ["AccountInfoProver"];
18 | module.exports.dependencies = ["Reliquary", "BlockHistory"];
19 |
--------------------------------------------------------------------------------
/deploy/14_transaction.js:
--------------------------------------------------------------------------------
1 |
2 | const { ethers } = require("hardhat");
3 |
4 | module.exports = async ({getNamedAccounts, deployments}) => {
5 | const {deploy} = deployments;
6 | const {deployer} = await getNamedAccounts();
7 | const reliquary = await ethers.getContract("Reliquary");
8 | const blockHistory = await ethers.getContract("BlockHistory");
9 |
10 | await deploy("TransactionProver", {
11 | from: deployer,
12 | args: [blockHistory.address, reliquary.address],
13 | log: true,
14 | });
15 | };
16 |
17 | module.exports.tags = ["TransactionProver"];
18 | module.exports.dependencies = ["Reliquary", "BlockHistory"];
19 |
--------------------------------------------------------------------------------
/deploy/09_account_storage.js:
--------------------------------------------------------------------------------
1 | const { ethers } = require("hardhat");
2 |
3 | module.exports = async ({getNamedAccounts, deployments}) => {
4 | const {deploy} = deployments;
5 | const {deployer} = await getNamedAccounts();
6 | const reliquary = await ethers.getContract("Reliquary");
7 | const blockHistory = await ethers.getContract("BlockHistory");
8 |
9 | await deploy("AccountStorageProver", {
10 | from: deployer,
11 | args: [blockHistory.address, reliquary.address],
12 | log: true,
13 | });
14 | };
15 |
16 | module.exports.tags = ["AccountStorageProver"];
17 | module.exports.dependencies = ["Reliquary", "BlockHistory"];
18 |
--------------------------------------------------------------------------------
/deploy/08_cached_storage.js:
--------------------------------------------------------------------------------
1 | const { ethers } = require("hardhat");
2 |
3 | module.exports = async ({getNamedAccounts, deployments}) => {
4 | const {deploy} = deployments;
5 | const {deployer} = await getNamedAccounts();
6 | const reliquary = await ethers.getContract("Reliquary");
7 | const blockHistory = await ethers.getContract("BlockHistory");
8 |
9 | await deploy("CachedStorageSlotProver", {
10 | from: deployer,
11 | args: [blockHistory.address, reliquary.address],
12 | log: true,
13 | });
14 | };
15 |
16 | module.exports.tags = ["CachedStorageSlotProver"];
17 | module.exports.dependencies = ["Reliquary", "BlockHistory"];
18 |
--------------------------------------------------------------------------------
/contracts/interfaces/IRecursiveVerifier.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.0;
6 |
7 | import {RecursiveProof} from "../lib/Proofs.sol";
8 |
9 | /**
10 | * @title Verifier of zk-SNARK proofs
11 | * @author Theori, Inc.
12 | * @notice Provider of validity checking of zk-SNARKs
13 | */
14 | interface IRecursiveVerifier {
15 | /**
16 | * @notice Checks the validity of SNARK data
17 | * @param proof the proof to verify
18 | * @return the validity of the proof
19 | */
20 | function verify(RecursiveProof calldata proof) external view returns (bool);
21 | }
22 |
--------------------------------------------------------------------------------
/deploy/10_multistorage.js:
--------------------------------------------------------------------------------
1 |
2 | const { ethers } = require("hardhat");
3 |
4 | module.exports = async ({getNamedAccounts, deployments}) => {
5 | const {deploy} = deployments;
6 | const {deployer} = await getNamedAccounts();
7 | const reliquary = await ethers.getContract("Reliquary");
8 | const blockHistory = await ethers.getContract("BlockHistory");
9 |
10 | await deploy("MultiStorageSlotProver", {
11 | from: deployer,
12 | args: [blockHistory.address, reliquary.address],
13 | log: true,
14 | });
15 | };
16 |
17 | module.exports.tags = ["MultiStorageSlotProver"];
18 | module.exports.dependencies = ["Reliquary", "BlockHistory"];
19 |
--------------------------------------------------------------------------------
/contracts/interfaces/IProxyBlockHistory.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.0;
6 |
7 | import "./IBlockHistory.sol";
8 |
9 | /**
10 | * @title Block history provider
11 | * @author Theori, Inc.
12 | * @notice IBlockHistory provides a way to verify a blockhash
13 | */
14 |
15 | interface IProxyBlockHistory is IBlockHistory {
16 | /**
17 | * @notice Import a trusted block hash from the messenger
18 | * @param number the block number to import
19 | * @param hash the block hash
20 | */
21 | function importTrustedHash(uint256 number, bytes32 hash) external;
22 | }
23 |
--------------------------------------------------------------------------------
/deploy/11_cached_multistorage.js:
--------------------------------------------------------------------------------
1 |
2 | const { ethers } = require("hardhat");
3 |
4 | module.exports = async ({getNamedAccounts, deployments}) => {
5 | const {deploy} = deployments;
6 | const {deployer} = await getNamedAccounts();
7 | const reliquary = await ethers.getContract("Reliquary");
8 | const blockHistory = await ethers.getContract("BlockHistory");
9 |
10 | await deploy("CachedMultiStorageSlotProver", {
11 | from: deployer,
12 | args: [blockHistory.address, reliquary.address],
13 | log: true,
14 | });
15 | };
16 |
17 | module.exports.tags = ["CachedMultiStorageSlotProver"];
18 | module.exports.dependencies = ["Reliquary", "BlockHistory"];
19 |
--------------------------------------------------------------------------------
/contracts/interfaces/IFeeDelegate.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.0;
6 |
7 | /**
8 | * @title Fee provider
9 | * @author Theori, Inc.
10 | * @notice Acceptor of fees for functions requiring payment
11 | */
12 | interface IFeeDelegate {
13 | /**
14 | * @notice Accept any required fee from the sender
15 | * @param sender the originator of the call that may be paying
16 | * @param data opaque data to help determine costs
17 | * @dev reverts if a fee is required and not provided
18 | */
19 | function checkFee(address sender, bytes calldata data) external payable;
20 | }
21 |
--------------------------------------------------------------------------------
/contracts/interfaces/ITokenURI.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.0;
6 |
7 | /**
8 | * @title NFT Token URI provider
9 | * @author Theori, Inc.
10 | * @notice Outsourced tokenURI provider for NFT/SBT tokens
11 | */
12 | interface ITokenURI {
13 | /**
14 | * @notice Get the URI for the given token
15 | * @param tokenID the unique ID for the token
16 | * @return the string of the URI
17 | * @dev when called with an invalid tokenID, this may revert,
18 | * or it may return invalid output
19 | */
20 | function tokenURI(uint256 tokenID) external view returns (string memory);
21 | }
22 |
--------------------------------------------------------------------------------
/contracts/lib/Facts.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.12;
6 |
7 | type FactSignature is bytes32;
8 |
9 | struct Fact {
10 | address account;
11 | FactSignature sig;
12 | bytes data;
13 | }
14 |
15 | library Facts {
16 | uint8 internal constant NO_FEE = 0;
17 |
18 | function toFactSignature(uint8 cls, bytes memory data) internal pure returns (FactSignature) {
19 | return FactSignature.wrap(bytes32((uint256(keccak256(data)) << 8) | cls));
20 | }
21 |
22 | function toFactClass(FactSignature factSig) internal pure returns (uint8) {
23 | return uint8(uint256(FactSignature.unwrap(factSig)));
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/contracts/interfaces/IProver.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | import "../lib/Facts.sol";
6 |
7 | pragma solidity >=0.8.12;
8 |
9 | /**
10 | * @title IProver
11 | * @author Theori, Inc.
12 | * @notice IProver is a standard interface implemented by some Relic provers.
13 | * Supports proving a fact ephemerally or proving and storing it in the
14 | * Reliquary.
15 | */
16 | interface IProver {
17 | /**
18 | * @notice prove a fact ephemerally
19 | * @param proof the encoded proof, depends on the prover implementation
20 | * @param store whether to store the facts in the reliquary
21 | * @return fact the proven fact information
22 | */
23 | function prove(bytes calldata proof, bool store) external payable returns (Fact memory fact);
24 | }
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |

5 | Contracts
6 |
7 |
8 |
9 |
10 |
11 | ## Usage
12 |
13 | The solidity and yul contracts can be built using hardhat:
14 |
15 | ```
16 | npm install
17 | npx hardhat compile
18 | ```
19 |
20 | The test suite uses a hardhat fork of the Ethereum mainnet. After specifing an archive node in `.env`, tests can be run with:
21 |
22 | ```
23 | npx hardhat test
24 | ```
25 |
26 | ## License
27 |
28 | Documentation and source code in this repository is licensed for reference use only. You may download it, read it, and/or build it only for the purposes of determining its correctness and security. You may not distribute it or use it in any derivative works without permission of Theori, Inc.
29 |
--------------------------------------------------------------------------------
/contracts/interfaces/IRelicReceiver.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.0;
6 |
7 | import "../lib/Facts.sol";
8 |
9 | /**
10 | * @title IRelicReceiver
11 | * @author Theori, Inc.
12 | * @notice IRelicReceiver has callbacks to receives ephemeral facts from Relic
13 | */
14 | interface IRelicReceiver {
15 | /**
16 | * @notice receives an ephemeral fact from Relic
17 | * @param initiator the account which initiated the fact proving
18 | * @param fact the proven fact information
19 | * @param data extra data passed from the initiator - this data may come
20 | * from untrusted parties and thus should be validated
21 | */
22 | function receiveFact(
23 | address initiator,
24 | Fact calldata fact,
25 | bytes calldata data
26 | ) external;
27 | }
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | 1. Definitions
2 |
3 | "License" is this document.
4 |
5 | "Licensor" is the copyright owner that is granting the License.
6 |
7 | "Software" is any documentation or source code in this directory or a child directory.
8 |
9 | 2. Grant of Copyright License
10 |
11 | Subject to the terms of this License, the Licensor grants you a non-transferable, non-exclusive, worldwide, royalty-free copyright license to reproduce the software for use as a reference for the sole purposes of evaluating the correctness and security of the software, and specifically excludes the right to distribute the software or any derivative work.
12 |
13 | 3. No Warranty
14 |
15 | Licensor provides the software "as-is" without any warranties or conditions of any kind, including, without limitation, any warranties of merchantability, non-infringement, or fitness for a particular purpose. You assume all risks of using the software.
16 |
17 |
--------------------------------------------------------------------------------
/contracts/interfaces/IBatchProver.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | import "../lib/Facts.sol";
6 |
7 | pragma solidity >=0.8.12;
8 |
9 | /**
10 | * @title IBatchProver
11 | * @author Theori, Inc.
12 | * @notice IBatchProver is a standard interface implemented by some Relic provers.
13 | * Supports proving multiple facts ephemerally or proving and storing
14 | * them in the Reliquary.
15 | */
16 | interface IBatchProver {
17 | /**
18 | * @notice prove multiple facts ephemerally
19 | * @param proof the encoded proof, depends on the prover implementation
20 | * @param store whether to store the facts in the reliquary
21 | * @return facts the proven facts' information
22 | */
23 | function proveBatch(bytes calldata proof, bool store)
24 | external
25 | payable
26 | returns (Fact[] memory facts);
27 | }
28 |
--------------------------------------------------------------------------------
/deploy/16_optimism_messenger.js:
--------------------------------------------------------------------------------
1 | const { companionNetworks, ethers } = require("hardhat");
2 |
3 | module.exports = async ({getNamedAccounts, deployments}) => {
4 | if (network.config.bridged === true || network.config.l2Native === true) {
5 | return;
6 | }
7 | const {deploy} = deployments;
8 | const {deployer} = await getNamedAccounts();
9 | const reliquary = await ethers.getContract("Reliquary");
10 | const blockHistory = await ethers.getContract("BlockHistory");
11 |
12 | const portal = config.networks[
13 | companionNetworks["optimism"].deployments.getNetworkName()
14 | ].portal
15 | await deploy("OptimismBlockHashMessenger", {
16 | from: deployer,
17 | args: [reliquary.address, blockHistory.address, portal],
18 | log: true,
19 | });
20 | };
21 |
22 | module.exports.tags = ["OptimismBlockHashMessenger"];
23 | module.exports.dependencies = ["Reliquary", "BlockHistory"];
24 |
--------------------------------------------------------------------------------
/contracts/interfaces/IBlockHistory.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.0;
6 |
7 | /**
8 | * @title Block history provider
9 | * @author Theori, Inc.
10 | * @notice IBlockHistory provides a way to verify a blockhash
11 | */
12 |
13 | interface IBlockHistory {
14 | /**
15 | * @notice Determine if the given hash corresponds to the given block
16 | * @param hash the hash if the block in question
17 | * @param num the number of the block in question
18 | * @param proof any witness data required to prove the block hash is
19 | * correct (such as a Merkle or SNARK proof)
20 | * @return boolean indicating if the block hash can be verified correct
21 | */
22 | function validBlockHash(
23 | bytes32 hash,
24 | uint256 num,
25 | bytes calldata proof
26 | ) external view returns (bool);
27 | }
28 |
--------------------------------------------------------------------------------
/deploy/03_birth_certificate.js:
--------------------------------------------------------------------------------
1 | const { ethers } = require("hardhat");
2 |
3 | module.exports = async ({getNamedAccounts, deployments}) => {
4 | const {deploy} = deployments;
5 | const {deployer} = await getNamedAccounts();
6 | const reliquary = await ethers.getContract("Reliquary");
7 | const blockHistory = await ethers.getContract("BlockHistory");
8 |
9 | await deploy("BirthCertificateRelic", {
10 | from: deployer,
11 | args: [reliquary.address],
12 | log: true,
13 | skipIfAlreadyDeployed: true,
14 | });
15 | const token = await ethers.getContract("BirthCertificateRelic");
16 |
17 | await deploy("BirthCertificateProver", {
18 | from: deployer,
19 | args: [blockHistory.address, reliquary.address, token.address],
20 | log: true,
21 | });
22 | };
23 |
24 | module.exports.tags = ["BirthCertificate"];
25 | module.exports.dependencies = ["Reliquary", "BlockHistory"];
26 |
--------------------------------------------------------------------------------
/deploy/15_zksync_messenger.js:
--------------------------------------------------------------------------------
1 | const { ethers, companionNetworks } = require("hardhat");
2 |
3 | module.exports = async ({getNamedAccounts, deployments}) => {
4 | if (network.config.bridged === true || network.config.l2Native === true) {
5 | return;
6 | }
7 | const {deploy} = deployments;
8 | const {deployer} = await getNamedAccounts();
9 | const reliquary = await ethers.getContract("Reliquary");
10 | const blockHistory = await ethers.getContract("BlockHistory");
11 |
12 | const diamondProxy = config.networks[
13 | companionNetworks["zkSync"].deployments.getNetworkName()
14 | ].diamondProxy
15 |
16 | await deploy("ZkSyncBlockHashMessenger", {
17 | from: deployer,
18 | args: [reliquary.address, blockHistory.address, diamondProxy],
19 | log: true,
20 | });
21 | };
22 |
23 | module.exports.tags = ["ZkSyncBlockHashMessenger"];
24 | module.exports.dependencies = ["Reliquary", "BlockHistory"];
25 |
--------------------------------------------------------------------------------
/contracts/zksync/ZkSyncProxyBlockHistory.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity ^0.8.0;
6 |
7 | import "@matterlabs/zksync-contracts/l2/contracts/vendor/AddressAliasHelper.sol";
8 | import "../ProxyBlockHistory.sol";
9 |
10 | /**
11 | * @title ZkSyncProxyBlockHistory
12 | * @author Theori, Inc.
13 | * @notice A wrapper around ProxyBlockHistory which translates the messenger address
14 | * according to the ZkSync aliasing rules.
15 | */
16 | contract ZkSyncProxyBlockHistory is ProxyBlockHistory {
17 | constructor(
18 | address reliquary,
19 | address messenger,
20 | address l1BlockHistory,
21 | bytes32 merkleRootsSlot
22 | )
23 | ProxyBlockHistory(
24 | reliquary,
25 | AddressAliasHelper.applyL1ToL2Alias(messenger),
26 | l1BlockHistory,
27 | merkleRootsSlot
28 | )
29 | {}
30 | }
31 |
--------------------------------------------------------------------------------
/deploy/17_base_messenger.js:
--------------------------------------------------------------------------------
1 | const { companionNetworks, ethers } = require("hardhat");
2 |
3 | module.exports = async ({getNamedAccounts, deployments}) => {
4 | if (network.config.bridged === true || network.config.l2Native === true) {
5 | return;
6 | }
7 | const {deploy} = deployments;
8 | const {deployer} = await getNamedAccounts();
9 | const reliquary = await ethers.getContract("Reliquary");
10 | const blockHistory = await ethers.getContract("BlockHistory");
11 |
12 | const portal = config.networks[
13 | companionNetworks["base"].deployments.getNetworkName()
14 | ].portal
15 | await deploy("BaseBlockHashMessenger", {
16 | contract: "OptimismBlockHashMessenger",
17 | from: deployer,
18 | args: [reliquary.address, blockHistory.address, portal],
19 | log: true,
20 | });
21 | };
22 |
23 | module.exports.tags = ["BaseBlockHashMessenger"];
24 | module.exports.dependencies = ["Reliquary", "BlockHistory"];
25 |
--------------------------------------------------------------------------------
/deploy/18_blast_messenger.js:
--------------------------------------------------------------------------------
1 | const { companionNetworks, ethers } = require("hardhat");
2 |
3 | module.exports = async ({getNamedAccounts, deployments}) => {
4 | if (network.config.bridged === true || network.config.l2Native === true) {
5 | return;
6 | }
7 | const {deploy} = deployments;
8 | const {deployer} = await getNamedAccounts();
9 | const reliquary = await ethers.getContract("Reliquary");
10 | const blockHistory = await ethers.getContract("BlockHistory");
11 |
12 | const portal = config.networks[
13 | companionNetworks["blast"].deployments.getNetworkName()
14 | ].portal
15 | await deploy("BlastBlockHashMessenger", {
16 | contract: "OptimismBlockHashMessenger",
17 | from: deployer,
18 | args: [reliquary.address, blockHistory.address, portal],
19 | log: true,
20 | });
21 | };
22 |
23 | module.exports.tags = ["BlastBlockHashMessenger"];
24 | module.exports.dependencies = ["Reliquary", "BlockHistory"];
25 |
--------------------------------------------------------------------------------
/deploy/04_attendance.js:
--------------------------------------------------------------------------------
1 | const { ethers } = require("hardhat");
2 |
3 | module.exports = async ({getNamedAccounts, deployments}) => {
4 | if (network.config.bridged === true || network.config.l2Native === true) {
5 | return;
6 | }
7 |
8 | const {deploy} = deployments;
9 | const {deployer} = await getNamedAccounts();
10 | const reliquary = await ethers.getContract("Reliquary");
11 | const blockHistory = await ethers.getContract("BlockHistory");
12 |
13 | await deploy("AttendanceArtifact", {
14 | from: deployer,
15 | args: [reliquary.address],
16 | log: true,
17 | skipIfAlreadyDeployed: true,
18 | });
19 | const token = await ethers.getContract("AttendanceArtifact");
20 |
21 | await deploy("AttendanceProver", {
22 | from: deployer,
23 | args: [reliquary.address, token.address],
24 | log: true,
25 | });
26 | };
27 |
28 | module.exports.tags = ["Attendance"];
29 | module.exports.dependencies = ["Reliquary"];
30 |
--------------------------------------------------------------------------------
/contracts/test/BlockHashMessengerForTesting.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.0;
6 |
7 | import "../interfaces/IProxyBlockHistory.sol";
8 |
9 | /**
10 | * @title BlockHashMessengerForTesting
11 | * @author Theori, Inc.
12 | * @notice Accepts arbitrary merkle roots and sends them to an L2 BlockHistory.
13 | * This contract is abstract so different message passing logic can be
14 | * impelemented for each L2.
15 | */
16 | abstract contract BlockHashMessengerForTesting {
17 | function _sendMessage(
18 | address destination,
19 | bytes calldata params,
20 | bytes memory message
21 | ) internal virtual;
22 |
23 | function sendBlockHashForTesting(
24 | address destination,
25 | bytes calldata params,
26 | uint256 number,
27 | bytes32 blockHash
28 | ) external payable {
29 | _sendMessage(
30 | destination,
31 | params,
32 | abi.encodeWithSelector(IProxyBlockHistory.importTrustedHash.selector, number, blockHash)
33 | );
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/contracts/interfaces/IERC5192.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.12;
6 |
7 | /**
8 | * @title EIP-5192 specification
9 | * @author Theori, Inc.
10 | * @notice EIP-5192 events and functions
11 | */
12 | interface IERC5192 {
13 | /// @notice Emitted when the locking status is changed to locked.
14 | /// @dev If a token is minted and the status is locked, this event should be emitted.
15 | /// @param tokenId The identifier for a token.
16 | event Locked(uint256 tokenId);
17 |
18 | /// @notice Emitted when the locking status is changed to unlocked.
19 | /// @dev If a token is minted and the status is unlocked, this event should be emitted.
20 | /// @param tokenId The identifier for a token.
21 | event Unlocked(uint256 tokenId);
22 |
23 | /// @notice Returns the locking status of an Soulbound Token
24 | /// @dev SBTs assigned to zero address are considered invalid, and queries
25 | /// about them do throw.
26 | /// @param tokenId The identifier for an SBT.
27 | function locked(uint256 tokenId) external view returns (bool);
28 | }
29 |
--------------------------------------------------------------------------------
/contracts/interfaces/IBeaconBlockHistory.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.0;
6 |
7 | import "./IBlockHistory.sol";
8 |
9 | /**
10 | * @title Beacon Block history provider
11 | * @author Theori, Inc.
12 | * @notice IBeaconBlockHistory provides a way to verify beacon block roots as well as execution block hashes
13 | */
14 |
15 | interface IBeaconBlockHistory is IBlockHistory {
16 | function UPGRADE_BLOCK() external view returns (uint256 blockNum);
17 |
18 | /**
19 | * @notice verifies a beacon block root
20 | * @param proof the proof of the beacon blcok
21 | * @return blockRoot the `BeaconBlock` root
22 | */
23 | function verifyBeaconBlockRoot(
24 | bytes calldata proof
25 | ) external view returns (bytes32 blockRoot);
26 |
27 | /**
28 | * @notice gets the cached block summary for the given slot (if it exists)
29 | * @param slot the slot number to query
30 | * @return result the cached block summary (or bytes32(0) if it is not cached)
31 | */
32 | function getBlockSummary(uint256 slot) external view returns (bytes32 result);
33 | }
34 |
--------------------------------------------------------------------------------
/contracts/lib/Propogate.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.0;
6 |
7 | library Propogate {
8 | /**
9 | * @notice propogates the current calldata to the destination
10 | * via a staticcall() and returns or reverts accordingly
11 | * @dev this is much cheaper than manually building the calldata again
12 | */
13 | function staticcall(address destination) internal view {
14 | assembly {
15 | // we are not returning to solidity, so we can take ownership of all memory
16 | calldatacopy(0, 0, calldatasize())
17 | let success := staticcall(gas(), destination, 0, calldatasize(), 0, 0)
18 | returndatacopy(0, 0, returndatasize())
19 | // Depending on the success, either revert or return
20 | switch success
21 | case 0 {
22 | // End execution and revert state changes
23 | revert(0, returndatasize())
24 | }
25 | default {
26 | // Return data with length of size at pointers position
27 | return(0, returndatasize())
28 | }
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/contracts/tokens/MockRelic.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.12;
6 |
7 | import "../RelicToken.sol";
8 | import "../Reliquary.sol";
9 |
10 | contract MockRelic is RelicToken {
11 | FactSignature immutable factSig;
12 | Reliquary immutable reliquary;
13 |
14 | constructor(
15 | uint8 factCls,
16 | bytes memory factDesc,
17 | Reliquary _reliquary
18 | ) RelicToken() {
19 | require(block.chainid == 31337, "testing only");
20 | factSig = Facts.toFactSignature(factCls, factDesc);
21 | reliquary = _reliquary;
22 | }
23 |
24 | function hasToken(address owner, uint96 data) internal view override returns (bool result) {
25 | require(data == 0);
26 | (result, , ) = reliquary.verifyFactNoFee(owner, factSig);
27 | }
28 |
29 | function name() external pure override returns (string memory) {
30 | return "Mock Relic";
31 | }
32 |
33 | function symbol() external pure override returns (string memory) {
34 | return "MOCK";
35 | }
36 |
37 | function tokenURI(
38 | uint256 /* tokenID */
39 | ) external pure override returns (string memory) {
40 | return "";
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/contracts/test/ZkSyncBlockHashMessengerForTesting.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity ^0.8.0;
6 |
7 | import "@matterlabs/zksync-contracts/l1/contracts/zksync/interfaces/IMailbox.sol";
8 | import "./BlockHashMessengerForTesting.sol";
9 |
10 | /**
11 | * @title ZkSyncBlockHashMessengerForTesting
12 | * @author Theori, Inc.
13 | * @notice The L1 messenger contract to send block history merkle roots to zkSync.
14 | */
15 | contract ZkSyncBlockHashMessengerForTesting is BlockHashMessengerForTesting {
16 | address public immutable zkSync;
17 |
18 | constructor(address _zkSync) {
19 | zkSync = _zkSync;
20 | }
21 |
22 | function _sendMessage(
23 | address destination,
24 | bytes calldata params,
25 | bytes memory data
26 | ) internal override {
27 | (uint256 l2GasLimit, uint256 l2GasPerPubdataByteLimit) = abi.decode(
28 | params,
29 | (uint256, uint256)
30 | );
31 | IMailbox(zkSync).requestL2Transaction{value: msg.value}(
32 | destination,
33 | 0,
34 | data,
35 | l2GasLimit,
36 | l2GasPerPubdataByteLimit,
37 | new bytes[](0),
38 | msg.sender
39 | );
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/deploy/01_verifiers.js:
--------------------------------------------------------------------------------
1 | const fetch = require('node-fetch');
2 | const { config, ethers, network } = require("hardhat");
3 | const { readFileSync } = require("fs");
4 |
5 | module.exports = async ({getNamedAccounts, deployments}) => {
6 | if (network.config.bridged === true || network.config.l2Native === true) {
7 | return;
8 | }
9 |
10 | const {deploy} = deployments;
11 | const {deployer} = await getNamedAccounts();
12 |
13 | // deploy all the verifiers
14 | const sizes = config.relic.vkSizes;
15 | for (var i = 0; i < sizes.length; i++) {
16 | let vkSize = sizes[i] / 16;
17 | let vkRaw = readFileSync(`test/data/rendered-vk-outer-${sizes[i] / 16}`);
18 | const VK_LENGTH = 35;
19 | if (vkRaw.length != VK_LENGTH * 32) {
20 | throw `rendered-vk-outer-${vkSize} has incorret size`;
21 | }
22 |
23 | // convert vk to uint256[VK_LENGTH]
24 | let vk = [];
25 | for (var j = 0; j < VK_LENGTH; j++) {
26 | vk.push(ethers.BigNumber.from(vkRaw.slice(j * 32, j * 32 + 32)));
27 | }
28 |
29 | await deploy(`Verifier-${sizes[i]}`, {
30 | contract: "contracts/Verifier.yul:Verifier",
31 | from: deployer,
32 | args: [vk],
33 | log: true,
34 | });
35 | }
36 | };
37 | module.exports.tags = ["Verifiers"];
38 |
--------------------------------------------------------------------------------
/contracts/ZkSyncBlockHashMessenger.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity ^0.8.0;
6 |
7 | import "@matterlabs/zksync-contracts/l1/contracts/zksync/interfaces/IMailbox.sol";
8 | import "./interfaces/IReliquary.sol";
9 | import "./BlockHashMessenger.sol";
10 |
11 | /**
12 | * @title ZkSyncBlockHashMessenger
13 | * @author Theori, Inc.
14 | * @notice The L1 messenger contract to send block hashes to zkSync.
15 | */
16 | contract ZkSyncBlockHashMessenger is BlockHashMessenger {
17 | address public immutable zkSync;
18 |
19 | constructor(
20 | IReliquary _reliquary,
21 | address _blockHistory,
22 | address _zkSync
23 | ) BlockHashMessenger(_reliquary, _blockHistory) {
24 | zkSync = _zkSync;
25 | }
26 |
27 | function _sendMessage(
28 | address destination,
29 | bytes calldata params,
30 | bytes memory data
31 | ) internal override {
32 | (uint256 l2GasLimit, uint256 l2GasPerPubdataByteLimit) = abi.decode(
33 | params,
34 | (uint256, uint256)
35 | );
36 | IMailbox(zkSync).requestL2Transaction{value: msg.value}(
37 | destination,
38 | 0,
39 | data,
40 | l2GasLimit,
41 | l2GasPerPubdataByteLimit,
42 | new bytes[](0),
43 | msg.sender
44 | );
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/contracts/optimism/OptimismProxyBlockHistory.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity ^0.8.0;
6 |
7 | import "@eth-optimism/contracts/standards/AddressAliasHelper.sol";
8 | import "../ProxyBlockHistory.sol";
9 |
10 | interface IL1Block {
11 | function hash() external view returns (bytes32);
12 | function number() external view returns (uint64);
13 | }
14 |
15 | /**
16 | * @title OptimismProxyBlockHistory
17 | * @author Theori, Inc.
18 | * @notice A wrapper around ProxyBlockHistory which translates the messenger address
19 | * according to the Optimism aliasing rules and allows direct committing of
20 | * the accessible L1 block data.
21 | */
22 | contract OptimismProxyBlockHistory is ProxyBlockHistory {
23 | constructor(
24 | address reliquary,
25 | address messenger,
26 | address l1BlockHistory,
27 | bytes32 merkleRootsSlot
28 | )
29 | ProxyBlockHistory(
30 | reliquary,
31 | AddressAliasHelper.applyL1ToL2Alias(messenger),
32 | l1BlockHistory,
33 | merkleRootsSlot
34 | )
35 | {}
36 |
37 | function commitCurrentL1BlockHash() external {
38 | // https://community.optimism.io/docs/protocol/protocol-2.0/#l1block
39 | IL1Block l1Block = IL1Block(0x4200000000000000000000000000000000000015);
40 | _importTrustedHash(uint256(l1Block.number()), l1Block.hash());
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "relic",
3 | "devDependencies": {
4 | "@matterlabs/hardhat-zksync-deploy": "^0.6.3",
5 | "@matterlabs/hardhat-zksync-solc": "0.3.14",
6 | "@matterlabs/hardhat-zksync-verify": "^0.2.0",
7 | "@matterlabs/zksync-contracts": "^0.6.1",
8 | "@nomicfoundation/hardhat-network-helpers": "^1.0.8",
9 | "@nomiclabs/hardhat-ethers": "npm:hardhat-deploy-ethers@^0.3.0-beta.13",
10 | "@relicprotocol/client": "^0.1.1",
11 | "chai": "^4.3.6",
12 | "ethereum-waffle": "^3.4.4",
13 | "hardhat-gas-reporter": "^1.0.8",
14 | "solc": ">=0.8.12",
15 | "solidity-coverage": "^0.7.21"
16 | },
17 | "dependencies": {
18 | "@chainsafe/persistent-merkle-tree": "^0.6.1",
19 | "@eth-optimism/contracts": "^0.6.0",
20 | "@eth-optimism/sdk": "^3.1.6",
21 | "@lodestar/api": "^1.15.0",
22 | "@lodestar/types": "^1.15.0",
23 | "@nomiclabs/hardhat-waffle": "^2.0.6",
24 | "@openzeppelin/contracts": "^4.7.3",
25 | "core-js": "^3.25.0",
26 | "dotenv": "^16.0.2",
27 | "ethers": "^5.7.2",
28 | "flat-cache": "^3.0.4",
29 | "hardhat": "^2.16.1",
30 | "hardhat-deploy": "v0.11.45",
31 | "node-fetch": "^2.6.7",
32 | "rlp": "^3.0.0",
33 | "sqlite3": "^5.0.11"
34 | },
35 | "overrides": {
36 | "@nomiclabs/hardhat-waffle": {
37 | "@nomiclabs/hardhat-ethers": "npm:hardhat-deploy-ethers@^0.3.0-beta.13"
38 | },
39 | "@matterlabs/zksync-contracts": {
40 | "@openzeppelin/contracts": "^4.7.3"
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/contracts/lib/AuxMerkleTree.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.0;
6 |
7 | import "./AnemoiJive.sol";
8 |
9 | /**
10 | * @title Auxiliary Merkle Tree
11 | * @author Theori, Inc.
12 | * @notice Gas optimized arithmetic-friendly merkle tree code.
13 | * @dev uses Anemoi / Jive 2-to-1
14 | */
15 | library AuxMerkleTree {
16 | /**
17 | * @notice computes a jive merkle root of the provided hashes, in place
18 | * @param temp the mutable array of hashes
19 | * @return root the merkle root hash
20 | */
21 | function computeRoot(bytes32[] memory temp) internal view returns (bytes32 root) {
22 | uint256 count = temp.length;
23 | while (count > 1) {
24 | unchecked {
25 | for (uint256 i = 0; i < count / 2; i++) {
26 | uint256 x;
27 | uint256 y;
28 | assembly {
29 | let ptr := add(temp, add(0x20, mul(0x40, i)))
30 | x := mload(ptr)
31 | ptr := add(ptr, 0x20)
32 | y := mload(ptr)
33 | }
34 | x = AnemoiJive.compress(x, y);
35 | assembly {
36 | mstore(add(temp, add(0x20, mul(0x20, i))), x)
37 | }
38 | }
39 | count >>= 1;
40 | }
41 | }
42 | return temp[0];
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/contracts/provers/MockProver.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.12;
6 |
7 | import "./Prover.sol";
8 | import "../RelicToken.sol";
9 | import "../interfaces/IReliquary.sol";
10 |
11 | contract MockProver is Prover {
12 | FactSignature public immutable factSig;
13 | RelicToken immutable token;
14 |
15 | struct MockProof {
16 | address account;
17 | uint48 blockNum;
18 | }
19 |
20 | constructor(
21 | uint8 factCls,
22 | bytes memory factDesc,
23 | IReliquary _reliquary,
24 | RelicToken _token
25 | ) Prover(_reliquary) {
26 | require(block.chainid == 31337, "testing only");
27 | factSig = Facts.toFactSignature(factCls, factDesc);
28 | token = _token;
29 | }
30 |
31 | function parseMockProof(bytes calldata proof) internal pure returns (MockProof calldata res) {
32 | assembly {
33 | res := proof.offset
34 | }
35 | }
36 |
37 | function _prove(bytes calldata encodedProof) internal view override returns (Fact memory) {
38 | MockProof memory proof = parseMockProof(encodedProof);
39 | bytes memory data = abi.encodePacked(proof.blockNum);
40 | return Fact(proof.account, factSig, data);
41 | }
42 |
43 | function _afterStore(Fact memory fact, bool alreadyStored) internal override {
44 | if (!alreadyStored) {
45 | token.mint(fact.account, 0);
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/contracts/tokens/BirthCertificateRelic.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.12;
6 |
7 | import "@openzeppelin/contracts/access/Ownable.sol";
8 |
9 | import "../RelicTokenConfigurable.sol";
10 | import "../Reliquary.sol";
11 |
12 | /**
13 | * @title Birth Certificate Relic Token
14 | * @author Theori, Inc.
15 | * @notice Configurable soul-bound tokens issued to show an account's
16 | * birth certificate
17 | */
18 | contract BirthCertificateRelic is RelicTokenConfigurable {
19 | Reliquary immutable reliquary;
20 | FactSignature immutable BIRTH_CERTIFICATE_SIG;
21 |
22 | constructor(Reliquary _reliquary) RelicToken() Ownable() {
23 | BIRTH_CERTIFICATE_SIG = Facts.toFactSignature(Facts.NO_FEE, abi.encode("BirthCertificate"));
24 | reliquary = _reliquary;
25 | }
26 |
27 | /**
28 | * @inheritdoc RelicToken
29 | * @dev Do not validate data as it may contain URI provider information
30 | */
31 | function hasToken(
32 | address who,
33 | uint96 /* data */
34 | ) internal view override returns (bool result) {
35 | (result, ) = reliquary.verifyFactVersionNoFee(who, BIRTH_CERTIFICATE_SIG);
36 | }
37 |
38 | /// @inheritdoc IERC721Metadata
39 | function name() external pure override returns (string memory) {
40 | return "Birth Certificate Relic";
41 | }
42 |
43 | /// @inheritdoc IERC721Metadata
44 | function symbol() external pure override returns (string memory) {
45 | return "BCR";
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/contracts/OptimismBlockHashMessenger.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity ^0.8.0;
6 |
7 | import "./interfaces/IReliquary.sol";
8 | import "./BlockHashMessenger.sol";
9 |
10 | // OptimismPortal low-level L1->L2 messaging interface
11 | interface IPortal {
12 | function depositTransaction(
13 | address _to,
14 | uint256 _value,
15 | uint64 _gasLimit,
16 | bool _isCreation,
17 | bytes calldata _data
18 | ) external payable;
19 | }
20 |
21 | /**
22 | * @title OptimismBlockHashMessenger
23 | * @author Theori, Inc.
24 | * @notice The L1 messenger contract to send block hashes to Optimism.
25 | * Also works for compatible Optimism forks, such as Base.
26 | */
27 | contract OptimismBlockHashMessenger is BlockHashMessenger {
28 | address public immutable portal;
29 |
30 | constructor(
31 | IReliquary _reliquary,
32 | address _blockHistory,
33 | address _portal
34 | ) BlockHashMessenger(_reliquary, _blockHistory) {
35 | portal = _portal;
36 | }
37 |
38 | function _sendMessage(
39 | address destination,
40 | bytes calldata params,
41 | bytes memory data
42 | ) internal override {
43 | uint64 gasLimit = abi.decode(params, (uint64));
44 | require(msg.value == 0, "Optimism messaging does not pay fee via msg.value");
45 | IPortal(portal).depositTransaction(
46 | destination,
47 | 0,
48 | gasLimit,
49 | false,
50 | data
51 | );
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/contracts/provers/BlockHeaderProver.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.12;
6 |
7 | import "../interfaces/IReliquary.sol";
8 | import "../lib/CoreTypes.sol";
9 | import "../lib/FactSigs.sol";
10 | import "./StateVerifier.sol";
11 | import "./Prover.sol";
12 |
13 | /**
14 | * @title BlockHeaderProver
15 | * @author Theori, Inc.
16 | * @notice BlockHeaderProver proves that a block header is valid and included in the current chain
17 | */
18 | contract BlockHeaderProver is Prover, StateVerifier {
19 | constructor(address blockHistory, IReliquary _reliquary)
20 | Prover(_reliquary)
21 | StateVerifier(blockHistory, _reliquary)
22 | {}
23 |
24 | struct BlockHeaderProof {
25 | bytes header;
26 | bytes blockProof;
27 | }
28 |
29 | function parseBlockHeaderProof(bytes calldata proof)
30 | internal
31 | pure
32 | returns (BlockHeaderProof calldata res)
33 | {
34 | assembly {
35 | res := proof.offset
36 | }
37 | }
38 |
39 | /**
40 | * @notice Proves that a block header was valid
41 | *
42 | * @param encodedProof the encoded BlockHeaderProof
43 | */
44 | function _prove(bytes calldata encodedProof) internal view override returns (Fact memory) {
45 | BlockHeaderProof calldata proof = parseBlockHeaderProof(encodedProof);
46 | CoreTypes.BlockHeaderData memory head = verifyBlockHeader(proof.header, proof.blockProof);
47 | return Fact(address(0), FactSigs.blockHeaderSig(head.Number), abi.encode(head));
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/contracts/BlockHashMessenger.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.0;
6 |
7 | import "./lib/MerkleTree.sol";
8 | import "./interfaces/IReliquary.sol";
9 | import "./interfaces/IProxyBlockHistory.sol";
10 |
11 | /**
12 | * @title BlockHashMessenger
13 | * @author Theori, Inc.
14 | * @notice Sends a block hash verified by the L1 BlockHistory and sends them to
15 | * a ProxyBlockHistory. This contract is abstract so different message
16 | * passing logic can be impelemented for each L2 / side chain.
17 | */
18 | abstract contract BlockHashMessenger {
19 | IReliquary public immutable reliquary;
20 | address public immutable blockHistory;
21 |
22 | constructor(IReliquary _reliquary, address _blockHistory) {
23 | reliquary = _reliquary;
24 | blockHistory = _blockHistory;
25 | }
26 |
27 | function _sendMessage(
28 | address destination,
29 | bytes calldata params,
30 | bytes memory message
31 | ) internal virtual;
32 |
33 | function sendBlockHash(
34 | address destination,
35 | bytes calldata params,
36 | uint256 number,
37 | bytes32 blockHash,
38 | bytes calldata proof
39 | ) external payable {
40 | require(
41 | reliquary.validBlockHash(blockHistory, blockHash, number, proof),
42 | "Invalid block hash"
43 | );
44 | _sendMessage(
45 | destination,
46 | params,
47 | abi.encodeWithSelector(IProxyBlockHistory.importTrustedHash.selector, number, blockHash)
48 | );
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/contracts/tokens/AttendanceArtifact.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.12;
6 |
7 | import "@openzeppelin/contracts/access/Ownable.sol";
8 |
9 | import "../lib/Facts.sol";
10 | import "../RelicTokenConfigurable.sol";
11 | import "../Reliquary.sol";
12 | import "../interfaces/ITokenURI.sol";
13 |
14 | /**
15 | * @title Attendance Artifact Token
16 | * @author Theori, Inc.
17 | * @notice Configurable soul-bound tokens issued only to those that can prove they
18 | * attended events.
19 | */
20 | contract AttendanceArtifact is RelicTokenConfigurable {
21 | Reliquary immutable reliquary;
22 |
23 | constructor(Reliquary _reliquary) RelicToken() Ownable() {
24 | reliquary = _reliquary;
25 | }
26 |
27 | /**
28 | * @inheritdoc RelicToken
29 | * @dev High 32 bits of data are URI provider information that is ignored here
30 | */
31 | function hasToken(address who, uint96 data) internal view override returns (bool result) {
32 | uint64 eventId = uint64(data);
33 | FactSignature sig = Facts.toFactSignature(
34 | Facts.NO_FEE,
35 | abi.encode("EventAttendance", "EventID", eventId)
36 | );
37 | (result, ) = reliquary.verifyFactVersionNoFee(who, sig);
38 | }
39 |
40 | /// @inheritdoc IERC721Metadata
41 | function name() external pure override returns (string memory) {
42 | return "Attendance Artifact";
43 | }
44 |
45 | /// @inheritdoc IERC721Metadata
46 | function symbol() external pure override returns (string memory) {
47 | return "RAA";
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/contracts/tokens/MockURIProvider.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.12;
6 |
7 | import "@openzeppelin/contracts/utils/Base64.sol";
8 |
9 | import "../interfaces/ITokenURI.sol";
10 |
11 | contract MockTokenURI is ITokenURI {
12 | constructor() {
13 | require(block.chainid == 31337, "testing only");
14 | }
15 |
16 | function tokenURI(uint256) external pure returns (string memory) {
17 | return
18 | string(
19 | abi.encodePacked(
20 | "data:application/json;base64,",
21 | Base64.encode(
22 | bytes(
23 | abi.encodePacked(
24 | '{"name":"',
25 | "MockEvent",
26 | '", "description":"',
27 | "token description for testing",
28 | '", "image": "data:image/svg+xml;base64,',
29 | Base64.encode(
30 | bytes(
31 | abi.encodePacked(
32 | ''
33 | )
34 | )
35 | ),
36 | '"}'
37 | )
38 | )
39 | )
40 | )
41 | );
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/contracts/optimism/interfaces/IOptimismNativeBlockHistory.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.0;
6 |
7 | import "../../interfaces/IBlockHistory.sol";
8 |
9 | /**
10 | * @title Optimism native block history provider
11 | * @author Theori, Inc.
12 | */
13 |
14 | interface IOptimismNativeBlockHistory is IBlockHistory {
15 | // https://github.com/ethereum-optimism/optimism/blob/65ec61dde94ffa93342728d324fecf474d228e1f/packages/contracts-bedrock/contracts/libraries/Types.sol#L33
16 | /**
17 | * @notice Struct representing the elements that are hashed together to generate an output root
18 | * which itself represents a snapshot of the L2 state.
19 | *
20 | * @custom:field version Version of the output root.
21 | * @custom:field stateRoot Root of the state trie at the block of this output.
22 | * @custom:field messagePasserStorageRoot Root of the message passer storage trie.
23 | * @custom:field latestBlockhash Hash of the block this output was generated from.
24 | */
25 | struct OutputRootProof {
26 | bytes32 version;
27 | bytes32 stateRoot;
28 | bytes32 messagePasserStorageRoot;
29 | bytes32 latestBlockhash;
30 | }
31 |
32 | function proxyMultiStorageSlotProver() external view returns (address);
33 | function l2OutputOracle() external view returns (address);
34 | function OUTPUT_ROOTS_BASE_SLOT() external view returns (bytes32);
35 | function FINALIZATION_PERIOD_SECONDS() external view returns (uint256);
36 |
37 | function importCheckpointBlockFromL1(
38 | bytes calldata proof,
39 | uint256 index,
40 | uint256 l1BlockNumber,
41 | OutputRootProof calldata outputRootProof
42 | ) external;
43 | }
44 |
--------------------------------------------------------------------------------
/contracts/zksync/ZkSyncProxyBeaconBlockHistory.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity ^0.8.0;
6 |
7 | import "@matterlabs/zksync-contracts/l2/contracts/vendor/AddressAliasHelper.sol";
8 | import "../ProxyBeaconBlockHistory.sol";
9 |
10 | interface IL1Block {
11 | function hash() external view returns (bytes32);
12 | function number() external view returns (uint64);
13 | }
14 |
15 | /**
16 | * @title ZkSyncProxyBeaconBlockHistory
17 | * @author Theori, Inc.
18 | * @notice A wrapper around ProxyBeaconBlockHistory which translates the messenger address
19 | * according to the Optimism aliasing rules and allows direct committing of
20 | * the accessible L1 block data.
21 | */
22 | contract ZkSyncProxyBeaconBlockHistory is ProxyBeaconBlockHistory {
23 | address public immutable messenger;
24 |
25 | constructor(
26 | address _messenger,
27 | address _reliquary,
28 | address _preDencunBlockHistory,
29 | uint256 _CAPELLA_SLOT,
30 | uint256 _DENCUN_SLOT,
31 | uint256 _UPGRADE_BLOCK
32 | )
33 | ProxyBeaconBlockHistory(
34 | _reliquary,
35 | _preDencunBlockHistory,
36 | _CAPELLA_SLOT,
37 | _DENCUN_SLOT,
38 | _UPGRADE_BLOCK
39 | )
40 | {
41 | messenger = AddressAliasHelper.applyL1ToL2Alias(_messenger);
42 | }
43 |
44 | /**
45 | * @notice Import a trusted block hash from the messenger
46 | * @param number the block number to import
47 | * @param hash the block hash
48 | */
49 | function importTrustedHash(uint256 number, bytes32 hash) external {
50 | require(msg.sender == messenger, "only the L1 messenger can import trusted block hashes");
51 | _storeCommittedBlock(number, hash);
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/contracts/provers/LogProver.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.12;
6 |
7 | import "../interfaces/IReliquary.sol";
8 | import "../lib/FactSigs.sol";
9 | import "../RelicToken.sol";
10 | import "./Prover.sol";
11 | import "./StateVerifier.sol";
12 |
13 | /**
14 | * @title LogProver
15 | * @author Theori, Inc.
16 | * @notice LogProver proves that log events occured in some block.
17 | */
18 | contract LogProver is Prover, StateVerifier {
19 | constructor(address blockHistory, IReliquary _reliquary)
20 | Prover(_reliquary)
21 | StateVerifier(blockHistory, _reliquary)
22 | {}
23 |
24 | struct LogProof {
25 | uint256 txIdx;
26 | uint256 logIdx;
27 | bytes receiptProof;
28 | bytes header;
29 | bytes blockProof;
30 | }
31 |
32 | function parseLogProof(bytes calldata proof) internal pure returns (LogProof calldata res) {
33 | assembly {
34 | res := proof.offset
35 | }
36 | }
37 |
38 | /**
39 | * @notice Proves that a log occured in a block.
40 | *
41 | * @param encodedProof the encoded LogProof
42 | */
43 | function _prove(bytes calldata encodedProof) internal view override returns (Fact memory) {
44 | LogProof calldata proof = parseLogProof(encodedProof);
45 | (CoreTypes.BlockHeaderData memory head, CoreTypes.LogData memory log) = verifyLogAtBlock(
46 | proof.txIdx,
47 | proof.logIdx,
48 | proof.receiptProof,
49 | proof.header,
50 | proof.blockProof
51 | );
52 | return
53 | Fact(
54 | log.Address,
55 | FactSigs.logFactSig(head.Number, proof.txIdx, proof.logIdx),
56 | abi.encode(log)
57 | );
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/contracts/test/RelicReceiverForTesting.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 | pragma solidity >=0.8.0;
5 |
6 | import "../lib/CoreTypes.sol";
7 | import "../lib/Facts.sol";
8 | import "../lib/FactSigs.sol";
9 | import "../interfaces/IRelicReceiver.sol";
10 | import "../interfaces/IEphemeralFacts.sol";
11 |
12 | /**
13 | * @title RelicReceiverForTesting
14 | * @author Theori, Inc.
15 | * @notice Simple implementation of IRelicReciever, expects extra data to be the fact
16 | * signature data, and emits logs for each fact received.
17 | */
18 | contract RelicReceiverForTesting is IRelicReceiver {
19 | IEphemeralFacts immutable ephemeralFacts;
20 |
21 | constructor(IEphemeralFacts _ephemeralFacts) {
22 | ephemeralFacts = _ephemeralFacts;
23 | }
24 |
25 | event FactReceived(address initiator, string name);
26 |
27 | /**
28 | * @notice receives an ephemeral fact from Relic
29 | * @param initiator the account which initiated the fact proving
30 | * @param fact the proven fact information
31 | * @param data extra data passed from the initiator - this data may come
32 | * from untrusted parties and thus should be validated
33 | */
34 | function receiveFact(
35 | address initiator,
36 | Fact calldata fact,
37 | bytes calldata data
38 | ) external {
39 | require(msg.sender == address(ephemeralFacts), "only EphemeralFacts can call receiveFact");
40 |
41 | FactSignature computedSig = Facts.toFactSignature(Facts.NO_FEE, data);
42 | require(
43 | FactSignature.unwrap(fact.sig) == FactSignature.unwrap(computedSig),
44 | "extra data does not match fact signature"
45 | );
46 |
47 | string memory name = abi.decode(data, (string));
48 | emit FactReceived(initiator, name);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/contracts/provers/TransactionProver.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.12;
6 |
7 | import "../interfaces/IReliquary.sol";
8 | import "../lib/FactSigs.sol";
9 | import "../RelicToken.sol";
10 | import "./Prover.sol";
11 | import "./StateVerifier.sol";
12 |
13 | /**
14 | * @title TransactionProver
15 | * @author Theori, Inc.
16 | * @notice TransactionProver proves that a transaction hash occurred in a block.
17 | */
18 | contract TransactionProver is Prover, StateVerifier {
19 | constructor(address blockHistory, IReliquary _reliquary)
20 | Prover(_reliquary)
21 | StateVerifier(blockHistory, _reliquary)
22 | {}
23 |
24 | struct TransactionProof {
25 | uint256 txIdx;
26 | bytes transactionProof;
27 | bytes header;
28 | bytes blockProof;
29 | }
30 |
31 | function parseTransactionProof(bytes calldata proof)
32 | internal
33 | pure
34 | returns (TransactionProof calldata res)
35 | {
36 | assembly {
37 | res := proof.offset
38 | }
39 | }
40 |
41 | /**
42 | * @notice Proves that a log occured in a block.
43 | *
44 | * @param encodedProof the encoded TransactionProof
45 | */
46 | function _prove(bytes calldata encodedProof) internal view override returns (Fact memory) {
47 | TransactionProof calldata proof = parseTransactionProof(encodedProof);
48 | (CoreTypes.BlockHeaderData memory head, bytes32 transaction) = verifyTransactionAtBlock(
49 | proof.txIdx,
50 | proof.transactionProof,
51 | proof.header,
52 | proof.blockProof
53 | );
54 | return
55 | Fact(
56 | address(0),
57 | FactSigs.transactionFactSig(transaction),
58 | abi.encode(head.Number, proof.txIdx)
59 | );
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/contracts/provers/Prover.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.12;
6 |
7 | import "@openzeppelin/contracts/utils/introspection/ERC165.sol";
8 |
9 | import "../interfaces/IReliquary.sol";
10 | import "../interfaces/IProver.sol";
11 |
12 | abstract contract Prover is ERC165, IProver {
13 | IReliquary immutable reliquary;
14 |
15 | constructor(IReliquary _reliquary) {
16 | reliquary = _reliquary;
17 | }
18 |
19 | event FactProven(Fact fact);
20 |
21 | // must implemented by each prover
22 | function _prove(bytes calldata proof) internal view virtual returns (Fact memory);
23 |
24 | // can optionally be overridden by each prover
25 | function _afterStore(Fact memory fact, bool alreadyStored) internal virtual {}
26 |
27 | /**
28 | * @notice proves a fact ephemerally and returns the fact information
29 | * @param proof the encoded proof for this prover
30 | * @param store whether to store the fact in the reqliquary
31 | */
32 | function prove(bytes calldata proof, bool store) public payable returns (Fact memory fact) {
33 | reliquary.checkProveFactFee{value: msg.value}(msg.sender);
34 | fact = _prove(proof);
35 | emit FactProven(fact);
36 | if (store) {
37 | (bool alreadyStored, , ) = reliquary.getFact(fact.account, fact.sig);
38 | reliquary.setFact(fact.account, fact.sig, fact.data);
39 | _afterStore(fact, alreadyStored);
40 | }
41 | }
42 |
43 | /**
44 | * @inheritdoc IERC165
45 | * @dev Supported interfaces: IProver
46 | */
47 | function supportsInterface(bytes4 interfaceId)
48 | public
49 | view
50 | virtual
51 | override(ERC165)
52 | returns (bool)
53 | {
54 | return (interfaceId == type(IProver).interfaceId || super.supportsInterface(interfaceId));
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/contracts/provers/AccountStorageProver.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.12;
6 |
7 | import "../interfaces/IReliquary.sol";
8 | import "../RelicToken.sol";
9 | import "./Prover.sol";
10 | import "./StateVerifier.sol";
11 | import "../lib/FactSigs.sol";
12 |
13 | /**
14 | * @title AccountStorageProver
15 | * @author Theori, Inc.
16 | * @notice AccountStorageProver proves an account's storage root at a particular block
17 | */
18 | contract AccountStorageProver is Prover, StateVerifier {
19 | constructor(address blockHistory, IReliquary _reliquary)
20 | Prover(_reliquary)
21 | StateVerifier(blockHistory, _reliquary)
22 | {}
23 |
24 | struct AccountStorageProof {
25 | address account;
26 | bytes accountProof;
27 | bytes header;
28 | bytes blockProof;
29 | }
30 |
31 | function parseAccountStorageProof(bytes calldata proof)
32 | internal
33 | pure
34 | returns (AccountStorageProof calldata res)
35 | {
36 | assembly {
37 | res := proof.offset
38 | }
39 | }
40 |
41 | /**
42 | * @notice Proves that a storage slot had a particular value at a particular block.
43 | *
44 | * @param encodedProof the encoded AccountStorageProof
45 | */
46 | function _prove(bytes calldata encodedProof) internal view override returns (Fact memory) {
47 | AccountStorageProof calldata proof = parseAccountStorageProof(encodedProof);
48 | (
49 | bool exists,
50 | CoreTypes.BlockHeaderData memory head,
51 | CoreTypes.AccountData memory acc
52 | ) = verifyAccountAtBlock(proof.account, proof.accountProof, proof.header, proof.blockProof);
53 | require(exists, "Account does not exist at block");
54 |
55 | return
56 | Fact(proof.account, FactSigs.accountStorageFactSig(head.Number, acc.StorageRoot), "");
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/contracts/lib/BaseFeeStats.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | /*
6 | * @author Theori, Inc.
7 | */
8 |
9 | pragma solidity >=0.8.0;
10 |
11 | struct BaseFeeStats {
12 | uint256 cumulative_1;
13 | uint256 cumulative_2;
14 | uint256 cumulative_3;
15 | uint256 cumulative_4;
16 | }
17 |
18 | using BaseFeeStatsOps for BaseFeeStats global;
19 |
20 | library BaseFeeStatsOps {
21 | function eq(BaseFeeStats calldata a, BaseFeeStats calldata b) internal pure returns (bool) {
22 | return (a.cumulative_1 == b.cumulative_1 &&
23 | a.cumulative_2 == b.cumulative_2 &&
24 | a.cumulative_3 == b.cumulative_3 &&
25 | a.cumulative_4 == b.cumulative_4);
26 | }
27 | }
28 |
29 | /**
30 | * @notice Small hack to compute sizeof(BaseFeeStats)
31 | * @dev avoids the need to hardcode this size
32 | * @dev note that this only works because BaseFeeStats is statically sized
33 | * @return size
34 | */
35 | function baseFeeStatsSize() pure returns (uint256 size) {
36 | BaseFeeStats[2] calldata fakeArr;
37 | assembly {
38 | fakeArr := 0
39 | }
40 | BaseFeeStats calldata fake = fakeArr[1];
41 | assembly {
42 | size := fake
43 | }
44 | }
45 |
46 | /**
47 | * @notice reads a BaseFeeStats from its encoded form
48 | * @param words the hash words
49 | * @return result the parsed base fee stats
50 | */
51 | function readBaseFeeStats(uint256[] calldata words) pure returns (BaseFeeStats memory result) {
52 | result = BaseFeeStats(words[0], words[1], words[2], words[3]);
53 | }
54 |
55 | /**
56 | * @notice hashes the base fee stats struct as is done in the ZK-circuits
57 | * @param stats the input stats
58 | * @return hash the resulting hash
59 | */
60 | function hashBaseFeeStats(BaseFeeStats memory stats) view returns (bytes32 hash) {
61 | uint256 size = baseFeeStatsSize();
62 | assembly {
63 | if iszero(staticcall(gas(), 0x2, stats, size, 0x0, 0x20)) {
64 | revert(0, 0)
65 | }
66 | hash := mload(0x0)
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/contracts/provers/WithdrawalProver.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.12;
6 |
7 | import "../interfaces/IReliquary.sol";
8 | import "../RelicToken.sol";
9 | import "./Prover.sol";
10 | import "./StateVerifier.sol";
11 | import "../lib/FactSigs.sol";
12 |
13 | /**
14 | * @title WithdrawalProver
15 | * @author Theori, Inc.
16 | * @notice WithdrawalProver proves valid withdrawals at some particular block.
17 | */
18 | contract WithdrawalProver is Prover, StateVerifier {
19 | constructor(address blockHistory, IReliquary _reliquary)
20 | Prover(_reliquary)
21 | StateVerifier(blockHistory, _reliquary)
22 | {}
23 |
24 | struct WithdrawalProof {
25 | uint256 idx;
26 | bytes withdrawalProof;
27 | bytes header;
28 | bytes blockProof;
29 | }
30 |
31 | function parseWithdrawalProof(bytes calldata proof)
32 | internal
33 | pure
34 | returns (WithdrawalProof calldata res)
35 | {
36 | assembly {
37 | res := proof.offset
38 | }
39 | }
40 |
41 | /**
42 | * @notice Proves that a storage slot had a particular value at a particular block.
43 | *
44 | * @param encodedProof the encoded WithdrawalProof
45 | */
46 | function _prove(bytes calldata encodedProof) internal view override returns (Fact memory) {
47 | WithdrawalProof calldata proof = parseWithdrawalProof(encodedProof);
48 | (
49 | CoreTypes.BlockHeaderData memory head,
50 | CoreTypes.WithdrawalData memory withdrawal
51 | ) = verifyWithdrawalAtBlock(
52 | proof.idx,
53 | proof.withdrawalProof,
54 | proof.header,
55 | proof.blockProof
56 | );
57 |
58 | bytes memory data = abi.encode(withdrawal);
59 | return
60 | Fact(
61 | withdrawal.Address,
62 | FactSigs.withdrawalFactSig(head.Number, withdrawal.Index),
63 | data
64 | );
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/contracts/provers/StorageSlotProver.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.12;
6 |
7 | import "../interfaces/IReliquary.sol";
8 | import "../RelicToken.sol";
9 | import "./Prover.sol";
10 | import "./StateVerifier.sol";
11 | import "../lib/FactSigs.sol";
12 |
13 | /**
14 | * @title StorageSlotProver
15 | * @author Theori, Inc.
16 | * @notice StorageSlotProver proves that a storage slot had a particular value
17 | * at a particular block.
18 | */
19 | contract StorageSlotProver is Prover, StateVerifier {
20 | constructor(address blockHistory, IReliquary _reliquary)
21 | Prover(_reliquary)
22 | StateVerifier(blockHistory, _reliquary)
23 | {}
24 |
25 | struct StorageSlotProof {
26 | address account;
27 | bytes accountProof;
28 | bytes32 slot;
29 | bytes slotProof;
30 | bytes header;
31 | bytes blockProof;
32 | }
33 |
34 | function parseStorageSlotProof(bytes calldata proof)
35 | internal
36 | pure
37 | returns (StorageSlotProof calldata res)
38 | {
39 | assembly {
40 | res := proof.offset
41 | }
42 | }
43 |
44 | /**
45 | * @notice Proves that a storage slot had a particular value at a particular block.
46 | *
47 | * @param encodedProof the encoded StorageSlotProof
48 | */
49 | function _prove(bytes calldata encodedProof) internal view override returns (Fact memory) {
50 | StorageSlotProof calldata proof = parseStorageSlotProof(encodedProof);
51 | (
52 | bool exists,
53 | CoreTypes.BlockHeaderData memory head,
54 | CoreTypes.AccountData memory acc
55 | ) = verifyAccountAtBlock(proof.account, proof.accountProof, proof.header, proof.blockProof);
56 | require(exists, "Account does not exist at block");
57 |
58 | bytes memory value = verifyStorageSlot(proof.slot, proof.slotProof, acc.StorageRoot);
59 | return Fact(proof.account, FactSigs.storageSlotFactSig(proof.slot, head.Number), value);
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/contracts/provers/CachedStorageSlotProver.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.12;
6 |
7 | import "../interfaces/IReliquary.sol";
8 | import "../RelicToken.sol";
9 | import "./Prover.sol";
10 | import "./StateVerifier.sol";
11 | import "../lib/FactSigs.sol";
12 |
13 | /**
14 | * @title CachedStorageSlotProver
15 | * @author Theori, Inc.
16 | * @notice CachedStorageSlotProver proves that a storage slot had a particular value
17 | * at a particular block, using a cached account storage root
18 | */
19 | contract CachedStorageSlotProver is Prover, StateVerifier {
20 | constructor(address blockHistory, IReliquary _reliquary)
21 | Prover(_reliquary)
22 | StateVerifier(blockHistory, _reliquary)
23 | {}
24 |
25 | struct CachedStorageSlotProof {
26 | address account;
27 | uint256 blockNumber;
28 | bytes32 storageRoot;
29 | bytes32 slot;
30 | bytes slotProof;
31 | }
32 |
33 | function parseCachedStorageSlotProof(bytes calldata proof)
34 | internal
35 | pure
36 | returns (CachedStorageSlotProof calldata res)
37 | {
38 | assembly {
39 | res := proof.offset
40 | }
41 | }
42 |
43 | /**
44 | * @notice Proves that a storage slot had a particular value at a particular block.
45 | *
46 | * @param encodedProof the encoded CachedStorageSlotProof
47 | */
48 | function _prove(bytes calldata encodedProof) internal view override returns (Fact memory) {
49 | CachedStorageSlotProof calldata proof = parseCachedStorageSlotProof(encodedProof);
50 |
51 | (bool exists, , ) = reliquary.getFact(
52 | proof.account,
53 | FactSigs.accountStorageFactSig(proof.blockNumber, proof.storageRoot)
54 | );
55 | require(exists, "Cached storage root doesn't exist");
56 |
57 | bytes memory value = verifyStorageSlot(proof.slot, proof.slotProof, proof.storageRoot);
58 | return
59 | Fact(proof.account, FactSigs.storageSlotFactSig(proof.slot, proof.blockNumber), value);
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/contracts/optimism/OptimismProxyBeaconBlockHistory.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity ^0.8.0;
6 |
7 | import "@eth-optimism/contracts/standards/AddressAliasHelper.sol";
8 | import "../ProxyBeaconBlockHistory.sol";
9 |
10 | interface IL1Block {
11 | function hash() external view returns (bytes32);
12 | function number() external view returns (uint64);
13 | }
14 |
15 | /**
16 | * @title OptimismProxyBeaconBlockHistory
17 | * @author Theori, Inc.
18 | * @notice A wrapper around ProxyBeaconBlockHistory which translates the messenger address
19 | * according to the Optimism aliasing rules and allows direct committing of
20 | * the accessible L1 block data.
21 | */
22 | contract OptimismProxyBeaconBlockHistory is ProxyBeaconBlockHistory {
23 | address public immutable messenger;
24 |
25 | constructor(
26 | address _messenger,
27 | address _reliquary,
28 | address _preDencunBlockHistory,
29 | uint256 _CAPELLA_SLOT,
30 | uint256 _DENCUN_SLOT,
31 | uint256 _UPGRADE_BLOCK
32 | )
33 | ProxyBeaconBlockHistory(
34 | _reliquary,
35 | _preDencunBlockHistory,
36 | _CAPELLA_SLOT,
37 | _DENCUN_SLOT,
38 | _UPGRADE_BLOCK
39 | )
40 | {
41 | messenger = AddressAliasHelper.applyL1ToL2Alias(_messenger);
42 | }
43 |
44 | /**
45 | * @notice Import a trusted block hash from the messenger
46 | * @param number the block number to import
47 | * @param hash the block hash
48 | */
49 | function importTrustedHash(uint256 number, bytes32 hash) external {
50 | require(msg.sender == messenger, "only the L1 messenger can import trusted block hashes");
51 | _storeCommittedBlock(number, hash);
52 | }
53 |
54 | function commitCurrentL1BlockHash() external {
55 | // https://community.optimism.io/docs/protocol/protocol-2.0/#l1block
56 | IL1Block l1Block = IL1Block(0x4200000000000000000000000000000000000015);
57 | _storeCommittedBlock(uint256(l1Block.number()), l1Block.hash());
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/contracts/provers/BatchProver.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.12;
6 |
7 | import "@openzeppelin/contracts/utils/introspection/ERC165.sol";
8 |
9 | import "../interfaces/IReliquary.sol";
10 | import "../interfaces/IBatchProver.sol";
11 |
12 | abstract contract BatchProver is ERC165, IBatchProver {
13 | IReliquary immutable reliquary;
14 |
15 | constructor(IReliquary _reliquary) {
16 | reliquary = _reliquary;
17 | }
18 |
19 | // Signaling event for off-chain indexers, does not contain data in order
20 | // to save gas
21 | event FactsProven();
22 |
23 | // must implemented by each prover
24 | function _prove(bytes calldata proof) internal view virtual returns (Fact[] memory);
25 |
26 | // can optionally be overridden by each prover
27 | function _afterStore(Fact memory fact, bool alreadyStored) internal virtual {}
28 |
29 | /**
30 | * @notice proves a fact ephemerally and returns the fact information
31 | * @param proof the encoded proof for this prover
32 | * @param store whether to store the fact in the reqliquary
33 | */
34 | function proveBatch(bytes calldata proof, bool store)
35 | public
36 | payable
37 | returns (Fact[] memory facts)
38 | {
39 | reliquary.checkProveFactFee{value: msg.value}(msg.sender);
40 | facts = _prove(proof);
41 | emit FactsProven();
42 | for (uint256 i = 0; i < facts.length; i++) {
43 | Fact memory fact = facts[i];
44 | if (store) {
45 | (bool alreadyStored, , ) = reliquary.getFact(fact.account, fact.sig);
46 | reliquary.setFact(fact.account, fact.sig, fact.data);
47 | _afterStore(fact, alreadyStored);
48 | }
49 | }
50 | }
51 |
52 | /**
53 | * @inheritdoc IERC165
54 | * @dev Supported interfaces: IProver
55 | */
56 | function supportsInterface(bytes4 interfaceId)
57 | public
58 | view
59 | virtual
60 | override(ERC165)
61 | returns (bool)
62 | {
63 | return (interfaceId == type(IBatchProver).interfaceId ||
64 | super.supportsInterface(interfaceId));
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/contracts/provers/CachedMultiStorageSlotProver.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.12;
6 |
7 | import "../interfaces/IReliquary.sol";
8 | import "../RelicToken.sol";
9 | import "../BlockHistory.sol";
10 | import "./BatchProver.sol";
11 | import "./StateVerifier.sol";
12 | import "../lib/FactSigs.sol";
13 |
14 | /**
15 | * @title CachedMultiStorageSlotProver
16 | * @author Theori, Inc.
17 | * @notice CachedMultiStorageSlotProver batch proves multiple storage slots from an account
18 | * at a particular block, using a cached account storage root
19 | */
20 | contract CachedMultiStorageSlotProver is BatchProver, StateVerifier {
21 | constructor(BlockHistory blockHistory, IReliquary _reliquary)
22 | BatchProver(_reliquary)
23 | StateVerifier(address(blockHistory), _reliquary)
24 | {}
25 |
26 | struct CachedMultiStorageSlotProof {
27 | address account;
28 | uint256 blockNumber;
29 | bytes32 storageRoot;
30 | bytes proofNodes;
31 | bytes32[] slots;
32 | bytes slotProofs;
33 | }
34 |
35 | function parseCachedMultiStorageSlotProof(bytes calldata proof)
36 | internal
37 | pure
38 | returns (CachedMultiStorageSlotProof calldata res)
39 | {
40 | assembly {
41 | res := proof.offset
42 | }
43 | }
44 |
45 | /**
46 | * @notice Proves that a storage slot had a particular value at a particular block.
47 | *
48 | * @param encodedProof the encoded CachedMultiStorageSlotProof
49 | */
50 | function _prove(bytes calldata encodedProof) internal view override returns (Fact[] memory) {
51 | CachedMultiStorageSlotProof calldata proof = parseCachedMultiStorageSlotProof(encodedProof);
52 | (bool exists, , ) = reliquary.getFact(
53 | proof.account,
54 | FactSigs.accountStorageFactSig(proof.blockNumber, proof.storageRoot)
55 | );
56 | require(exists, "Cached storage root doesn't exist");
57 |
58 | BytesCalldata[] memory values = verifyMultiStorageSlot(
59 | proof.proofNodes,
60 | proof.slots,
61 | proof.slotProofs,
62 | proof.storageRoot
63 | );
64 |
65 | Fact[] memory facts = new Fact[](values.length);
66 |
67 | for (uint256 i = 0; i < values.length; i++) {
68 | facts[i] = Fact(
69 | proof.account,
70 | FactSigs.storageSlotFactSig(proof.slots[i], proof.blockNumber),
71 | values[i].convert()
72 | );
73 | }
74 | return facts;
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/contracts/interfaces/IEphemeralFacts.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.0;
6 |
7 | import "./IRelicReceiver.sol";
8 |
9 | interface IEphemeralFacts {
10 | struct ReceiverContext {
11 | address initiator;
12 | IRelicReceiver receiver;
13 | bytes extra;
14 | uint256 gasLimit;
15 | bool requireSuccess;
16 | }
17 |
18 | struct FactDescription {
19 | address account;
20 | bytes sigData;
21 | }
22 |
23 | event FactRequested(FactDescription desc, ReceiverContext context, uint256 bounty);
24 |
25 | event ReceiveSuccess(IRelicReceiver receiver, bytes32 requestId);
26 |
27 | event ReceiveFailure(IRelicReceiver receiver, bytes32 requestId);
28 |
29 | event BountyClaimed(address indexed relayer, bytes32 indexed requestId, uint256 bounty);
30 |
31 | /**
32 | * @notice proves a fact ephemerally and provides it to the receiver
33 | * @param context the ReceiverContext for delivering the fact
34 | * @param prover the prover module to use, must implement IProver
35 | * @param proof the proof to pass to the prover
36 | */
37 | function proveEphemeral(
38 | ReceiverContext calldata context,
39 | address prover,
40 | bytes calldata proof
41 | ) external payable;
42 |
43 | /**
44 | * @notice proves a batch of facts ephemerally and provides them to the receivers
45 | * @param contexts the ReceiverContexts for delivering the facts
46 | * @param prover the prover module to use, must implement IBatchProver
47 | * @param proof the proof to pass to the prover
48 | */
49 | function batchProveEphemeral(
50 | ReceiverContext[] calldata contexts,
51 | address prover,
52 | bytes calldata proof
53 | ) external payable;
54 |
55 | /**
56 | * @notice requests a fact to be proven asynchronously and passed to the receiver,
57 | * @param account the account associated with the fact
58 | * @param sigData the fact data which determines the fact signature (class is assumed to be NO_FEE)
59 | * @param receiver the contract to receive the fact
60 | * @param data the extra data to pass to the receiver
61 | * @param gasLimit the maxmium gas used by the receiver
62 | * @dev msg.value is added to the bounty for this fact request,
63 | * incentivizing somebody to prove it
64 | */
65 | function requestFact(
66 | address account,
67 | bytes calldata sigData,
68 | IRelicReceiver receiver,
69 | bytes calldata data,
70 | uint256 gasLimit
71 | ) external payable;
72 | }
73 |
--------------------------------------------------------------------------------
/contracts/test/BlockHistoryForTesting.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.0;
6 |
7 | import "../BlockHistory.sol";
8 |
9 | /**
10 | * @title BlockHistoryForTesting
11 | * @author Theori, Inc.
12 | * @notice BlockHistory extension to aid in testing. Should never be deployed
13 | * in production.
14 | *
15 | */
16 | contract BlockHistoryForTesting is BlockHistory {
17 | constructor(
18 | uint256[] memory sizes,
19 | IRecursiveVerifier[] memory verifiers,
20 | address reliquary
21 | ) BlockHistory(sizes, verifiers, reliquary) {
22 | require(block.chainid == 31337, "testing only");
23 | }
24 |
25 | /**
26 | * @notice directly sets the parentHash and lastHash; used for testing,
27 | * only callable if deployed contract is for testing
28 | *
29 | * @param parent the parentHash
30 | * @param last the lastHash
31 | */
32 | function setHashesForTesting(bytes32 parent, bytes32 last) external onlyRole(ADMIN_ROLE) {
33 | parentHash = parent;
34 | lastHash = last;
35 | }
36 |
37 | /**
38 | * @notice directly sets the earliestRoot; used for testing,
39 | * only callable if deployed contract is for testing
40 | *
41 | * @param num the new earliestRoot
42 | */
43 | function setEarliestRootForTesting(uint256 num) external onlyRole(ADMIN_ROLE) {
44 | earliestRoot = num;
45 | }
46 |
47 | /**
48 | * @notice Stores the merkle roots starting at the index; used for testing,
49 | * only callable if deploy contract is for testing
50 | *
51 | * @param index the index for the first merkle root
52 | * @param roots the block merkle roots
53 | * @param aux the auxiliary roots
54 | */
55 | function storeMerkleRootsForTesting(
56 | uint256 index,
57 | bytes32[] calldata roots,
58 | bytes32[] calldata aux
59 | ) external onlyRole(ADMIN_ROLE) {
60 | storeMerkleRoots(index, roots, aux);
61 | }
62 |
63 | /**
64 | * @notice Same as assertValidBlockHash, except it ignores the feeProvider and
65 | * is view; only callable if deployed contract is for testing
66 | *
67 | * @param hash the hash to check
68 | * @param num the block number for the alleged hash
69 | * @param proof the merkle witness or SNARK proof (if needed)
70 | */
71 | function assertValidBlockHashForTesting(
72 | bytes32 hash,
73 | uint256 num,
74 | bytes calldata proof
75 | ) external view onlyRole(ADMIN_ROLE) {
76 | require(_validBlockHash(hash, num, proof), "invalid block hash");
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/contracts/provers/MultiStorageSlotProver.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.12;
6 |
7 | import "../interfaces/IReliquary.sol";
8 | import "../RelicToken.sol";
9 | import "./BatchProver.sol";
10 | import "./StateVerifier.sol";
11 | import "../lib/FactSigs.sol";
12 |
13 | /**
14 | * @title MultiStorageSlotProver
15 | * @author Theori, Inc.
16 | * @notice MultiStorageSlotProver batch proves multiple storage slots from an account
17 | * at a particular block.
18 | */
19 | contract MultiStorageSlotProver is BatchProver, StateVerifier {
20 | constructor(address blockHistory, IReliquary _reliquary)
21 | BatchProver(_reliquary)
22 | StateVerifier(blockHistory, _reliquary)
23 | {}
24 |
25 | struct MultiStorageSlotProof {
26 | address account;
27 | bytes accountProof;
28 | bytes header;
29 | bytes blockProof;
30 | bytes proofNodes;
31 | bytes32[] slots;
32 | bytes slotProofs;
33 | bool includeHeader;
34 | }
35 |
36 | function parseMultiStorageSlotProof(bytes calldata proof)
37 | internal
38 | pure
39 | returns (MultiStorageSlotProof calldata res)
40 | {
41 | assembly {
42 | res := proof.offset
43 | }
44 | }
45 |
46 | /**
47 | * @notice Proves that a storage slot had a particular value at a particular block.
48 | *
49 | * @param encodedProof the encoded MultiStorageSlotProof
50 | */
51 | function _prove(bytes calldata encodedProof) internal view override returns (Fact[] memory) {
52 | MultiStorageSlotProof calldata proof = parseMultiStorageSlotProof(encodedProof);
53 | (
54 | bool exists,
55 | CoreTypes.BlockHeaderData memory head,
56 | CoreTypes.AccountData memory acc
57 | ) = verifyAccountAtBlock(proof.account, proof.accountProof, proof.header, proof.blockProof);
58 | require(exists, "Account does not exist at block");
59 |
60 | BytesCalldata[] memory values = verifyMultiStorageSlot(
61 | proof.proofNodes,
62 | proof.slots,
63 | proof.slotProofs,
64 | acc.StorageRoot
65 | );
66 |
67 | uint256 slotsOffset = proof.includeHeader ? 1 : 0;
68 | Fact[] memory facts = new Fact[](values.length + slotsOffset);
69 |
70 | if (proof.includeHeader) {
71 | facts[0] = Fact(address(0), FactSigs.blockHeaderSig(head.Number), abi.encode(head));
72 | }
73 | for (uint256 i = 0; i < values.length; i++) {
74 | facts[slotsOffset + i] = Fact(
75 | proof.account,
76 | FactSigs.storageSlotFactSig(proof.slots[i], head.Number),
77 | values[i].convert()
78 | );
79 | }
80 | return facts;
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/contracts/provers/BirthCertificateProver.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.12;
6 |
7 | import "../interfaces/IReliquary.sol";
8 | import "../RelicToken.sol";
9 | import "./StateVerifier.sol";
10 | import "./Prover.sol";
11 | import "../lib/FactSigs.sol";
12 |
13 | /**
14 | * @title BirthCertificateProver
15 | * @author Theori, Inc.
16 | * @notice BirthCertificateProver proves that an account existed in a given block
17 | * and stores the oldest known account proof in the fact database
18 | */
19 | contract BirthCertificateProver is Prover, StateVerifier {
20 | FactSignature public immutable BIRTH_CERTIFICATE_SIG;
21 | RelicToken immutable token;
22 |
23 | struct AccountProof {
24 | address account;
25 | bytes accountProof;
26 | bytes header;
27 | bytes blockProof;
28 | }
29 |
30 | constructor(
31 | address blockHistory,
32 | IReliquary _reliquary,
33 | RelicToken _token
34 | ) Prover(_reliquary) StateVerifier(blockHistory, _reliquary) {
35 | BIRTH_CERTIFICATE_SIG = FactSigs.birthCertificateFactSig();
36 | token = _token;
37 | }
38 |
39 | function parseAccountProof(bytes calldata proof)
40 | internal
41 | pure
42 | returns (AccountProof calldata res)
43 | {
44 | assembly {
45 | res := proof.offset
46 | }
47 | }
48 |
49 | /**
50 | * @notice Proves that an account existed in the given block
51 | *
52 | * @param encodedProof the encoded AccountProof
53 | */
54 | function _prove(bytes calldata encodedProof) internal view override returns (Fact memory) {
55 | AccountProof calldata proof = parseAccountProof(encodedProof);
56 | (bool exists, CoreTypes.BlockHeaderData memory head, ) = verifyAccountAtBlock(
57 | proof.account,
58 | proof.accountProof,
59 | proof.header,
60 | proof.blockProof
61 | );
62 | require(exists, "Account does not exist at block");
63 |
64 | (bool proven, , bytes memory data) = reliquary.getFact(
65 | proof.account,
66 | BIRTH_CERTIFICATE_SIG
67 | );
68 |
69 | if (proven) {
70 | uint48 blockNum = uint48(bytes6(data));
71 | require(blockNum >= head.Number, "older block already proven");
72 | }
73 |
74 | data = abi.encodePacked(uint48(head.Number), uint64(head.Time));
75 | return Fact(proof.account, BIRTH_CERTIFICATE_SIG, data);
76 | }
77 |
78 | /**
79 | * @notice handles minting the token after a fact is stored
80 | *
81 | * @param fact the fact which was stored
82 | */
83 | function _afterStore(Fact memory fact, bool alreadyStored) internal override {
84 | if (!alreadyStored) {
85 | token.mint(fact.account, 0);
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/contracts/lib/Proofs.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | /*
6 | * @author Theori, Inc.
7 | */
8 |
9 | pragma solidity >=0.8.0;
10 |
11 | import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
12 |
13 | uint256 constant BASE_PROOF_SIZE = 34;
14 | uint256 constant SUBPROOF_LIMBS_SIZE = 16;
15 |
16 | struct RecursiveProof {
17 | uint256[BASE_PROOF_SIZE] base;
18 | uint256[SUBPROOF_LIMBS_SIZE] subproofLimbs;
19 | uint256[] inputs;
20 | }
21 |
22 | struct SignedRecursiveProof {
23 | RecursiveProof inner;
24 | bytes signature;
25 | }
26 |
27 | /**
28 | * @notice recover the signer of the proof
29 | * @param proof the SignedRecursiveProof
30 | * @return the address of the signer
31 | */
32 | function getProofSigner(SignedRecursiveProof calldata proof) pure returns (address) {
33 | bytes32 msgHash = keccak256(
34 | abi.encodePacked("\x19Ethereum Signed Message:\n", "32", hashProof(proof.inner))
35 | );
36 | return ECDSA.recover(msgHash, proof.signature);
37 | }
38 |
39 | /**
40 | * @notice hash the contents of a RecursiveProof
41 | * @param proof the RecursiveProof
42 | * @return result a 32-byte digest of the proof
43 | */
44 | function hashProof(RecursiveProof calldata proof) pure returns (bytes32 result) {
45 | uint256[] calldata inputs = proof.inputs;
46 | assembly {
47 | let ptr := mload(0x40)
48 | let contigLen := mul(0x20, add(BASE_PROOF_SIZE, SUBPROOF_LIMBS_SIZE))
49 | let inputsLen := mul(0x20, inputs.length)
50 | calldatacopy(ptr, proof, contigLen)
51 | calldatacopy(add(ptr, contigLen), inputs.offset, inputsLen)
52 | result := keccak256(ptr, add(contigLen, inputsLen))
53 | }
54 | }
55 |
56 | /**
57 | * @notice reverse the byte order of a uint256
58 | * @param input the input value
59 | * @return v the byte-order reversed value
60 | */
61 | function byteReverse(uint256 input) pure returns (uint256 v) {
62 | v = input;
63 |
64 | uint256 MASK08 = 0xFF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00;
65 | uint256 MASK16 = 0xFFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000;
66 | uint256 MASK32 = 0xFFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000;
67 | uint256 MASK64 = 0xFFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF0000000000000000;
68 |
69 | // swap bytes
70 | v = ((v & MASK08) >> 8) | ((v & (~MASK08)) << 8);
71 |
72 | // swap 2-byte long pairs
73 | v = ((v & MASK16) >> 16) | ((v & (~MASK16)) << 16);
74 |
75 | // swap 4-byte long pairs
76 | v = ((v & MASK32) >> 32) | ((v & (~MASK32)) << 32);
77 |
78 | // swap 8-byte long pairs
79 | v = ((v & MASK64) >> 64) | ((v & (~MASK64)) << 64);
80 |
81 | // swap 16-byte long pairs
82 | v = (v >> 128) | (v << 128);
83 | }
84 |
85 | /**
86 | * @notice reads a 32-byte hash from its little-endian word-encoded form
87 | * @param words the hash words
88 | * @return the hash
89 | */
90 | function readHashWords(uint256[] calldata words) pure returns (bytes32) {
91 | uint256 mask = 0xffffffffffffffff;
92 | uint256 result = (words[0] & mask);
93 | result |= (words[1] & mask) << 0x40;
94 | result |= (words[2] & mask) << 0x80;
95 | result |= (words[3] & mask) << 0xc0;
96 | return bytes32(byteReverse(result));
97 | }
98 |
--------------------------------------------------------------------------------
/contracts/provers/AccountInfoProver.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.12;
6 |
7 | import "../interfaces/IReliquary.sol";
8 | import "../RelicToken.sol";
9 | import "./Prover.sol";
10 | import "./StateVerifier.sol";
11 | import "../lib/FactSigs.sol";
12 |
13 | enum AccountInfo {
14 | StorageRoot,
15 | CodeHash,
16 | Balance,
17 | Nonce,
18 | RawHeader
19 | }
20 |
21 | /**
22 | * @title AccountInfoProver
23 | * @author Theori, Inc.
24 | * @notice AccountInfoProver proves info (nonce, balance, codehash) about an account at a particular block
25 | */
26 | contract AccountInfoProver is Prover, StateVerifier {
27 | constructor(address blockHistory, IReliquary _reliquary)
28 | Prover(_reliquary)
29 | StateVerifier(blockHistory, _reliquary)
30 | {}
31 |
32 | struct AccountInfoProof {
33 | address account;
34 | bytes accountProof;
35 | bytes header;
36 | bytes blockProof;
37 | AccountInfo info;
38 | }
39 |
40 | function parseAccountInfoProof(bytes calldata proof)
41 | internal
42 | pure
43 | returns (AccountInfoProof calldata res)
44 | {
45 | assembly {
46 | res := proof.offset
47 | }
48 | }
49 |
50 | /**
51 | * @notice Proves that an account contained particular info (nonce, balance, codehash) at a particular block.
52 | *
53 | * @param encodedProof the encoded AccountInfoProof
54 | */
55 | function _prove(bytes calldata encodedProof) internal view override returns (Fact memory) {
56 | AccountInfoProof calldata proof = parseAccountInfoProof(encodedProof);
57 | (
58 | bool exists,
59 | CoreTypes.BlockHeaderData memory head,
60 | CoreTypes.AccountData memory acc
61 | ) = verifyAccountAtBlock(proof.account, proof.accountProof, proof.header, proof.blockProof);
62 | require(exists, "Account does not exist at block");
63 |
64 | if (proof.info == AccountInfo.StorageRoot) {
65 | return
66 | Fact(
67 | proof.account,
68 | FactSigs.accountStorageFactSig(head.Number, acc.StorageRoot),
69 | ""
70 | );
71 | } else if (proof.info == AccountInfo.CodeHash) {
72 | return
73 | Fact(proof.account, FactSigs.accountCodeHashFactSig(head.Number, acc.CodeHash), "");
74 | } else if (proof.info == AccountInfo.Balance) {
75 | return
76 | Fact(
77 | proof.account,
78 | FactSigs.accountBalanceFactSig(head.Number),
79 | abi.encodePacked(acc.Balance)
80 | );
81 | } else if (proof.info == AccountInfo.Nonce) {
82 | return
83 | Fact(
84 | proof.account,
85 | FactSigs.accountNonceFactSig(head.Number),
86 | abi.encodePacked(acc.Nonce)
87 | );
88 | } else if (proof.info == AccountInfo.RawHeader) {
89 | return Fact(proof.account, FactSigs.accountFactSig(head.Number), abi.encode(acc));
90 | } else {
91 | revert("Unknown account info requested");
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/contracts/lib/Storage.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.0;
6 |
7 | /**
8 | * @title Storage
9 | * @author Theori, Inc.
10 | * @notice Helper functions for handling storage slot facts and computing storage slots
11 | */
12 | library Storage {
13 | /**
14 | * @notice compute the slot for an element of a mapping
15 | * @param base the slot of the struct base
16 | * @param key the mapping key, padded to 32 bytes
17 | */
18 | function mapElemSlot(bytes32 base, bytes32 key) internal pure returns (bytes32) {
19 | return keccak256(abi.encodePacked(key, base));
20 | }
21 |
22 | /**
23 | * @notice compute the slot for an element of a static array
24 | * @param base the slot of the struct base
25 | * @param idx the index of the element
26 | * @param slotsPerElem the number of slots per element
27 | */
28 | function staticArrayElemSlot(
29 | bytes32 base,
30 | uint256 idx,
31 | uint256 slotsPerElem
32 | ) internal pure returns (bytes32) {
33 | return bytes32(uint256(base) + idx * slotsPerElem);
34 | }
35 |
36 | /**
37 | * @notice compute the slot for an element of a dynamic array
38 | * @param base the slot of the struct base
39 | * @param idx the index of the element
40 | * @param slotsPerElem the number of slots per element
41 | */
42 | function dynamicArrayElemSlot(
43 | bytes32 base,
44 | uint256 idx,
45 | uint256 slotsPerElem
46 | ) internal pure returns (bytes32) {
47 | return bytes32(uint256(keccak256(abi.encode(base))) + idx * slotsPerElem);
48 | }
49 |
50 | /**
51 | * @notice compute the slot for a struct field given the base slot and offset
52 | * @param base the slot of the struct base
53 | * @param offset the slot offset in the struct
54 | */
55 | function structFieldSlot(
56 | bytes32 base,
57 | uint256 offset
58 | ) internal pure returns (bytes32) {
59 | return bytes32(uint256(base) + offset);
60 | }
61 |
62 | function _parseUint256(bytes memory data) internal pure returns (uint256) {
63 | return uint256(bytes32(data)) >> (256 - 8 * data.length);
64 | }
65 |
66 | /**
67 | * @notice parse a uint256 from storage slot bytes
68 | * @param data the storage slot bytes
69 | * @return address the parsed address
70 | */
71 | function parseUint256(bytes memory data) internal pure returns (uint256) {
72 | require(data.length <= 32, 'data is not a uint256');
73 | return _parseUint256(data);
74 | }
75 |
76 | /**
77 | * @notice parse a uint64 from storage slot bytes
78 | * @param data the storage slot bytes
79 | */
80 | function parseUint64(bytes memory data) internal pure returns (uint64) {
81 | require(data.length <= 8, 'data is not a uint64');
82 | return uint64(_parseUint256(data));
83 | }
84 |
85 | /**
86 | * @notice parse an address from storage slot bytes
87 | * @param data the storage slot bytes
88 | */
89 | function parseAddress(bytes memory data) internal pure returns (address) {
90 | require(data.length <= 20, 'data is not an address');
91 | return address(uint160(_parseUint256(data)));
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/contracts/lib/Callbacks.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.0;
6 |
7 | /**
8 | * @notice Helper for providing fair gas limits in callbacks, adapted from Chainlink
9 | */
10 | library Callbacks {
11 | // 5k is plenty for an EXTCODESIZE call (2600) + warm CALL (100)
12 | // and some arithmetic operations.
13 | uint256 private constant GAS_FOR_CALL_EXACT_CHECK = 5_000;
14 |
15 | /**
16 | * @notice calls target address with exactly gasAmount gas and data as calldata
17 | * or reverts if at least gasAmount gas is not available.
18 | * @param gasAmount the exact amount of gas to call with
19 | * @param target the address to call
20 | * @param data the calldata to pass
21 | * @return success whether the call succeeded
22 | */
23 | function callWithExactGas(
24 | uint256 gasAmount,
25 | address target,
26 | bytes memory data
27 | ) internal returns (bool success, bytes memory result) {
28 | // solhint-disable-next-line no-inline-assembly
29 | uint256 returnsize;
30 | assembly {
31 | function notEnoughGas() {
32 | // revert Error("not enough gas for call")
33 | mstore(0x00, hex"08c379a000000000000000000000000000000000000000000000000000000000")
34 | mstore(0x20, hex"0000002000000000000000000000000000000000000000000000000000000000")
35 | mstore(0x40, hex"0000001b6e6f7420656e6f7567682067617320666f722063616c6c0000000000")
36 | revert(0, 0x60)
37 | }
38 | function notContract() {
39 | // revert Error("call target not contract")
40 | mstore(0x00, hex"08c379a000000000000000000000000000000000000000000000000000000000")
41 | mstore(0x20, hex"0000002000000000000000000000000000000000000000000000000000000000")
42 | mstore(0x40, hex"0000001a63616c6c20746172676574206e6f74206120636f6e74726163740000")
43 | revert(0, 0x60)
44 | }
45 | let g := gas()
46 | // Compute g -= GAS_FOR_CALL_EXACT_CHECK and check for underflow
47 | // The gas actually passed to the callee is min(gasAmount, 63//64*gas available).
48 | // We want to ensure that we revert if gasAmount > 63//64*gas available
49 | // as we do not want to provide them with less, however that check itself costs
50 | // gas. GAS_FOR_CALL_EXACT_CHECK ensures we have at least enough gas to be able
51 | // to revert if gasAmount > 63//64*gas available.
52 | if lt(g, GAS_FOR_CALL_EXACT_CHECK) {
53 | notEnoughGas()
54 | }
55 | g := sub(g, GAS_FOR_CALL_EXACT_CHECK)
56 | // if g - g//64 <= gasAmount, revert
57 | // (we subtract g//64 because of EIP-150)
58 | if iszero(gt(sub(g, div(g, 64)), gasAmount)) {
59 | notEnoughGas()
60 | }
61 | // solidity calls check that a contract actually exists at the destination, so we do the same
62 | if iszero(extcodesize(target)) {
63 | notContract()
64 | }
65 | // call and return whether we succeeded. ignore return data
66 | // call(gas,addr,value,argsOffset,argsLength,retOffset,retLength)
67 | success := call(gasAmount, target, 0, add(data, 0x20), mload(data), 0, 0)
68 | returnsize := returndatasize()
69 | }
70 | // copy the return data
71 | result = new bytes(returnsize);
72 | assembly {
73 | returndatacopy(add(result, 0x20), 0, returnsize)
74 | }
75 | }
76 |
77 | /**
78 | * @notice reverts the current call with the provided raw data
79 | * @param data the revert data to return
80 | */
81 | function revertWithData(bytes memory data) internal pure {
82 | assembly {
83 | revert(add(data, 0x20), mload(data))
84 | }
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/contracts/lib/BytesCalldata.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.13;
6 |
7 | // custom bytes calldata pointer storing (length | offset) in one word,
8 | // also allows calldata pointers to be stored in memory
9 | type BytesCalldata is uint256;
10 |
11 | using BytesCalldataOps for BytesCalldata global;
12 |
13 | // can't introduce global using .. for non UDTs
14 | // each consumer should add the following line:
15 | using BytesCalldataOps for bytes;
16 |
17 | /**
18 | * @author Theori, Inc
19 | * @title BytesCalldataOps
20 | * @notice Common operations for bytes calldata, implemented for both the builtin
21 | * type and our BytesCalldata type. These operations are heavily optimized
22 | * and omit safety checks, so this library should only be used when memory
23 | * safety is not a security issue.
24 | */
25 | library BytesCalldataOps {
26 | function length(BytesCalldata bc) internal pure returns (uint256 result) {
27 | assembly {
28 | result := shr(128, shl(128, bc))
29 | }
30 | }
31 |
32 | function offset(BytesCalldata bc) internal pure returns (uint256 result) {
33 | assembly {
34 | result := shr(128, bc)
35 | }
36 | }
37 |
38 | function convert(BytesCalldata bc) internal pure returns (bytes calldata value) {
39 | assembly {
40 | value.offset := shr(128, bc)
41 | value.length := shr(128, shl(128, bc))
42 | }
43 | }
44 |
45 | function convert(bytes calldata inp) internal pure returns (BytesCalldata bc) {
46 | assembly {
47 | bc := or(shl(128, inp.offset), inp.length)
48 | }
49 | }
50 |
51 | function slice(
52 | BytesCalldata bc,
53 | uint256 start,
54 | uint256 len
55 | ) internal pure returns (BytesCalldata result) {
56 | assembly {
57 | result := shl(128, add(shr(128, bc), start)) // add to the offset and clear the length
58 | result := or(result, len) // set the new length
59 | }
60 | }
61 |
62 | function slice(
63 | bytes calldata value,
64 | uint256 start,
65 | uint256 len
66 | ) internal pure returns (bytes calldata result) {
67 | assembly {
68 | result.offset := add(value.offset, start)
69 | result.length := len
70 | }
71 | }
72 |
73 | function prefix(BytesCalldata bc, uint256 len) internal pure returns (BytesCalldata result) {
74 | assembly {
75 | result := shl(128, shr(128, bc)) // clear out the length
76 | result := or(result, len) // set it to the new length
77 | }
78 | }
79 |
80 | function prefix(bytes calldata value, uint256 len)
81 | internal
82 | pure
83 | returns (bytes calldata result)
84 | {
85 | assembly {
86 | result.offset := value.offset
87 | result.length := len
88 | }
89 | }
90 |
91 | function suffix(BytesCalldata bc, uint256 start) internal pure returns (BytesCalldata result) {
92 | assembly {
93 | result := add(bc, shl(128, start)) // add to the offset
94 | result := sub(result, start) // subtract from the length
95 | }
96 | }
97 |
98 | function suffix(bytes calldata value, uint256 start)
99 | internal
100 | pure
101 | returns (bytes calldata result)
102 | {
103 | assembly {
104 | result.offset := add(value.offset, start)
105 | result.length := sub(value.length, start)
106 | }
107 | }
108 |
109 | function split(BytesCalldata bc, uint256 start)
110 | internal
111 | pure
112 | returns (BytesCalldata, BytesCalldata)
113 | {
114 | return (prefix(bc, start), suffix(bc, start));
115 | }
116 |
117 | function split(bytes calldata value, uint256 start)
118 | internal
119 | pure
120 | returns (bytes calldata, bytes calldata)
121 | {
122 | return (prefix(value, start), suffix(value, start));
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/contracts/RelicTokenConfigurable.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.12;
6 |
7 | import "@openzeppelin/contracts/access/Ownable.sol";
8 | import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
9 | import "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol";
10 | import "@openzeppelin/contracts/utils/introspection/ERC165.sol";
11 |
12 | import "./interfaces/IERC5192.sol";
13 | import "./interfaces/ITokenURI.sol";
14 | import "./RelicToken.sol";
15 |
16 | /**
17 | * @title RelicTokenConfigurable
18 | * @author Theori, Inc.
19 | * @notice RelicTokenConfigurable augments the RelicToken contract to allow
20 | * for configurable tokenURIs.
21 | */
22 | abstract contract RelicTokenConfigurable is RelicToken {
23 | mapping(uint64 => ITokenURI[]) TokenURIProviders;
24 |
25 | function addURIProvider(address provider, uint64 bulkId) external onlyOwner {
26 | TokenURIProviders[bulkId].push(ITokenURI(provider));
27 | }
28 |
29 | function getLatestProviderIdx(uint64 bulkId) internal view returns (uint256) {
30 | uint256 length = TokenURIProviders[bulkId].length;
31 | if (length == 0) {
32 | return 0;
33 | }
34 | return length - 1;
35 | }
36 |
37 | /**
38 | * @notice Helper function to break a tokenId into its constituent data
39 | * @return who the address bound to this token
40 | * @return data any additional data bound to this token
41 | * @return idx the index of the URI provider for this token
42 | * @return bulkId the generic "class" of this Relic
43 | * (eg: eventId for AttendanceArtifacts)
44 | */
45 | function parseTokenIdData(uint256 tokenId)
46 | internal
47 | pure
48 | returns (
49 | address who,
50 | uint96 data,
51 | uint32 idx,
52 | uint64 bulkId
53 | )
54 | {
55 | (who, data) = parseTokenId(tokenId);
56 | idx = uint32(data >> 64);
57 | bulkId = uint64(data);
58 | }
59 |
60 | /// @inheritdoc RelicToken
61 | function mint(address who, uint96 data) public override {
62 | require(uint32(data >> 64) == 0, "high data bits in use");
63 | uint64 bulkId = uint64(data);
64 | uint32 providerIdx = uint32(getLatestProviderIdx(bulkId));
65 | uint96 newData = uint96(bulkId) | (uint96(providerIdx) << 64);
66 |
67 | return super.mint(who, newData);
68 | }
69 |
70 | /// @inheritdoc RelicToken
71 | function tokenURI(uint256 tokenId) external view override returns (string memory) {
72 | (address who, uint96 data, uint32 providerIdx, uint64 bulkId) = parseTokenIdData(tokenId);
73 |
74 | require(hasToken(who, data), "token does not exist");
75 | require(providerIdx < TokenURIProviders[bulkId].length, "uri provider not set");
76 |
77 | return TokenURIProviders[bulkId][providerIdx].tokenURI(tokenId);
78 | }
79 |
80 | /**
81 | * @notice request a token be replaced with another representing the same
82 | * data, but with a different URI provider.
83 | * @param tokenId the existing token to recycle
84 | * @param newProviderIdx the new URI provider to use
85 | * @dev emits Unlock and Transfer to 0 for the existing tokenId, and then
86 | * Transfer and Lock for the new tokenId.
87 | * @dev Due to not using storage, this function will not revert so long as
88 | * the msg.sender is entitled to the tokenId they originally provide,
89 | * even if that tokenId has never been issued. Misuse may cause confusion
90 | * for tools attempting to track ERC-721 transfers.
91 | */
92 | function exchange(uint256 tokenId, uint32 newProviderIdx) external {
93 | (address who, uint96 data, , uint64 bulkId) = parseTokenIdData(tokenId);
94 |
95 | require(who == msg.sender, "only token owner may exchange");
96 | require(hasToken(who, data), "token does not exist");
97 | require(newProviderIdx < TokenURIProviders[bulkId].length, "invalid URI provider");
98 |
99 | uint256 newTokenId = (uint256(uint160(who)) |
100 | (uint256(bulkId) << 160) |
101 | (uint256(newProviderIdx) << 224));
102 |
103 | emit Unlocked(tokenId);
104 | emit Transfer(who, address(0), tokenId);
105 | emit Transfer(address(0), who, newTokenId);
106 | emit Locked(newTokenId);
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/contracts/optimism/OptimismNativeBlockHistory.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.0;
6 |
7 | import "../BlockHistory.sol";
8 | import "../interfaces/IBatchProver.sol";
9 | import "../interfaces/IReliquary.sol";
10 | import "../lib/Storage.sol";
11 | import "../lib/FactSigs.sol";
12 | import "../lib/CoreTypes.sol";
13 | import "./interfaces/IOptimismNativeBlockHistory.sol";
14 |
15 | /**
16 | * @title OptimismNativeBlockHistory
17 | * @author Theori, Inc.
18 | * @notice OptimismNativeBlockHistory extends BlockHistory with the
19 | * ability to trustlessly import a historical blockhash from
20 | * an Optimism L2 output root stored on the L1. This is done
21 | * using Relic's trustless access to L1 data. For more on how
22 | * that works, see the ProxyBlockHistory contract.
23 | */
24 | contract OptimismNativeBlockHistory is IOptimismNativeBlockHistory, BlockHistory {
25 | address public immutable override proxyMultiStorageSlotProver;
26 | address public immutable override l2OutputOracle;
27 | bytes32 public immutable override OUTPUT_ROOTS_BASE_SLOT;
28 | uint256 public immutable override FINALIZATION_PERIOD_SECONDS;
29 |
30 | /// @notice Hashes the various elements of an output root proof into an output root hash which
31 | /// can be used to check if the proof is valid.
32 | /// @param _outputRootProof Output root proof which should hash to an output root.
33 | /// @return Hashed output root proof.
34 | function hashOutputRootProof(OutputRootProof calldata _outputRootProof) internal pure returns (bytes32) {
35 | return keccak256(
36 | abi.encode(
37 | _outputRootProof.version,
38 | _outputRootProof.stateRoot,
39 | _outputRootProof.messagePasserStorageRoot,
40 | _outputRootProof.latestBlockhash
41 | )
42 | );
43 | }
44 |
45 | constructor(
46 | uint256[] memory _sizes,
47 | IRecursiveVerifier[] memory _verifiers,
48 | address _reliquary,
49 | address _proxyMultiStorageSlotProver,
50 | address _l2OutputOracle,
51 | bytes32 _outputRootsBaseSlot,
52 | uint256 _finalizationPeriodSeconds
53 | ) BlockHistory(_sizes, _verifiers, _reliquary) {
54 | proxyMultiStorageSlotProver = _proxyMultiStorageSlotProver;
55 | l2OutputOracle = _l2OutputOracle;
56 | OUTPUT_ROOTS_BASE_SLOT = _outputRootsBaseSlot;
57 | FINALIZATION_PERIOD_SECONDS = _finalizationPeriodSeconds;
58 | }
59 |
60 | function importCheckpointBlockFromL1(
61 | bytes calldata proof,
62 | uint256 index,
63 | uint256 l1BlockNumber,
64 | OutputRootProof calldata outputRootProof
65 | ) external {
66 | require(index < 2 ** 64, "invalid output root index");
67 |
68 | Fact[] memory facts = IBatchProver(proxyMultiStorageSlotProver).proveBatch(proof, false);
69 | require(facts.length == 3, "invalid number of facts");
70 |
71 | require(
72 | FactSignature.unwrap(facts[0].sig) ==
73 | FactSignature.unwrap(FactSigs.blockHeaderSig(l1BlockNumber)),
74 | "first fact is not block header"
75 | );
76 | CoreTypes.BlockHeaderData memory l1Block = abi.decode(facts[0].data, (CoreTypes.BlockHeaderData));
77 |
78 | bytes32 slot = Storage.dynamicArrayElemSlot(OUTPUT_ROOTS_BASE_SLOT, index, 2);
79 | FactSignature expected = FactSigs.storageSlotFactSig(slot, l1BlockNumber);
80 | require(
81 | FactSignature.unwrap(facts[1].sig) == FactSignature.unwrap(expected),
82 | "first fact sig is incorrect"
83 | );
84 | expected = FactSigs.storageSlotFactSig(bytes32(uint256(slot) + 1), l1BlockNumber);
85 | require(
86 | FactSignature.unwrap(facts[2].sig) == FactSignature.unwrap(expected),
87 | "first fact sig is incorrect"
88 | );
89 |
90 | bytes32 outputRoot = bytes32(Storage.parseUint256(facts[1].data));
91 | require(outputRoot == hashOutputRootProof(outputRootProof), "outputRootProof is invalid");
92 |
93 | uint256 timestampAndBlockNum = Storage.parseUint256(facts[2].data);
94 | uint256 timestamp = uint256(uint128(timestampAndBlockNum));
95 | require(
96 | timestamp + FINALIZATION_PERIOD_SECONDS < l1Block.Time,
97 | "root not finalized at given L1 block"
98 | );
99 |
100 | uint256 blockNum = timestampAndBlockNum >> 128;
101 | _storeCommittedBlock(blockNum, outputRootProof.latestBlockhash);
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/utils/network.js:
--------------------------------------------------------------------------------
1 | const { companionNetworks } = require("hardhat")
2 | const { EthersProviderWrapper } = require("@nomiclabs/hardhat-ethers/internal/ethers-provider-wrapper")
3 | const util = require("util")
4 |
5 | const BEACON_GENESIS_TIMESTAMP = {
6 | 1: 1606824023,
7 | 11155111: 1655733600,
8 | }
9 |
10 | function patchProviderPolling(provider) {
11 | // if `provider` is a proxy, extract the inner object
12 | if (util.types.isProxy(provider)) {
13 | provider.__proto__.__interceptor = function () { provider = this }
14 | provider.__interceptor()
15 | }
16 | provider.pollingInterval = 40000
17 | if (!provider._getNetwork) {
18 | provider._getNetwork = provider.getNetwork
19 | }
20 | provider.getNetwork = function() {
21 | if (this._network) { return Promise.resolve(this._network) }
22 | return this._getNetwork()
23 | }
24 | }
25 |
26 | function getMessengerName() {
27 | if (network.config.bridged !== true) {
28 | throw Error("not on bridged network")
29 | }
30 | if (network.config.zksync) {
31 | return "ZkSyncBlockHashMessenger"
32 | } else if (network.config.optimism) {
33 | if (network.name.includes("base")) {
34 | return "BaseBlockHashMessenger"
35 | } else if (network.name.includes("blast")) {
36 | return "BlastBlockHashMessenger"
37 | } else {
38 | return "OptimismBlockHashMessenger"
39 | }
40 | } else {
41 | throw Error("unsupported network")
42 | }
43 | }
44 |
45 |
46 | function getL1Provider() {
47 | if (companionNetworks['l1']) {
48 | const provider = new EthersProviderWrapper(
49 | companionNetworks['l1'].provider,
50 | companionNetworks['l1'].deployments.getNetworkName()
51 | )
52 | patchProviderPolling(provider)
53 | return provider
54 | } else {
55 | return ethers.provider
56 | }
57 | }
58 |
59 | function getSigner(network, address) {
60 | return companionNetworks[network].deployments.getSigner(address)
61 | }
62 |
63 | async function getProxyContract(name) {
64 | const deployment = await companionNetworks['proxy'].deployments.get(name)
65 | const signer = await getSigner('proxy', deployment.receipt.from)
66 | return new ethers.Contract(deployment.address, deployment.abi, signer)
67 | }
68 |
69 | async function getL1Contract(name) {
70 | const deployment = await companionNetworks['l1'].deployments.get(name)
71 | const signer = await companionNetworks['l1'].deployments.getSigner(deployment.receipt.from)
72 | patchProviderPolling(signer.provider)
73 | return new ethers.Contract(deployment.address, deployment.abi, signer)
74 | }
75 |
76 | async function getLogs(provider, filter) {
77 | if (filter.fromBlock === undefined) {
78 | filter.fromBlock = 0
79 | }
80 | while (true) {
81 | try {
82 | logs = await provider.getLogs(filter)
83 | } catch (e) {
84 | if (e.code == -32011) {
85 | // ProviderError: no backends available for method
86 | continue
87 | } else if (e.code == -32614) {
88 | // ProviderError: eth_getLogs is limited to a 10,000 range
89 | if (filter.fromBlock != 0) {
90 | filter.toBlock = filter.fromBlock + 10_000;
91 | } else if (filter.toBlock === undefined) {
92 | filter.fromBlock = await provider.getBlockNumber() - 9900;
93 | } else {
94 | // unfixable
95 | throw e;
96 | }
97 | } else if (e.code == -32000) {
98 | // ProviderError: block range too large
99 | throw e;
100 | } else {
101 | throw e;
102 | }
103 | continue
104 | }
105 | return logs
106 | }
107 | }
108 |
109 | function beaconGenesisTimestamp(chainId) {
110 | if (!isL1ChainId(chainId)) {
111 | throw new NotL1Network(chainId)
112 | }
113 | return BEACON_GENESIS_TIMESTAMP[chainId]
114 | }
115 |
116 | function timestampToSlot(timestamp, chainId) {
117 | if (!isL1ChainId(chainId)) {
118 | throw new NotL1Network(chainId)
119 | }
120 | const timeDiff = timestamp - BEACON_GENESIS_TIMESTAMP[chainId]
121 | if (timeDiff % TIME_PER_SLOT != 0) {
122 | throw new UnexpectedSlotTime(timestamp)
123 | }
124 | return timeDiff / TIME_PER_SLOT
125 | }
126 |
127 | function slotToTimestamp(slot, chainId) {
128 | if (!isL1ChainId(chainId)) {
129 | throw new NotL1Network(chainId)
130 | }
131 | return BEACON_GENESIS_TIMESTAMP[chainId] + TIME_PER_SLOT * slot
132 | }
133 |
134 | module.exports = {
135 | getSigner,
136 | getProxyContract,
137 | getLogs,
138 | getL1Contract,
139 | getL1Provider,
140 | getMessengerName,
141 | patchProviderPolling,
142 | beaconGenesisTimestamp,
143 | timestampToSlot,
144 | slotToTimestamp,
145 | }
146 |
--------------------------------------------------------------------------------
/contracts/lib/MerkleTree.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.0;
6 |
7 | /**
8 | * @title Merkle Tree
9 | * @author Theori, Inc.
10 | * @notice Gas optimized SHA256 Merkle tree code.
11 | */
12 | library MerkleTree {
13 | /**
14 | * @notice performs one merkle combination of two node hashes
15 | */
16 | function combine(bytes32 left, bytes32 right) internal view returns (bytes32 result) {
17 | assembly {
18 | mstore(0, left)
19 | mstore(0x20, right)
20 | // compute sha256
21 | if iszero(staticcall(gas(), 0x2, 0x0, 0x40, 0x0, 0x20)) {
22 | revert(0, 0)
23 | }
24 | result := mload(0)
25 | }
26 | }
27 |
28 | /**
29 | * @notice computes a SHA256 merkle root of the provided hashes, in place
30 | * @param temp the mutable array of hashes
31 | * @return the merkle root hash
32 | */
33 | function computeRoot(bytes32[] memory temp) internal view returns (bytes32) {
34 | uint256 count = temp.length;
35 | assembly {
36 | // repeat until we arrive at one root hash
37 | for {
38 |
39 | } gt(count, 1) {
40 |
41 | } {
42 | let dataElementLocation := add(temp, 0x20)
43 | let hashElementLocation := add(temp, 0x20)
44 | for {
45 | let i := 0
46 | } lt(i, count) {
47 | i := add(i, 2)
48 | } {
49 | if iszero(
50 | staticcall(gas(), 0x2, hashElementLocation, 0x40, dataElementLocation, 0x20)
51 | ) {
52 | revert(0, 0)
53 | }
54 | dataElementLocation := add(dataElementLocation, 0x20)
55 | hashElementLocation := add(hashElementLocation, 0x40)
56 | }
57 | count := shr(1, count)
58 | }
59 | }
60 | return temp[0];
61 | }
62 |
63 | /**
64 | * @notice compute the root of the merkle tree according to the proof
65 | * @param index the index of the node to check
66 | * @param leaf the leaf to check
67 | * @param proofHashes the proof, i.e. the sequence of siblings from the
68 | * node to root
69 | */
70 | function proofRoot(
71 | uint256 index,
72 | bytes32 leaf,
73 | bytes32[] calldata proofHashes
74 | ) internal view returns (bytes32 result) {
75 | assembly {
76 | result := leaf
77 | let start := proofHashes.offset
78 | let end := add(start, mul(proofHashes.length, 0x20))
79 | for {
80 | let ptr := start
81 | } lt(ptr, end) {
82 | ptr := add(ptr, 0x20)
83 | } {
84 | let proofHash := calldataload(ptr)
85 |
86 | // use scratch space (0x0 - 0x40) for hash input
87 | switch and(index, 1)
88 | case 0 {
89 | mstore(0x0, result)
90 | mstore(0x20, proofHash)
91 | }
92 | case 1 {
93 | mstore(0x0, proofHash)
94 | mstore(0x20, result)
95 | }
96 |
97 | // compute sha256
98 | if iszero(staticcall(gas(), 0x2, 0x0, 0x40, 0x0, 0x20)) {
99 | revert(0, 0)
100 | }
101 | result := mload(0x0)
102 |
103 | index := shr(1, index)
104 | }
105 | }
106 | require(index == 0, "invalid index for proof");
107 | }
108 |
109 | /**
110 | * @notice compute the root of the merkle tree containing the given leaf
111 | * at index 0 and default values for all other leaves
112 | * @param depth the depth of the tree
113 | * @param leaf the non-default leaf
114 | * @param defaultLeaf the default leaf for all other positions
115 | */
116 | function rootWithDefault(
117 | uint256 depth,
118 | bytes32 leaf,
119 | bytes32 defaultLeaf
120 | ) internal view returns (bytes32 result) {
121 | assembly {
122 | result := leaf
123 | // the default value will live at 0x20 and be updated each iteration
124 | mstore(0x20, defaultLeaf)
125 | for { } depth { depth := sub(depth, 1) } {
126 | // compute sha256 of result || default
127 | mstore(0x0, result)
128 | if iszero(staticcall(gas(), 0x2, 0x0, 0x40, 0x0, 0x20)) {
129 | revert(0, 0)
130 | }
131 | result := mload(0x0)
132 | if iszero(depth) {
133 | break
134 | }
135 |
136 | // compute sha256 of default || default
137 | mstore(0x0, mload(0x20))
138 | if iszero(staticcall(gas(), 0x2, 0x0, 0x40, 0x20, 0x20)) {
139 | revert(0, 0)
140 | }
141 | }
142 | }
143 | }
144 |
145 | /**
146 | * @notice check if a hash is in the merkle tree for rootHash
147 | * @param rootHash the merkle root
148 | * @param index the index of the node to check
149 | * @param hash the hash to check
150 | * @param proofHashes the proof, i.e. the sequence of siblings from the
151 | * node to root
152 | */
153 | function validProof(
154 | bytes32 rootHash,
155 | uint256 index,
156 | bytes32 hash,
157 | bytes32[] calldata proofHashes
158 | ) internal view returns (bool result) {
159 | return rootHash == proofRoot(index, hash, proofHashes);
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/utils/blockproof.js:
--------------------------------------------------------------------------------
1 | const {
2 | arrayify, concat, defaultAbiCoder, sha256, keccak256,
3 | solidityPack, ParamType, RLP
4 | } = require("ethers/lib/utils");
5 |
6 | const MERKLE_PROOF_TYPE = 0;
7 | const SNARK_PROOF_TYPE = 1;
8 |
9 | function buildMerkleRoot(hashes) {
10 | let count = hashes.length;
11 | let temp = new Array(count / 2);
12 | for (let i = 0; i < count; i += 2) {
13 | temp[i >> 1] = sha256(concat([hashes[i], hashes[i + 1]]));
14 | }
15 | count >>= 1;
16 | while (count > 1) {
17 | for (let i = 0; i < count; i += 2) {
18 | temp[i >> 1] = sha256(concat([temp[i], temp[i + 1]]));
19 | }
20 | count >>= 1;
21 | }
22 | return temp[0];
23 | }
24 |
25 | function buildMerkleProof(hashes, idx) {
26 | let count = hashes.length;
27 | let temp = new Array(count / 2);
28 | let proof = new Array();
29 | for (let i = 0; i < count; i += 2) {
30 | if (idx == i) {
31 | proof.push(hashes[i + 1]);
32 | } else if (idx == i + 1) {
33 | proof.push(hashes[i]);
34 | }
35 | temp[i >> 1] = sha256(concat([hashes[i], hashes[i + 1]]));
36 | }
37 | idx >>= 1;
38 | count >>= 1;
39 | while (count > 1) {
40 | for (let i = 0; i < count; i += 2) {
41 | if (idx == i) {
42 | proof.push(temp[i + 1]);
43 | } else if (idx == i + 1) {
44 | proof.push(temp[i]);
45 | }
46 | temp[i >> 1] = sha256(concat([temp[i], temp[i + 1]]));
47 | }
48 | count >>= 1;
49 | idx >>= 1;
50 | }
51 | return proof;
52 | }
53 |
54 | function validMerkleProof(root, idx, hash, proofHashes) {
55 | let constructedHash = hash;
56 | for (let i = 0; i < proofHashes.length; i++) {
57 | if (idx & 1) {
58 | constructedHash = sha256(concat([proofHashes[i], constructedHash]));
59 | } else {
60 | constructedHash = sha256(concat([constructedHash, proofHashes[i]]));
61 | }
62 | idx >>= 1;
63 | }
64 | return root == constructedHash;
65 | }
66 |
67 | function headerRlp(header) {
68 | let list = [
69 | header.parentHash,
70 | header.sha3Uncles,
71 | header.miner,
72 | header.stateRoot,
73 | header.transactionsRoot,
74 | header.receiptsRoot,
75 | header.logsBloom,
76 | header.difficulty,
77 | header.number,
78 | header.gasLimit,
79 | header.gasUsed,
80 | header.timestamp,
81 | header.extraData,
82 | header.mixHash,
83 | header.nonce,
84 | ];
85 | if (header.baseFeePerGas) {
86 | list.push(header.baseFeePerGas);
87 | }
88 | if (header.withdrawalsRoot) {
89 | list.push(header.withdrawalsRoot);
90 | }
91 | if (header.blobGasUsed) {
92 | list.push(header.blobGasUsed);
93 | }
94 | if (header.excessBlobGas) {
95 | list.push(header.excessBlobGas);
96 | }
97 | if (header.parentBeaconBlockRoot) {
98 | list.push(header.parentBeaconBlockRoot);
99 | }
100 |
101 | list = list.map((v) => {
102 | if (v == "0x0") {
103 | return "0x";
104 | }
105 |
106 | if (v.length % 2 == 0) {
107 | return v;
108 | } else {
109 | return "0x0" + v.substring(2);
110 | }
111 | });
112 | return RLP.encode(list)
113 | }
114 |
115 |
116 | function encodeValidBlockMerkleProof(wrap, merkle) {
117 | let writer = defaultAbiCoder._getWriter();
118 | let type = ParamType.from("bytes32[]");
119 | defaultAbiCoder._getCoder(type).encode(writer, merkle);
120 | let proof = writer.data;
121 | if (wrap) return solidityPack(["uint8", "bytes"], [MERKLE_PROOF_TYPE, proof]);
122 | return proof;
123 | }
124 |
125 | async function encodeValidBlockSNARKProof(signer, wrap, numBlocks, endBlock, snark, merkle) {
126 | let sig = signer == null ? "0x" : await signProof(signer, snark);
127 | let proof = defaultAbiCoder.encode(
128 | [
129 | "uint256", "uint256",
130 | "tuple(tuple(uint256[34] base, uint256[16] subproofLimbs, uint256[] inputs), bytes)",
131 | "bytes32[]"
132 | ],
133 | [numBlocks, endBlock, [snark, sig], merkle]
134 | );
135 | if (wrap) return solidityPack(["uint8", "bytes"], [SNARK_PROOF_TYPE, proof]);
136 | return proof;
137 | }
138 |
139 | async function signProof(signer, proof) {
140 | let data = solidityPack(
141 | ["uint256[34]", "uint256[16]", "uint256[]"],
142 | [proof.base, proof.subproofLimbs, proof.inputs]
143 | );
144 | let hash = keccak256(data);
145 | return signer.signMessage(arrayify(hash));
146 | }
147 |
148 | function byteReverse(input) {
149 | let v = ethers.BigNumber.from(input);
150 |
151 | const FLIP_MASK = ethers.BigNumber.from("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")
152 | function not(x) {
153 | return x.xor(FLIP_MASK)
154 | }
155 |
156 | const MASK08 = ethers.BigNumber.from("0xFF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00");
157 | const MASK16 = ethers.BigNumber.from("0xFFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000");
158 | const MASK32 = ethers.BigNumber.from("0xFFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000")
159 | const MASK64 = ethers.BigNumber.from("0xFFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF0000000000000000");
160 |
161 | // swap bytes
162 | v = v.and(MASK08).shr(8).or((v.and(not(MASK08))).shl(8));
163 |
164 | // swap 2-byte long pairs
165 | v = v.and(MASK16).shr(16).or(v.and(not(MASK16)).shl(16));
166 |
167 | // swap 4-byte long pairs
168 | v = v.and(MASK32).shr(32).or(v.and(not(MASK32)).shl(32));
169 |
170 | // swap 8-byte long pairs
171 | v = v.and(MASK64).shr(64).or(v.and(not(MASK64)).shl(64));
172 |
173 | // swap 16-byte long pairs
174 | v = v.shr(128).or(v.shl(128));
175 | }
176 |
177 | function readHashWords(words) {
178 | const mask = ethers.BigNumber.from("0xffffffffffffffff");
179 | result = words[0].and(mask)
180 | .or(words[1].and(mask).shl(0x40))
181 | .or(words[2].and(mask).shl(0x80))
182 | .or(words[3].and(mask).shl(0xc0));
183 | return byteReverse(result);
184 | }
185 |
186 | module.exports = {
187 | buildMerkleRoot, buildMerkleProof, validMerkleProof,
188 | encodeValidBlockMerkleProof, encodeValidBlockSNARKProof,
189 | signProof, headerRlp, readHashWords
190 | }
191 |
--------------------------------------------------------------------------------
/utils/blockhistory.js:
--------------------------------------------------------------------------------
1 | const { ethers } = require("hardhat")
2 | const { getLogs, getL1Provider, patchProviderPolling, slotToTimestamp } = require("./network")
3 |
4 | const NEGATIVE_ONE = ethers.BigNumber.from(-1)
5 |
6 | function max(vals) {
7 | return vals.reduce((l, r) => l.gt(r) ? l : r)
8 | }
9 |
10 | async function getLastMerkleRootBlock(blockHistory) {
11 | if (!blockHistory.filters.ImportMerkleRoot) {
12 | return NEGATIVE_ONE
13 | }
14 | const logs = await getLogs(
15 | blockHistory.provider,
16 | blockHistory.filters.ImportMerkleRoot()
17 | )
18 | if (logs.length == 0) {
19 | return NEGATIVE_ONE
20 | }
21 | const vals = logs.map((l) => {
22 | const rootIdx = ethers.BigNumber.from(logs[logs.length - 1].topics[1])
23 | return rootIdx.add(1).mul(8192).sub(1)
24 | })
25 | return max(vals)
26 | }
27 |
28 | async function getLastSummaryBlock(blockHistory) {
29 | if (!blockHistory.filters.ImportBlockSummary) {
30 | return NEGATIVE_ONE
31 | }
32 | const logs = await getLogs(
33 | blockHistory.provider,
34 | blockHistory.filters.ImportBlockSummary()
35 | )
36 | if (logs.length == 0) {
37 | return NEGATIVE_ONE
38 | }
39 | const vals = logs.map((l) => {
40 | const slot = ethers.BigNumber.from(logs[logs.length - 1].topics[1])
41 | return slot.sub(1)
42 | })
43 | const maxSlot = max(vals)
44 | const { chainId } = await getL1Provider().getNetwork()
45 | const maxTimestamp = slotToTimestamp(maxSlot.toNumber(), chainId)
46 | const maxBlock = await blockForTimestamp(getL1Provider(), maxTimestamp)
47 | return ethers.BigNumber.from(maxBlock.number)
48 | }
49 |
50 | async function getLastTrustedBlock(blockHistory) {
51 | if (!blockHistory.filters.TrustedBlockHash) {
52 | return NEGATIVE_ONE
53 | }
54 | const logs = await getLogs(
55 | blockHistory.provider,
56 | blockHistory.filters.TrustedBlockHash()
57 | )
58 | if (logs.length == 0) {
59 | return NEGATIVE_ONE
60 | }
61 | const vals = logs.map((l) => {
62 | const [blockNum] = ethers.utils.defaultAbiCoder.decode(
63 | ["uint256", "bytes32"],
64 | logs[logs.length - 1].data
65 | )
66 | return blockNum
67 | })
68 | return max(vals)
69 | }
70 |
71 | async function getLastPrecomiitedBlock(blockHistory) {
72 | if (!blockHistory.filters.PrecomittedBlock) {
73 | return NEGATIVE_ONE
74 | }
75 | const logs = await getLogs(
76 | blockHistory.provider,
77 | blockHistory.filters.PrecomittedBlock()
78 | )
79 | if (logs.length == 0) {
80 | return NEGATIVE_ONE
81 | }
82 | const vals = logs.map((l) => {
83 | return ethers.BigNumber.from(logs[logs.length - 1].topics[1])
84 | })
85 | return max(vals)
86 | }
87 |
88 | async function getLastVerifiableBlock(blockHistory) {
89 | let legacy = null;
90 | if (blockHistory.preDencunBlockHistory) {
91 | legacy = await blockHistory.preDencunBlockHistory()
92 | }
93 | const vals = await Promise.all([
94 | legacy ? getLastVerifiableBlock(legacy): NEGATIVE_ONE,
95 | getLastMerkleRootBlock(blockHistory),
96 | getLastSummaryBlock(blockHistory),
97 | getLastTrustedBlock(blockHistory),
98 | getLastPrecomiitedBlock(blockHistory)
99 | ])
100 | // return the max
101 | return max(vals)
102 | }
103 |
104 | async function findImportedBlockNumber(minBlockNum) {
105 | const l2BlockHistory = await ethers.getContract("BlockHistory")
106 | patchProviderPolling(l2BlockHistory.provider)
107 | if (l2BlockHistory.filters.TrustedBlockHash) {
108 | const logs = await getLogs(
109 | l2BlockHistory.provider,
110 | l2BlockHistory.filters.TrustedBlockHash()
111 | )
112 | for (const log of logs) {
113 | const parsed = l2BlockHistory.interface.parseLog(log)
114 | if (parsed.args.number.toNumber() >= minBlockNum) {
115 | return parsed.args.number.toNumber()
116 | }
117 | }
118 | } else if (l2BlockHistory.filters.PrecomittedBlock) {
119 | const logs = await getLogs(
120 | l2BlockHistory.provider,
121 | l2BlockHistory.filters.PrecomittedBlock()
122 | )
123 | for (const log of logs) {
124 | const parsed = l2BlockHistory.interface.parseLog(log)
125 | if (parsed.args.blockNum.toNumber() >= minBlockNum) {
126 | return parsed.args.blockNum.toNumber()
127 | }
128 | }
129 | }
130 | return null
131 | }
132 |
133 | async function blockForTimestamp(provider, timestamp) {
134 | const current = await provider.getBlock('latest')
135 | if (current.timestamp < timestamp) throw new Error('timestamp after latest block')
136 | let start = await provider.getBlock(1)
137 | let end = current
138 |
139 | while (end.number - start.number > 1) {
140 | let quantile =
141 | (timestamp - start.timestamp) / (end.timestamp - start.timestamp)
142 | let nextNum =
143 | start.number + Math.floor((end.number - start.number) * quantile)
144 | if (nextNum == start.number) nextNum++
145 | if (nextNum == end.number) nextNum--
146 | let next = await provider.getBlock(nextNum)
147 | if (next.timestamp > timestamp) {
148 | end = next
149 | } else {
150 | start = next
151 | }
152 | }
153 | return start
154 | }
155 |
156 | async function waitForTrustedImport(block) {
157 | const header = await getL1Provider().getBlock(block)
158 | const proxyBlockHistory = await ethers.getContract("BlockHistory")
159 | patchProviderPolling(proxyBlockHistory.provider)
160 | const fromBlock = await blockForTimestamp(
161 | proxyBlockHistory.provider,
162 | header.timestamp
163 | ).then((b) => b.number)
164 | const filter = proxyBlockHistory.filters.PrecomittedBlock(header.number)
165 | while (true) {
166 | // query logs after setting up listener to avoid races
167 | const logs = await getLogs(proxyBlockHistory.provider, { ...filter, fromBlock })
168 | if (logs.length > 0) {
169 | return
170 | }
171 | await new Promise(res => setTimeout(res, 30000))
172 | }
173 | }
174 |
175 | async function getLastCachedSummary() {
176 | const proxyBlockHistory = await ethers.getContract("BlockHistory")
177 | patchProviderPolling(proxyBlockHistory.provider)
178 | const logs = await getLogs(
179 | proxyBlockHistory.provider,
180 | proxyBlockHistory.filters.ImportBlockSummary()
181 | )
182 | if (logs.length == 0) {
183 | return NEGATIVE_ONE
184 | }
185 | return max(logs.map(log => ethers.BigNumber.from(log.topics[1])))
186 | }
187 |
188 | module.exports = {
189 | blockForTimestamp,
190 | getLastVerifiableBlock,
191 | getLastCachedSummary,
192 | findImportedBlockNumber,
193 | waitForTrustedImport,
194 | }
195 |
--------------------------------------------------------------------------------
/contracts/provers/AttendanceProver.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.12;
6 |
7 | import "@openzeppelin/contracts/access/Ownable.sol";
8 | import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
9 | import "@openzeppelin/contracts/utils/Strings.sol";
10 |
11 | import "../interfaces/IReliquary.sol";
12 | import "../RelicToken.sol";
13 | import "../lib/FactSigs.sol";
14 |
15 | struct EventInfo {
16 | address signer;
17 | uint32 capacity;
18 | uint48 deadline;
19 | mapping(uint256 => uint256) claimed;
20 | }
21 |
22 | /**
23 | * @title Prover for attendance/participation
24 | * @notice AttendanceProver verifies statements signed by trusted sources
25 | * to assign attendance Artifacts to accounts
26 | */
27 | contract AttendanceProver is Ownable {
28 | IReliquary immutable reliquary;
29 | RelicToken immutable token;
30 | address public outerSigner;
31 | mapping(uint64 => EventInfo) public events;
32 |
33 | /**
34 | * @notice Emitted when a new event which may be attended is created
35 | * @param eventId The unique id of this event
36 | * @param deadline The timestamp after which no further attendance requests
37 | * will be processed
38 | * @param factSig The fact signature of this particular event
39 | */
40 | event NewEvent(uint64 eventId, uint48 deadline, FactSignature factSig);
41 |
42 | /**
43 | * @notice Creates a new attendance prover
44 | * @param _reliquary The Reliquary in which this prover resides
45 | * @param _token The Artifact producer associated with this prover
46 | */
47 | constructor(IReliquary _reliquary, RelicToken _token) Ownable() {
48 | reliquary = _reliquary;
49 | token = _token;
50 | }
51 |
52 | /**
53 | * @notice Sets the signer for the attestation that a request was made
54 | * by a particular account.
55 | * @param _outerSigner The address corresponding to the signer
56 | */
57 | function setOuterSigner(address _outerSigner) external onlyOwner {
58 | outerSigner = _outerSigner;
59 | }
60 |
61 | /**
62 | * @notice Add a new event which may be attended
63 | * @param eventId The unique eventId for the new event
64 | * @param signer The address for the signer which attests the claim code
65 | * is valid
66 | * @param deadline The timestamp after which no further attendance requests
67 | * will be processed
68 | * @param capacity The initial maximum number of attendees which can claim codes
69 | * @dev Emits NewEvent
70 | */
71 | function addEvent(
72 | uint64 eventId,
73 | address signer,
74 | uint48 deadline,
75 | uint32 capacity
76 | ) external onlyOwner {
77 | require(deadline > block.timestamp, "deadline already passed");
78 | EventInfo storage eventInfo = events[eventId];
79 | require(eventInfo.signer == address(0), "eventID exists");
80 | require(signer != address(0), "invalid signer");
81 |
82 | eventInfo.signer = signer;
83 | eventInfo.capacity = capacity;
84 | eventInfo.deadline = deadline;
85 | for (uint256 i = 0; i < capacity; i += 256) {
86 | eventInfo.claimed[i >> 8] = ~uint256(0);
87 | }
88 | emit NewEvent(eventId, deadline, FactSigs.eventFactSig(eventId));
89 | }
90 |
91 | function increaseCapacity(uint64 eventId, uint32 newCapacity) external onlyOwner {
92 | EventInfo storage eventInfo = events[eventId];
93 | require(eventInfo.signer != address(0), "invalid eventID");
94 |
95 | for (uint256 i = ((eventInfo.capacity + 255) & ~uint32(0xff)); i < newCapacity; i += 256) {
96 | events[eventId].claimed[i >> 8] = ~uint256(0);
97 | }
98 | eventInfo.capacity = newCapacity;
99 | }
100 |
101 | /**
102 | * @notice Checks the signer of a message created in accordance with eth_signMessage
103 | * @param data The data which was signed
104 | * @param signature The public ECDSA signature
105 | * @return The address of the signer
106 | */
107 | function getSigner(bytes memory data, bytes memory signature) internal pure returns (address) {
108 | bytes32 msgHash = keccak256(
109 | abi.encodePacked("\x19Ethereum Signed Message:\n", Strings.toString(data.length), data)
110 | );
111 | return ECDSA.recover(msgHash, signature);
112 | }
113 |
114 | /**
115 | * @notice Prove attendance for an event and claim the associated conveyances
116 | * @param account The account making the claim of attendance
117 | * @param eventId The event which was attended
118 | * @param number The unique id which may be redeemed only once from the event
119 | * @param signatureInner The signature attesting that the number and eventId are valid
120 | * @param signatureOuter The signature attesting that the account is the claimer of
121 | * the presented information
122 | * @dev Issues a fact in the Reliquary with the fact signature for this event
123 | * @dev Issues a soul-bound NFT Artifact for attending the event
124 | */
125 | function claim(
126 | address account,
127 | uint64 eventId,
128 | uint64 number,
129 | bytes memory signatureInner,
130 | bytes memory signatureOuter
131 | ) external payable {
132 | reliquary.checkProveFactFee{value: msg.value}(msg.sender);
133 |
134 | EventInfo storage eventInfo = events[eventId];
135 |
136 | require(eventInfo.signer != address(0), "invalid eventID");
137 | require(eventInfo.deadline >= block.timestamp, "claim expired");
138 | require(eventInfo.capacity > number, "id exceeds capacity");
139 |
140 | uint256 index = number / 256;
141 | uint64 bit = number % 256;
142 |
143 | uint256 oldslot = eventInfo.claimed[index];
144 | require((oldslot & (1 << bit)) != 0, "already claimed");
145 |
146 | bytes memory encoded = abi.encode(uint256(block.chainid), eventId, number);
147 | address signer = getSigner(encoded, signatureInner);
148 | require(signer == eventInfo.signer, "invalid inner signer");
149 |
150 | encoded = abi.encodePacked(signatureInner, account);
151 | signer = getSigner(encoded, signatureOuter);
152 | require(signer == outerSigner, "invalid outer signer");
153 |
154 | oldslot &= ~(1 << bit);
155 | eventInfo.claimed[index] = oldslot;
156 |
157 | FactSignature sig = FactSigs.eventFactSig(eventId);
158 | (bool proven, , ) = reliquary.getFact(account, sig);
159 | if (!proven) {
160 | bytes memory data = abi.encodePacked(
161 | uint32(number),
162 | uint48(block.number),
163 | uint64(block.timestamp)
164 | );
165 | reliquary.setFact(account, sig, data);
166 | token.mint(account, uint96(eventId));
167 | }
168 | }
169 | }
170 |
--------------------------------------------------------------------------------
/contracts/EphemeralFacts.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.0;
6 |
7 | import "@openzeppelin/contracts/utils/introspection/IERC165.sol";
8 |
9 | import "./interfaces/IRelicReceiver.sol";
10 | import "./interfaces/IReliquary.sol";
11 | import "./interfaces/IProver.sol";
12 | import "./interfaces/IBatchProver.sol";
13 | import "./interfaces/IEphemeralFacts.sol";
14 | import "./lib/Callbacks.sol";
15 |
16 | /**
17 | * @title EphemeralFacts
18 | * @author Theori, Inc.
19 | * @notice EphemeralFacts provides delivery of ephemeral facts: facts which are
20 | * passed directly to external receivers, rather than stored in the
21 | * Reliquary. It also allows placing bounties on specific fact proof
22 | * requests, which can be used to build a fact proving relay system.
23 | * Batch provers are supported, enabling an efficient request + relay
24 | * system using proof aggregation.
25 | */
26 | contract EphemeralFacts is IEphemeralFacts {
27 | IReliquary immutable reliquary;
28 |
29 | /// @dev track the bounty associated with each fact request
30 | mapping(bytes32 => uint256) public bounties;
31 |
32 | /// @dev track the collected bounties for the fact relayers
33 | mapping(address => uint256) public balance;
34 |
35 | constructor(IReliquary _reliquary) {
36 | reliquary = _reliquary;
37 | }
38 |
39 | /**
40 | * @dev collects the accumulated bounties for the caller
41 | */
42 | function collect() external {
43 | uint256 amount = balance[msg.sender];
44 | delete balance[msg.sender];
45 | (bool success, ) = msg.sender.call{value: amount}("");
46 | require(success, "bounty transfer failed");
47 | }
48 |
49 | /**
50 | * @dev computes the unique requestId for this fact request, used to track bounties
51 | * @param account the account associated with the fact
52 | * @param sig the fact signature
53 | * @param context context about the fact receiver callback
54 | */
55 | function requestId(
56 | address account,
57 | FactSignature sig,
58 | ReceiverContext memory context
59 | ) public pure returns (bytes32) {
60 | return keccak256(abi.encode(account, sig, context));
61 | }
62 |
63 | /**
64 | * @notice delivers the fact to the receiver, claiming any pending bounty on the request
65 | * @param context the contract to receive the fact
66 | * @param fact the fact information
67 | */
68 | function deliverFact(ReceiverContext calldata context, Fact memory fact) internal {
69 | bytes32 rid = requestId(fact.account, fact.sig, context);
70 | uint256 bounty = bounties[rid];
71 | require(
72 | context.initiator == msg.sender || bounty > 0,
73 | "cannot specify an initiator which didn't request the fact"
74 | );
75 | if (bounty > 0) {
76 | delete bounties[rid];
77 | emit BountyClaimed(msg.sender, rid, bounty);
78 | balance[msg.sender] += bounty;
79 | }
80 | bytes memory data = abi.encodeWithSelector(
81 | IRelicReceiver.receiveFact.selector,
82 | context.initiator,
83 | fact,
84 | context.extra
85 | );
86 | (bool success, bytes memory result) = Callbacks.callWithExactGas(
87 | context.gasLimit,
88 | address(context.receiver),
89 | data
90 | );
91 | if (success) {
92 | emit ReceiveSuccess(context.receiver, rid);
93 | } else if (context.requireSuccess) {
94 | Callbacks.revertWithData(result);
95 | } else {
96 | emit ReceiveFailure(context.receiver, rid);
97 | }
98 | }
99 |
100 | /**
101 | * @notice proves a fact ephemerally and provides it to the receiver
102 | * @param context the ReceiverContext for delivering the fact
103 | * @param prover the prover module to use, must implement IProver
104 | * @param proof the proof to pass to the prover
105 | */
106 | function proveEphemeral(
107 | ReceiverContext calldata context,
108 | address prover,
109 | bytes calldata proof
110 | ) external payable {
111 | // reverts if the prover doesn't exist or is revoked
112 | reliquary.checkProver(reliquary.provers(prover));
113 |
114 | // reverts if the prover doesn't support standard interface
115 | require(
116 | IERC165(prover).supportsInterface(type(IProver).interfaceId),
117 | "Prover doesn't implement IProver"
118 | );
119 |
120 | Fact memory fact = IProver(prover).prove{value: msg.value}(proof, false);
121 | deliverFact(context, fact);
122 | }
123 |
124 | /**
125 | * @notice proves a batch of facts ephemerally and provides them to the receivers
126 | * @param contexts the ReceiverContexts for delivering the facts
127 | * @param prover the prover module to use, must implement IBatchProver
128 | * @param proof the proof to pass to the prover
129 | */
130 | function batchProveEphemeral(
131 | ReceiverContext[] calldata contexts,
132 | address prover,
133 | bytes calldata proof
134 | ) external payable {
135 | // reverts if the prover doesn't exist or is revoked
136 | reliquary.checkProver(reliquary.provers(prover));
137 |
138 | // reverts if the prover doesn't support standard interface
139 | require(
140 | IERC165(prover).supportsInterface(type(IBatchProver).interfaceId),
141 | "Prover doesn't implement IBatchProver"
142 | );
143 |
144 | Fact[] memory facts = IBatchProver(prover).proveBatch{value: msg.value}(proof, false);
145 | require(facts.length == contexts.length);
146 |
147 | for (uint256 i = 0; i < facts.length; i++) {
148 | deliverFact(contexts[i], facts[i]);
149 | }
150 | }
151 |
152 | /**
153 | * @notice requests a fact to be proven asynchronously and passed to the receiver,
154 | * @param account the account associated with the fact
155 | * @param sigData the fact data which determines the fact signature (class is assumed to be NO_FEE)
156 | * @param receiver the contract to receive the fact
157 | * @param data the extra data to pass to the receiver
158 | * @param gasLimit the maxmium gas used by the receiver
159 | * @dev msg.value is added to the bounty for this fact request,
160 | * incentivizing somebody to prove it
161 | */
162 | function requestFact(
163 | address account,
164 | bytes calldata sigData,
165 | IRelicReceiver receiver,
166 | bytes calldata data,
167 | uint256 gasLimit
168 | ) external payable {
169 | FactSignature sig = Facts.toFactSignature(Facts.NO_FEE, sigData);
170 |
171 | // create the receiver context for the fact proof request
172 | // note that initiator and requireSuccess are hardcoded
173 | ReceiverContext memory context = ReceiverContext(
174 | msg.sender,
175 | receiver,
176 | data,
177 | gasLimit,
178 | false
179 | );
180 |
181 | uint256 bounty = bounties[requestId(account, sig, context)] += msg.value;
182 | emit FactRequested(FactDescription(account, sigData), context, bounty);
183 | }
184 | }
185 |
--------------------------------------------------------------------------------
/contracts/lib/AnemoiJive.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.0;
6 |
7 | /**
8 | * @title AnemoiJive
9 | * @author Theori, Inc.
10 | * @notice Implementation of the Anemoi hash function and Jive mode of operation
11 | */
12 | library AnemoiJive {
13 | uint256 constant beta = 5;
14 | uint256 constant alpha_inv =
15 | 17510594297471420177797124596205820070838691520332827474958563349260646796493;
16 | uint256 constant q =
17 | 21888242871839275222246405745257275088548364400416034343698204186575808495617;
18 | uint256 constant delta =
19 | 8755297148735710088898562298102910035419345760166413737479281674630323398247;
20 |
21 | function CD(uint256 round) internal pure returns (uint256, uint256) {
22 | if (round == 0)
23 | return (
24 | 37,
25 | 8755297148735710088898562298102910035419345760166413737479281674630323398284
26 | );
27 | if (round == 1)
28 | return (
29 | 13352247125433170118601974521234241686699252132838635793584252509352796067497,
30 | 5240474505904316858775051800099222288270827863409873986701694203345984265770
31 | );
32 | if (round == 2)
33 | return (
34 | 8959866518978803666083663798535154543742217570455117599799616562379347639707,
35 | 9012679925958717565787111885188464538194947839997341443807348023221726055342
36 | );
37 | if (round == 3)
38 | return (
39 | 3222831896788299315979047232033900743869692917288857580060845801753443388885,
40 | 21855834035835287540286238525800162342051591799629360593177152465113152235615
41 | );
42 | if (round == 4)
43 | return (
44 | 11437915391085696126542499325791687418764799800375359697173212755436799377493,
45 | 11227229470941648605622822052481187204980748641142847464327016901091886692935
46 | );
47 | if (round == 5)
48 | return (
49 | 14725846076402186085242174266911981167870784841637418717042290211288365715997,
50 | 8277823808153992786803029269162651355418392229624501612473854822154276610437
51 | );
52 | if (round == 6)
53 | return (
54 | 3625896738440557179745980526949999799504652863693655156640745358188128872126,
55 | 20904607884889140694334069064199005451741168419308859136555043894134683701950
56 | );
57 | if (round == 7)
58 | return (
59 | 463291105983501380924034618222275689104775247665779333141206049632645736639,
60 | 1902748146936068574869616392736208205391158973416079524055965306829204527070
61 | );
62 | if (round == 8)
63 | return (
64 | 17443852951621246980363565040958781632244400021738903729528591709655537559937,
65 | 14452570815461138929654743535323908350592751448372202277464697056225242868484
66 | );
67 | if (round == 9)
68 | return (
69 | 10761214205488034344706216213805155745482379858424137060372633423069634639664,
70 | 10548134661912479705005015677785100436776982856523954428067830720054853946467
71 | );
72 | if (round == 10)
73 | return (
74 | 1555059412520168878870894914371762771431462665764010129192912372490340449901,
75 | 17068729307795998980462158858164249718900656779672000551618940554342475266265
76 | );
77 | if (round == 11)
78 | return (
79 | 7985258549919592662769781896447490440621354347569971700598437766156081995625,
80 | 16199718037005378969178070485166950928725365516399196926532630556982133691321
81 | );
82 | if (round == 12)
83 | return (
84 | 9570976950823929161626934660575939683401710897903342799921775980893943353035,
85 | 19148564379197615165212957504107910110246052442686857059768087896511716255278
86 | );
87 | if (round == 13)
88 | return (
89 | 17962366505931708682321542383646032762931774796150042922562707170594807376009,
90 | 5497141763311860520411283868772341077137612389285480008601414949457218086902
91 | );
92 | if (round == 14)
93 | return (
94 | 12386136552538719544323156650508108618627836659179619225468319506857645902649,
95 | 18379046272821041930426853913114663808750865563081998867954732461233335541378
96 | );
97 | if (round == 15)
98 | return (
99 | 21184636178578575123799189548464293431630680704815247777768147599366857217074,
100 | 7696001730141875853127759241422464241772355903155684178131833937483164915734
101 | );
102 | if (round == 16)
103 | return (
104 | 3021529450787050964585040537124323203563336821758666690160233275817988779052,
105 | 963844642109550260189938374814031216012862679737123536423540607519656220143
106 | );
107 | if (round == 17)
108 | return (
109 | 7005374570978576078843482270548485551486006385990713926354381743200520456088,
110 | 12412434690468911461310698766576920805270445399824272791985598210955534611003
111 | );
112 | if (round == 18)
113 | return (
114 | 3870834761329466217812893622834770840278912371521351591476987639109753753261,
115 | 6971318955459107915662273112161635903624047034354567202210253298398705502050
116 | );
117 | revert();
118 | }
119 |
120 | function expmod(
121 | uint256 base,
122 | uint256 e,
123 | uint256 m
124 | ) internal view returns (uint256 o) {
125 | assembly {
126 | // define pointer
127 | let p := mload(0x40)
128 | // store data assembly-favouring ways
129 | mstore(p, 0x20) // Length of Base
130 | mstore(add(p, 0x20), 0x20) // Length of Exponent
131 | mstore(add(p, 0x40), 0x20) // Length of Modulus
132 | mstore(add(p, 0x60), base) // Base
133 | mstore(add(p, 0x80), e) // Exponent
134 | mstore(add(p, 0xa0), m) // Modulus
135 | if iszero(staticcall(sub(gas(), 2000), 0x05, p, 0xc0, p, 0x20)) {
136 | revert(0, 0)
137 | }
138 | // data
139 | o := mload(p)
140 | }
141 | }
142 |
143 | function sbox(uint256 x, uint256 y) internal view returns (uint256, uint256) {
144 | x = addmod(x, q - mulmod(beta, mulmod(y, y, q), q), q);
145 | y = addmod(y, q - expmod(x, alpha_inv, q), q);
146 | x = addmod(addmod(x, mulmod(beta, mulmod(y, y, q), q), q), delta, q);
147 | return (x, y);
148 | }
149 |
150 | function ll(uint256 x, uint256 y) internal pure returns (uint256 r0, uint256 r1) {
151 | r0 = addmod(x, mulmod(5, y, q), q);
152 | r1 = addmod(y, mulmod(5, r0, q), q);
153 | }
154 |
155 | function compress(uint256 x, uint256 y) internal view returns (uint256) {
156 | uint256 sum = addmod(x, y, q);
157 | uint256 c;
158 | uint256 d;
159 | for (uint256 r = 0; r < 19; r++) {
160 | (c, d) = CD(r);
161 | x = addmod(x, c, q);
162 | y = addmod(y, d, q);
163 | (x, y) = ll(x, y);
164 | (x, y) = sbox(x, y);
165 | }
166 | (x, y) = ll(x, y);
167 | return addmod(addmod(x, y, q), sum, q);
168 | }
169 | }
170 |
--------------------------------------------------------------------------------
/contracts/interfaces/IReliquary.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.12;
6 |
7 | import "../lib/Facts.sol";
8 |
9 | interface IReliquary {
10 | event NewProver(address prover, uint64 version);
11 | event PendingProverAdded(address prover, uint64 version, uint64 timestamp);
12 | event ProverRevoked(address prover, uint64 version);
13 | event RoleAdminChanged(
14 | bytes32 indexed role,
15 | bytes32 indexed previousAdminRole,
16 | bytes32 indexed newAdminRole
17 | );
18 | event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender);
19 | event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender);
20 |
21 | struct ProverInfo {
22 | uint64 version;
23 | FeeInfo feeInfo;
24 | bool revoked;
25 | }
26 |
27 | enum FeeFlags {
28 | FeeNone,
29 | FeeNative,
30 | FeeCredits,
31 | FeeExternalDelegate,
32 | FeeExternalToken
33 | }
34 |
35 | struct FeeInfo {
36 | uint8 flags;
37 | uint16 feeCredits;
38 | // feeWei = feeWeiMantissa * pow(10, feeWeiExponent)
39 | uint8 feeWeiMantissa;
40 | uint8 feeWeiExponent;
41 | uint32 feeExternalId;
42 | }
43 |
44 | function ADD_PROVER_ROLE() external view returns (bytes32);
45 |
46 | function CREDITS_ROLE() external view returns (bytes32);
47 |
48 | function DEFAULT_ADMIN_ROLE() external view returns (bytes32);
49 |
50 | function DELAY() external view returns (uint64);
51 |
52 | function GOVERNANCE_ROLE() external view returns (bytes32);
53 |
54 | function SUBSCRIPTION_ROLE() external view returns (bytes32);
55 |
56 | function activateProver(address prover) external;
57 |
58 | function addCredits(address user, uint192 amount) external;
59 |
60 | function addProver(address prover, uint64 version) external;
61 |
62 | function addSubscriber(address user, uint64 ts) external;
63 |
64 | function assertValidBlockHash(
65 | address verifier,
66 | bytes32 hash,
67 | uint256 num,
68 | bytes memory proof
69 | ) external payable;
70 |
71 | function assertValidBlockHashFromProver(
72 | address verifier,
73 | bytes32 hash,
74 | uint256 num,
75 | bytes memory proof
76 | ) external view;
77 |
78 | function checkProveFactFee(address sender) external payable;
79 |
80 | function checkProver(ProverInfo memory prover) external pure;
81 |
82 | function credits(address user) external view returns (uint192);
83 |
84 | function debugValidBlockHash(
85 | address verifier,
86 | bytes32 hash,
87 | uint256 num,
88 | bytes memory proof
89 | ) external view returns (bool);
90 |
91 | function debugVerifyFact(address account, FactSignature factSig)
92 | external
93 | view
94 | returns (
95 | bool exists,
96 | uint64 version,
97 | bytes memory data
98 | );
99 |
100 | function factFees(uint8)
101 | external
102 | view
103 | returns (
104 | uint8 flags,
105 | uint16 feeCredits,
106 | uint8 feeWeiMantissa,
107 | uint8 feeWeiExponent,
108 | uint32 feeExternalId
109 | );
110 |
111 | function feeAccounts(address)
112 | external
113 | view
114 | returns (uint64 subscriberUntilTime, uint192 credits);
115 |
116 | function feeExternals(uint256) external view returns (address);
117 |
118 | function getFact(address account, FactSignature factSig)
119 | external
120 | view
121 | returns (
122 | bool exists,
123 | uint64 version,
124 | bytes memory data
125 | );
126 |
127 | function getProveFactNativeFee(address prover) external view returns (uint256);
128 |
129 | function getProveFactTokenFee(address prover) external view returns (uint256);
130 |
131 | function getRoleAdmin(bytes32 role) external view returns (bytes32);
132 |
133 | function getVerifyFactNativeFee(FactSignature factSig) external view returns (uint256);
134 |
135 | function getVerifyFactTokenFee(FactSignature factSig) external view returns (uint256);
136 |
137 | function grantRole(bytes32 role, address account) external;
138 |
139 | function hasRole(bytes32 role, address account) external view returns (bool);
140 |
141 | function initialized() external view returns (bool);
142 |
143 | function isSubscriber(address user) external view returns (bool);
144 |
145 | function pendingProvers(address) external view returns (uint64 timestamp, uint64 version);
146 |
147 | function provers(address) external view returns (ProverInfo memory);
148 |
149 | function removeCredits(address user, uint192 amount) external;
150 |
151 | function removeSubscriber(address user) external;
152 |
153 | function renounceRole(bytes32 role, address account) external;
154 |
155 | function resetFact(address account, FactSignature factSig) external;
156 |
157 | function revokeProver(address prover) external;
158 |
159 | function revokeRole(bytes32 role, address account) external;
160 |
161 | function setCredits(address user, uint192 amount) external;
162 |
163 | function setFact(
164 | address account,
165 | FactSignature factSig,
166 | bytes memory data
167 | ) external;
168 |
169 | function setFactFee(
170 | uint8 cls,
171 | FeeInfo memory feeInfo,
172 | address feeExternal
173 | ) external;
174 |
175 | function setInitialized() external;
176 |
177 | function setProverFee(
178 | address prover,
179 | FeeInfo memory feeInfo,
180 | address feeExternal
181 | ) external;
182 |
183 | function setValidBlockFee(FeeInfo memory feeInfo, address feeExternal) external;
184 |
185 | function supportsInterface(bytes4 interfaceId) external view returns (bool);
186 |
187 | function validBlockHash(
188 | address verifier,
189 | bytes32 hash,
190 | uint256 num,
191 | bytes memory proof
192 | ) external payable returns (bool);
193 |
194 | function validBlockHashFromProver(
195 | address verifier,
196 | bytes32 hash,
197 | uint256 num,
198 | bytes memory proof
199 | ) external view returns (bool);
200 |
201 | function verifyBlockFeeInfo()
202 | external
203 | view
204 | returns (
205 | uint8 flags,
206 | uint16 feeCredits,
207 | uint8 feeWeiMantissa,
208 | uint8 feeWeiExponent,
209 | uint32 feeExternalId
210 | );
211 |
212 | function verifyFact(address account, FactSignature factSig)
213 | external
214 | payable
215 | returns (
216 | bool exists,
217 | uint64 version,
218 | bytes memory data
219 | );
220 |
221 | function verifyFactNoFee(address account, FactSignature factSig)
222 | external
223 | view
224 | returns (
225 | bool exists,
226 | uint64 version,
227 | bytes memory data
228 | );
229 |
230 | function verifyFactVersion(address account, FactSignature factSig)
231 | external
232 | payable
233 | returns (bool exists, uint64 version);
234 |
235 | function verifyFactVersionNoFee(address account, FactSignature factSig)
236 | external
237 | view
238 | returns (bool exists, uint64 version);
239 |
240 | function versions(uint64) external view returns (address);
241 |
242 | function withdrawFees(address token, address dest) external;
243 | }
244 |
--------------------------------------------------------------------------------
/deploy/02_block_history.js:
--------------------------------------------------------------------------------
1 | const { config, companionNetworks, network } = require("hardhat");
2 | const { getMerkleRootsSlot } = require("../utils/slots");
3 | const { getMessengerName } = require("../utils/importL2");
4 | const { getL1Contract } = require("../utils/network");
5 |
6 | module.exports = async ({getNamedAccounts, deployments}) => {
7 | const {deploy} = deployments;
8 | const {deployer} = await getNamedAccounts();
9 | const Reliquary = await deployments.get("Reliquary");
10 | let skipIfAlreadyDeployed = true;
11 | if (network.config.bridged === true) {
12 | if (network.config.zksync === true) {
13 | const messenger = await getL1Contract(getMessengerName())
14 | const l1BlockHistory = await companionNetworks["l1"].deployments.get("LegacyBlockHistory")
15 | const merkleRootsSlot = getMerkleRootsSlot(l1BlockHistory)
16 | await deploy("BlockHistory", {
17 | contract: "ZkSyncProxyBlockHistory",
18 | from: deployer,
19 | args: [Reliquary.address, messenger.address, l1BlockHistory.address, merkleRootsSlot],
20 | log: true,
21 | skipIfAlreadyDeployed,
22 | });
23 |
24 | let legacyBlockHistory = await deployments.get("BlockHistory")
25 | if (legacyBlockHistory.devdoc.title.includes("Beacon")) {
26 | legacyBlockHistory = await deployments.get("LegacyBlockHistory")
27 | } else {
28 | deployments.save("LegacyBlockHistory", legacyBlockHistory);
29 | }
30 |
31 | let l1NetworkName = await companionNetworks["l1"].deployments.getNetworkName();
32 | let l1NetworkConfig = config.networks[l1NetworkName];
33 | await deploy("BlockHistory", {
34 | contract: "ZkSyncProxyBeaconBlockHistory",
35 | from: deployer,
36 | args: [
37 | messenger.address,
38 | Reliquary.address,
39 | legacyBlockHistory.address,
40 | l1NetworkConfig.capellaSlot,
41 | l1NetworkConfig.denebSlot,
42 | l1NetworkConfig.upgradeBlock
43 | ],
44 | log: true,
45 | });
46 | } else if (network.config.optimism === true) {
47 | const messenger = await getL1Contract(getMessengerName())
48 | const l1BlockHistory = await companionNetworks["l1"].deployments.get("LegacyBlockHistory")
49 | const merkleRootsSlot = getMerkleRootsSlot(l1BlockHistory)
50 | await deploy("BlockHistory", {
51 | contract: "OptimismProxyBlockHistory",
52 | from: deployer,
53 | args: [Reliquary.address, messenger.address, l1BlockHistory.address, merkleRootsSlot],
54 | log: true,
55 | skipIfAlreadyDeployed,
56 | });
57 |
58 | // save and/or rename the BlockHistory deployment as LegacyBlockHistory
59 | let legacyBlockHistory = await deployments.get("BlockHistory")
60 | if (legacyBlockHistory.devdoc.title.includes("Beacon")) {
61 | legacyBlockHistory = await deployments.get("LegacyBlockHistory")
62 | } else {
63 | deployments.save("LegacyBlockHistory", legacyBlockHistory);
64 | }
65 |
66 | let l1NetworkName = await companionNetworks["l1"].deployments.getNetworkName();
67 | let l1NetworkConfig = config.networks[l1NetworkName];
68 | await deploy("BlockHistory", {
69 | contract: "OptimismProxyBeaconBlockHistory",
70 | from: deployer,
71 | args: [
72 | messenger.address,
73 | Reliquary.address,
74 | legacyBlockHistory.address,
75 | l1NetworkConfig.capellaSlot,
76 | l1NetworkConfig.denebSlot,
77 | l1NetworkConfig.upgradeBlock
78 | ],
79 | log: true,
80 | });
81 | }
82 | } else if (network.config.l2Native === true) {
83 | // no verifiers on native L2 deployments, only use precomitted blocks
84 | const sizes = [];
85 | const verifiers = [];
86 |
87 | // point to the deployed MultiStorageSlotProver on this network for the L1 data
88 | const proxyProver = await companionNetworks['proxy'].deployments.get("MultiStorageSlotProver")
89 |
90 | // gather necessary information from the 'L2OutputOracle' contract on L1
91 | const l2OutputOracle = new ethers.Contract(
92 | network.config.l2OutputOracle,
93 | [
94 | 'function FINALIZATION_PERIOD_SECONDS() external view returns (uint256)',
95 | 'function getL2Output(uint256) external view returns (bytes32, uint128, uint128)'
96 | ],
97 | new ethers.providers.JsonRpcProvider(
98 | config.networks[companionNetworks['l1'].deployments.getNetworkName()].url
99 | )
100 | )
101 | const blockTag = await l2OutputOracle.provider.getBlock().then(b => b.hash)
102 | const finalizationPeriodSeconds = await l2OutputOracle.FINALIZATION_PERIOD_SECONDS({blockTag})
103 | const [output0] = await l2OutputOracle.getL2Output(0, {blockTag})
104 |
105 | // find outputRoots slot
106 | var outputRootsSlot;
107 | for (outputRootsSlot = 0; ; outputRootsSlot++) {
108 | const slotVal = await l2OutputOracle.provider.getStorageAt(
109 | l2OutputOracle.address,
110 | ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(['uint256'], [outputRootsSlot])),
111 | blockTag
112 | )
113 | if (slotVal == output0) break
114 | if (outputRootsSlot > 128) throw Error("couldn't find output slot")
115 | }
116 | outputRootsSlot = ethers.utils.defaultAbiCoder.encode(['uint256'], [outputRootsSlot])
117 |
118 | await deploy("BlockHistory", {
119 | contract: "OptimismNativeBlockHistory",
120 | from: deployer,
121 | args: [
122 | sizes,
123 | verifiers,
124 | Reliquary.address,
125 | proxyProver.address,
126 | l2OutputOracle.address,
127 | outputRootsSlot,
128 | finalizationPeriodSeconds
129 | ],
130 | log: true,
131 | skipIfAlreadyDeployed,
132 | });
133 | } else {
134 | const sizes = config.relic.vkSizes;
135 | const verifiers = await Promise.all(
136 | sizes.map((size) => deployments.get(`Verifier-${size}`).then(v => v.address))
137 | );
138 | await deploy("BlockHistory", {
139 | from: deployer,
140 | args: [sizes, verifiers, Reliquary.address],
141 | log: true,
142 | skipIfAlreadyDeployed,
143 | });
144 |
145 | let legacyBlockHistory = await deployments.get("BlockHistory")
146 | if (legacyBlockHistory.devdoc.title.includes("Beacon")) {
147 | blockHistory = await deployments.get("LegacyBlockHistory")
148 | } else {
149 | deployments.save("LegacyBlockHistory", legacyBlockHistory);
150 | }
151 |
152 | await deploy("BlockHistory", {
153 | contract: "BeaconBlockHistory",
154 | from: deployer,
155 | args: [
156 | Reliquary.address,
157 | legacyBlockHistory.address,
158 | network.config.beaconOracleContract,
159 | network.config.capellaSlot,
160 | network.config.denebSlot,
161 | network.config.upgradeBlock
162 | ],
163 | log: true,
164 | });
165 | }
166 | };
167 | module.exports.tags = ["BlockHistory"];
168 | let dependencies = ["Reliquary"];
169 | if (network.config.bridged !== true || network.config.l2Native !== true)
170 | dependencies = dependencies.concat(["Verifiers"]);
171 | module.exports.dependencies = dependencies;
172 |
--------------------------------------------------------------------------------
/contracts/lib/SSZ.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.0;
6 |
7 | import "./MerkleTree.sol";
8 |
9 | import {byteReverse} from "./Proofs.sol";
10 |
11 | /**
12 | * @title SSZ
13 | * @author Theori, Inc.
14 | * @notice Selected SSZ merkle verification code for Beacon chain data structures
15 | *
16 | * @dev The indices hardcoded in this contract are primarily for the Dencun hardfork.
17 | * One exception is verifying execution payload fields, where Capella is also
18 | * supported. Also, this contract uses raw merkle indices rather than the
19 | * "generalized indices" specified in SSZ.
20 | *
21 | */
22 | library SSZ {
23 | // the total proof length for a historical block summaries proof
24 | uint256 constant HISTORICAL_SUMMARIES_TREE_DEPTH = 24;
25 |
26 | // the total proof length for a historical block summaries proof
27 | uint256 constant HISTORICAL_BLOCK_SUMMARIES_PROOF_LENGTH = 32;
28 |
29 | // index of the block_roots merkle root relative to a block root
30 | uint256 constant STATE_ROOT_INDEX = 3;
31 |
32 | // index of the block_roots field relative to a state root
33 | uint256 constant BLOCK_ROOTS_INDEX = 5;
34 |
35 | // index of the historical_summaries field relative to a state root
36 | uint256 constant HISTORICAL_SUMMARIES_INDEX = 27;
37 |
38 | // index of the slot value relative to a block root
39 | uint256 constant SLOT_INDEX = 0;
40 |
41 | // index of the execution payload relative to a block root
42 | uint256 constant EXECUTION_PAYLOAD_INDEX = 73;
43 |
44 | // index of the block number in the left subtree of the execution payload
45 | uint256 constant BLOCK_NUMBER_LEFT_SUBTREE_INDEX = 6;
46 |
47 | // index of the block hash in the right subtree of the execution payload
48 | uint256 constant BLOCK_HASH_RIGHT_SUBTREE_INDEX = 4;
49 |
50 | /**
51 | * @notice verify an SSZ merkle proof for `BeaconBlock.body.execution_payload.block_{number,hash}`
52 | */
53 | function verifyExecutionPayloadFields(
54 | bytes32[] calldata proof,
55 | bytes32 blockRoot,
56 | bool isCapella
57 | ) internal view returns (bytes32 blockHash, uint256 blockNumber) {
58 | if (isCapella) {
59 | require(proof.length == 15, "invalid proof length");
60 | } else {
61 | require(proof.length == 16, "invalid proof length");
62 | }
63 | blockHash = proof[0];
64 | bytes32 blockNumberAsHash = proof[1];
65 | bytes32 rightSubtreeRoot = MerkleTree.proofRoot(
66 | BLOCK_HASH_RIGHT_SUBTREE_INDEX,
67 | blockHash,
68 | proof[2:5]
69 | );
70 | bytes32 leftSubtreeRoot = MerkleTree.proofRoot(
71 | BLOCK_NUMBER_LEFT_SUBTREE_INDEX,
72 | blockNumberAsHash,
73 | proof[5:8]
74 | );
75 | bytes32 executionPayloadSubtreeRoot = MerkleTree.combine(leftSubtreeRoot, rightSubtreeRoot);
76 | // if in capella, we're already at the execution payload root
77 | // otherwise, we need one extra proof node
78 | bytes32 computedRoot = MerkleTree.proofRoot(
79 | isCapella ? EXECUTION_PAYLOAD_INDEX : EXECUTION_PAYLOAD_INDEX << 1,
80 | executionPayloadSubtreeRoot,
81 | proof[8:]
82 | );
83 | require(computedRoot == blockRoot, "invalid execution proof");
84 |
85 | blockNumber = byteReverse(uint256(blockNumberAsHash));
86 | }
87 |
88 | /**
89 | * @notice verify an SSZ merkle proof for `BeaconBlock.state_root`
90 | */
91 | function verifyBlockStateRoot(
92 | bytes32[] calldata proof,
93 | bytes32 blockRoot
94 | ) internal view returns (bytes32 stateRoot) {
95 | require(proof.length == 4, "invalid proof length");
96 | stateRoot = proof[0];
97 | bytes32 computedRoot = MerkleTree.proofRoot(STATE_ROOT_INDEX, stateRoot, proof[1:]);
98 | require(computedRoot == blockRoot, "invalid stateRoot proof");
99 | }
100 |
101 | /**
102 | * @notice verify an SSZ merkle proof for `BeaconBlock.slot`
103 | */
104 | function verifyBlockSlot(
105 | bytes32[] calldata proof,
106 | bytes32 blockRoot
107 | ) internal view returns (uint256 slot) {
108 | require(proof.length == 4, "invalid proof length");
109 | bytes32 slotAsHash = proof[0];
110 | bytes32 computedRoot = MerkleTree.proofRoot(SLOT_INDEX, slotAsHash, proof[1:]);
111 | require(computedRoot == blockRoot, "invalid slot proof");
112 | slot = byteReverse(uint256(slotAsHash));
113 | require(slot <= type(uint64).max, "invalid slot value");
114 | }
115 |
116 | /**
117 | * @notice verifies `state.historical_summaries[index].block_summary_root`
118 | */
119 | function verifyHistoricalBlockSummary(
120 | bytes32[] calldata proof,
121 | uint256 index,
122 | bytes32 stateRoot
123 | ) internal view returns (bytes32 historicalBlockSummary) {
124 | // proof length is an upper bound in this case, see below
125 | require(proof.length <= HISTORICAL_BLOCK_SUMMARIES_PROOF_LENGTH, "proof too long");
126 |
127 | historicalBlockSummary = proof[0];
128 | bytes32 historicalSummary = MerkleTree.combine(historicalBlockSummary, proof[1]);
129 | bytes32[] calldata topProof = proof[2:8];
130 |
131 | bytes32 intermediate = MerkleTree.proofRoot(
132 | index,
133 | historicalSummary,
134 | proof[8:]
135 | );
136 |
137 | // any missing proof nodes are implicit "default" values on the right side of the tree
138 | uint256 numImplicitNodes = HISTORICAL_BLOCK_SUMMARIES_PROOF_LENGTH - proof.length;
139 |
140 | // compute the defaultValue for our current depth
141 | bytes32 defaultValue = bytes32(0);
142 | for (uint256 i = 0; i < HISTORICAL_SUMMARIES_TREE_DEPTH - numImplicitNodes; i++) {
143 | defaultValue = MerkleTree.combine(defaultValue, defaultValue);
144 | }
145 |
146 | // compute the historical_summaries data root assuming default value
147 | bytes32 listDataRoot = MerkleTree.rootWithDefault(
148 | numImplicitNodes,
149 | intermediate,
150 | defaultValue
151 | );
152 |
153 | // finally, compute the overall state root
154 | bytes32 computedRoot = MerkleTree.proofRoot(
155 | HISTORICAL_SUMMARIES_INDEX << 1, // one extra proof node on the right for the list length
156 | listDataRoot,
157 | topProof
158 | );
159 | require(computedRoot == stateRoot, "invalid summary proof");
160 | }
161 |
162 | /**
163 | * @notice verifies `state.block_roots[index]`
164 | */
165 | function verifyRelativeBlockRoot(
166 | bytes32[] calldata proof,
167 | uint256 index,
168 | bytes32 stateRoot
169 | ) internal view returns (bytes32 blockRoot) {
170 | require(proof.length == 19, "invalid proof length");
171 | blockRoot = proof[0];
172 | bytes32 vectorRoot = MerkleTree.proofRoot(
173 | index,
174 | blockRoot,
175 | proof[1:14]
176 | );
177 | bytes32 computedRoot = MerkleTree.proofRoot(
178 | BLOCK_ROOTS_INDEX,
179 | vectorRoot,
180 | proof[14:]
181 | );
182 | require(computedRoot == stateRoot, "invalid relative proof");
183 | }
184 |
185 | /**
186 | * @notice verify an SSZ merkle proof for a Vector[Root, SLOTS_PER_HISTORICAL_ROOT]
187 | * @dev intended to be used with block summaries, i.e. `BeaconState.block_roots`
188 | */
189 | function verifySummaryIndex(
190 | bytes32[] calldata proof,
191 | uint256 index,
192 | bytes32 summaryRoot
193 | ) internal view returns (bytes32 blockRoot) {
194 | require(proof.length == 14, "invalid proof length");
195 | blockRoot = proof[0];
196 | bytes32 computedRoot = MerkleTree.proofRoot(index, blockRoot, proof[1:]);
197 | require(computedRoot == summaryRoot, "invalid summary proof");
198 | }
199 | }
200 |
--------------------------------------------------------------------------------
/utils/importL2.js:
--------------------------------------------------------------------------------
1 | const { ethers, companionNetworks, network } = require("hardhat")
2 | const { Provider: ZkProvider, Wallet: ZkWallet, utils: zksyncUtils } = require("zksync-web3")
3 | const { RelicClient, UnknownError, utils: relicUtils } = require("@relicprotocol/client")
4 |
5 | const { getMerkleRootsSlot } = require("./slots")
6 | const { getGasOptions } = require("./gas")
7 | const {
8 | getL1Provider,
9 | getLogs,
10 | getL1Contract,
11 | getMessengerName,
12 | patchProviderPolling
13 | } = require("./network")
14 |
15 | const {
16 | waitForTrustedImport
17 | } = require("./blockhistory")
18 |
19 |
20 | async function getMessengerParams(blockNum, blockHash) {
21 | // compute gas estimate
22 | const l2BlockHistory = await ethers.getContract("BlockHistory")
23 | const messenger = await getL1Contract(getMessengerName())
24 | const call = await l2BlockHistory.populateTransaction.importTrustedHash(blockNum, blockHash)
25 | let params, l2Fee;
26 | if (network.config.zksync) {
27 | const request = {
28 | contractAddress: call.to,
29 | calldata: call.data,
30 | caller: zksyncUtils.applyL1ToL2Alias(messenger.address)
31 | }
32 | const zkProvider = new ZkProvider(network.config.url)
33 | const zkWallet = new ZkWallet(network.config.accounts[0], zkProvider, getL1Provider())
34 | const l2GasLimit = await zkWallet.provider.estimateL1ToL2Execute(request)
35 | const l2Tx = await zkWallet.getRequestExecuteTx(request)
36 | l2Fee = l2Tx.value
37 |
38 | const l2GasPerPubdataByteLimit = zksyncUtils.REQUIRED_L1_TO_L2_GAS_PER_PUBDATA_LIMIT
39 | params = ethers.utils.defaultAbiCoder.encode(["uint256", "uint256"], [l2GasLimit, l2GasPerPubdataByteLimit])
40 | } else if (network.config.optimism) {
41 | const l2GasLimit = await l2BlockHistory.provider.estimateGas({
42 | ...call,
43 | from: zksyncUtils.applyL1ToL2Alias(messenger.address) // same alias rules as zkSync
44 | })
45 | params = ethers.utils.defaultAbiCoder.encode(["uint64"], [l2GasLimit])
46 | l2Fee = 0
47 | } else {
48 | throw new Error("unsupported network")
49 | }
50 | return { params, l2Fee }
51 | }
52 |
53 | async function commitCurrentL1BlockHash(minBlockNumber) {
54 | if (network.config.optimism !== true) throw Error("incompatible network")
55 | // TODO: parameterize which BlockHistory to commit to
56 | const l2BlockHistory = await ethers.getContract("BlockHistory")
57 | patchProviderPolling(l2BlockHistory.provider)
58 |
59 | if (minBlockNumber && minBlockNumber > 0) {
60 | const blockContract = new ethers.Contract(
61 | "0x4200000000000000000000000000000000000015",
62 | ['function number() view returns (uint256)'],
63 | l2BlockHistory.provider
64 | )
65 | console.log(`Waiting for L1 block provider to reach ${minBlockNumber}...`)
66 | while ((await blockContract.number()).lt(minBlockNumber)) {
67 | await new Promise(res => setTimeout(res, 6000))
68 | }
69 | }
70 |
71 | const tx = await l2BlockHistory.commitCurrentL1BlockHash(getGasOptions())
72 | console.log(`committing current L1 block in tx ${tx.hash}...`)
73 | const receipt = await tx.wait()
74 | const parsed = l2BlockHistory.interface.parseLog(receipt.logs[0])
75 | // argument name depends on version
76 | const result = parsed.args.number || parsed.args.blockNum
77 | return result.toNumber()
78 | }
79 |
80 | async function sendBlockHash(blockNum) {
81 | if (network.config.bridged !== true) throw Error("not on bridged network")
82 |
83 | const l1Provider = getL1Provider()
84 | const messenger = await getL1Contract(getMessengerName())
85 |
86 | const proxyBlockHistory = await ethers.getContract("BlockHistory")
87 |
88 | const blockHash = await l1Provider.getBlock(blockNum).then(b => b.hash)
89 |
90 | const relic = await RelicClient.fromProvider(l1Provider)
91 | const blockProof = blockNum > await l1Provider.getBlockNumber() - 200 ? "0x" : await relic.api.blockProof(blockHash).then(p => p.blockProof)
92 |
93 | const { params, l2Fee } = await getMessengerParams(blockNum, blockHash)
94 | const tx = await messenger.sendBlockHash(
95 | proxyBlockHistory.address,
96 | params,
97 | blockNum,
98 | blockHash,
99 | blockProof,
100 | { ...getGasOptions(), value: l2Fee }
101 | )
102 |
103 | console.log(`waiting for L1 tx: ${tx.hash}`)
104 | await tx.wait()
105 | console.log(`waiting for L2 tx...`)
106 | await waitForTrustedImport(blockHash)
107 | console.log("l2 tx completed")
108 | }
109 |
110 | const MAX_IMPORT_SIZE = 240
111 |
112 | async function proxyImport(blockHistory, relic, block, index, numRoots) {
113 | const l1BlockHistory = await companionNetworks['l1'].deployments.get("LegacyBlockHistory")
114 | const merkleRootsSlot = getMerkleRootsSlot(l1BlockHistory)
115 | const account = l1BlockHistory.address
116 | const slots = [...new Array(numRoots).keys()].map(i => relicUtils.mapElemSlot(merkleRootsSlot, index + i))
117 |
118 | console.log(`Fetching slot proofs for [${index}, ..., ${index + numRoots})`)
119 |
120 | let proof = ""
121 | do {
122 | try {
123 | let {} = { proof } = await relic.multiStorageSlotProver.getProofData({block, account, slots})
124 | } catch(e) {
125 | if (! (e instanceof UnknownError) || e.message != "No response data from API") {
126 | throw e
127 | }
128 | console.log("Error fetching proof... retrying")
129 | }
130 | } while( !proof );
131 |
132 | const [ _acc, accountProof, header, _bp, proofNodes, _s, slotProofs, _ih] = ethers.utils.defaultAbiCoder.decode(
133 | [
134 | 'address',
135 | 'bytes',
136 | 'bytes',
137 | 'bytes',
138 | 'bytes',
139 | 'uint256[]',
140 | 'bytes',
141 | 'bool',
142 | ],
143 | proof
144 | )
145 | // re-encode for the blockhistory format
146 | proof = ethers.utils.defaultAbiCoder.encode(
147 | [
148 | 'uint256',
149 | 'uint256',
150 | 'bytes',
151 | 'bytes',
152 | 'uint256[]',
153 | 'bytes',
154 | 'bytes'
155 | ],
156 | [ index, numRoots, header, accountProof, slots, proofNodes, slotProofs ]
157 | )
158 |
159 | console.log(`Importing [${index}, ..., ${index + numRoots})`)
160 | let tx = await blockHistory.importRoots(proof, getGasOptions())
161 | console.log(`Tx hash: ${tx.hash}`)
162 | await tx.wait()
163 | }
164 |
165 | async function importMerkleRoots(blockNum, index, numRoots) {
166 | const blockHistory = await ethers.getContract("LegacyBlockHistory")
167 |
168 | const l1Provider = getL1Provider()
169 | const relic = await RelicClient.fromProvider(l1Provider)
170 |
171 | for (let i = 0; i < numRoots; i += MAX_IMPORT_SIZE) {
172 | const start = index + i
173 | const size = Math.min(MAX_IMPORT_SIZE, numRoots - i)
174 | await proxyImport(blockHistory, relic, blockNum, start, size)
175 | }
176 | }
177 |
178 | async function lastImportedRoot(blockHistory) {
179 | const filter = blockHistory.filters.ImportMerkleRoot()
180 | let logs = await getLogs(blockHistory.provider, filter)
181 | const index = logs.length > 0 ? ethers.BigNumber.from(logs[logs.length - 1].topics[1]).toNumber() : -1
182 | const blockNum = logs.length > 0 ? logs[logs.length - 1].blockNumber : -1
183 | return { index, blockNum }
184 | }
185 |
186 | async function waitForL1Update() {
187 | if (network.config.bridged !== true) throw Error("not on bridged network")
188 | const l2BlockHistory = await ethers.getContract("LegacyBlockHistory")
189 | const l1BlockHistory = await getL1Contract("LegacyBlockHistory")
190 | patchProviderPolling(l1BlockHistory.provider);
191 | patchProviderPolling(l2BlockHistory.provider);
192 |
193 | while (true) {
194 | const lastL1 = await lastImportedRoot(l1BlockHistory)
195 | const lastL2 = await lastImportedRoot(l2BlockHistory)
196 |
197 | if (lastL2.index < lastL1.index) {
198 | return {
199 | blockNum: lastL1.blockNum,
200 | index: lastL2.index + 1,
201 | numRoots: lastL1.index - lastL2.index
202 | }
203 | }
204 |
205 | console.log("L2 is caught up, waiting for new import on L1...")
206 | await new Promise(res => l1BlockHistory.once("ImportMerkleRoot", res))
207 | }
208 | }
209 |
210 | module.exports = {
211 | commitCurrentL1BlockHash,
212 | getMessengerName,
213 | sendBlockHash,
214 | importMerkleRoots,
215 | waitForL1Update,
216 | }
217 |
--------------------------------------------------------------------------------
/contracts/RelicToken.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2022
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.12;
6 |
7 | import "@openzeppelin/contracts/access/Ownable.sol";
8 | import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
9 | import "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol";
10 | import "@openzeppelin/contracts/utils/introspection/ERC165.sol";
11 |
12 | import "./interfaces/IContractURI.sol";
13 | import "./interfaces/IERC5192.sol";
14 | import "./interfaces/ITokenURI.sol";
15 |
16 | /**
17 | * @title RelicToken
18 | * @author Theori, Inc.
19 | * @notice RelicToken is the base contract for all Relic SBTs. It implements
20 | * ERC721 (with transfers disables) and ERC5192.
21 | */
22 | abstract contract RelicToken is Ownable, ERC165, IERC721, IERC721Metadata, IERC5192 {
23 | mapping(address => bool) public provers;
24 |
25 | /// @notice contract metadata URI provider
26 | IContractURI contractURIProvider;
27 |
28 | /**
29 | * @notice determind if the given owner is entitiled to a token with the specific data
30 | * @param owner the address in question
31 | * @param data the opaque data in question
32 | * @return the existence of the given data
33 | */
34 | function hasToken(address owner, uint96 data) internal view virtual returns (bool);
35 |
36 | /**
37 | * @notice updates the set of contracts trusted to create new tokens and
38 | * possibly resolve entitlement questions
39 | * @param prover the address of the prover
40 | * @param valid whether the prover is trusted
41 | */
42 | function setProver(address prover, bool valid) external onlyOwner {
43 | provers[prover] = valid;
44 | }
45 |
46 | /**
47 | * @notice helper function to break a tokenId into its constituent data
48 | * @param tokenId the tokenId in question
49 | * @return who the address bound to this token
50 | * @return data any additional data bound to this token
51 | */
52 | function parseTokenId(uint256 tokenId) internal pure returns (address who, uint96 data) {
53 | who = address(bytes20(bytes32(tokenId << 96)));
54 | data = uint96(tokenId >> 160);
55 | }
56 |
57 | /**
58 | * @notice issue a new Relic
59 | * @param who the address to which this token should be bound
60 | * @param data any data to be associated with this token
61 | * @dev emits ERC-721 Transfer event and ERC-5192 Locked event. Note
62 | * that storage is not generally updated by this function.
63 | */
64 | function mint(address who, uint96 data) public virtual {
65 | require(provers[msg.sender], "only a prover can mint");
66 | require(hasToken(who, data), "cannot mint for invalid token");
67 |
68 | uint256 id = uint256(uint160(who)) | (uint256(data) << 160);
69 | emit Transfer(address(0), who, id);
70 | emit Locked(id);
71 | }
72 |
73 | /* begin ERC-721 spec functions */
74 | /**
75 | * @inheritdoc IERC721
76 | * @dev If the token has not been issued (no transfer event) this function
77 | * may still return an owner if there is an account entitled to this
78 | * token.
79 | */
80 | function ownerOf(uint256 id) public view virtual returns (address who) {
81 | uint96 data;
82 | (who, data) = parseTokenId(id);
83 | if (!hasToken(who, data)) {
84 | who = address(0);
85 | }
86 | }
87 |
88 | /**
89 | * @inheritdoc IERC721
90 | * @dev Balance will always be 0 if the address is not entitled to any
91 | * tokens, and 1 if they are entitled to a token. If multiple tokens
92 | * are minted, this will still return 1.
93 | */
94 | function balanceOf(address who) external view override returns (uint256 balance) {
95 | require(who != address(0), "ERC721: address zero is not a valid owner");
96 | if (hasToken(who, 0)) {
97 | balance = 1;
98 | }
99 | }
100 |
101 | /**
102 | * @inheritdoc IERC721
103 | * @dev Immediately reverts: Relics are soul-bound/non-transferrable
104 | */
105 | function safeTransferFrom(
106 | address, /* from */
107 | address, /* _to */
108 | uint256, /* _tokenId */
109 | bytes calldata /* data */
110 | ) external pure {
111 | revert("RelicToken is soulbound");
112 | }
113 |
114 | /**
115 | * @inheritdoc IERC721
116 | * @dev Immediately reverts: Relics are soul-bound/non-transferrable
117 | */
118 | function safeTransferFrom(
119 | address, /* from */
120 | address, /* to */
121 | uint256 /* tokenId */
122 | ) external pure {
123 | revert("RelicToken is soulbound");
124 | }
125 |
126 | /**
127 | * @inheritdoc IERC721
128 | * @dev Immediately reverts: Relics are soul-bound/non-transferrable
129 | */
130 | function transferFrom(
131 | address, /* from */
132 | address, /* to */
133 | uint256 /* id */
134 | ) external pure {
135 | revert("RelicToken is soulbound");
136 | }
137 |
138 | /**
139 | * @inheritdoc IERC721
140 | * @dev Immediately reverts: Relics are soul-bound/non-transferrable
141 | */
142 | function approve(
143 | address, /* to */
144 | uint256 /* tokenId */
145 | ) external pure {
146 | revert("RelicToken is soulbound");
147 | }
148 |
149 | /**
150 | * @inheritdoc IERC721
151 | * @dev Immediately reverts: Relics are soul-bound/non-transferrable
152 | */
153 | function setApprovalForAll(
154 | address, /* operator */
155 | bool /* _approved */
156 | ) external pure {
157 | revert("RelicToken is soulbound");
158 | }
159 |
160 | /**
161 | * @inheritdoc IERC721
162 | * @dev Always returns the null address: Relics are soul-bound/non-transferrable
163 | */
164 | function getApproved(
165 | uint256 /* tokenId */
166 | ) external pure returns (address operator) {
167 | operator = address(0);
168 | }
169 |
170 | /**
171 | * @inheritdoc IERC721
172 | * @dev Always returns false: Relics are soul-bound/non-transferrable
173 | */
174 | function isApprovedForAll(
175 | address, /* owner */
176 | address /* operator */
177 | ) external pure returns (bool) {
178 | return false;
179 | }
180 |
181 | /**
182 | * @inheritdoc IERC165
183 | * @dev Supported interfaces: IERC721, IERC721Metadata, IERC5192
184 | */
185 | function supportsInterface(bytes4 interfaceId)
186 | public
187 | view
188 | virtual
189 | override(ERC165, IERC165)
190 | returns (bool)
191 | {
192 | return (interfaceId == type(IERC721).interfaceId ||
193 | interfaceId == type(IERC721Metadata).interfaceId ||
194 | interfaceId == type(IERC5192).interfaceId ||
195 | super.supportsInterface(interfaceId));
196 | }
197 |
198 | /// @inheritdoc IERC721Metadata
199 | function name() external pure virtual returns (string memory);
200 |
201 | /// @inheritdoc IERC721Metadata
202 | function symbol() external pure virtual returns (string memory);
203 |
204 | /// @inheritdoc IERC721Metadata
205 | function tokenURI(uint256 tokenID) external view virtual returns (string memory);
206 |
207 | /* end ERC-721 spec functions */
208 |
209 | /* begin ERC-5192 spec functions */
210 | /**
211 | * @inheritdoc IERC5192
212 | * @dev All valid tokens are locked: Relics are soul-bound/non-transferrable
213 | */
214 | function locked(uint256 id) external view returns (bool) {
215 | return ownerOf(id) != address(0);
216 | }
217 |
218 | /* end ERC-5192 spec functions */
219 |
220 | /* begin OpenSea metadata functions */
221 | /**
222 | * @notice contract metadata URI as defined by OpenSea
223 | */
224 | function contractURI() external view returns (string memory) {
225 | return contractURIProvider.contractURI();
226 | }
227 |
228 | /**
229 | * @notice set contract-level metadata URI provider
230 | * @param provider new metadata URI provider
231 | */
232 | function setContractURIProvider(IContractURI provider) external onlyOwner {
233 | contractURIProvider = provider;
234 | }
235 | /* end OpenSea metadata functions */
236 | }
237 |
--------------------------------------------------------------------------------
/contracts/ProxyBeaconBlockHistory.sol:
--------------------------------------------------------------------------------
1 | /// SPDX-License-Identifier: UNLICENSED
2 | /// (c) Theori, Inc. 2024
3 | /// All rights reserved
4 |
5 | pragma solidity >=0.8.0;
6 |
7 | import "@openzeppelin/contracts/access/AccessControl.sol";
8 |
9 | import "./BeaconBlockHistoryBase.sol";
10 | import "./lib/Propogate.sol";
11 | import "./interfaces/IBlockHistory.sol";
12 | import "./interfaces/IBeaconBlockHistory.sol";
13 |
14 | /**
15 | * @title ProxyBeaconBlockHistory
16 | * @author Theori, Inc.
17 | * @notice ProxyBeaconBlockHistory allows trustless and cheap verification of any post-Dencun
18 | * L1 beacon block root from some L2 chain. Since the beacon blocks contain the
19 | * execution block headers, this also enables verifying those execution block hashes.
20 | *
21 | * @notice By propogating some queries to Relic's original ProxyBlockHistory contract,
22 | * this contract enables cheap verification of *all* L1 execution block hashes
23 | * back to genesis.
24 | *
25 | * @dev This works by leveraging the `parent_beacon_block_root` header field introduced
26 | * in EIP-4788: https://eips.ethereum.org/EIPS/eip-4788#block-structure-and-validity.
27 | *
28 | * Given access to a recent execution block header, we parse the `parent_beacon_block_root`.
29 | * Then, using an SSZ merkle proof of the `BeaconState.block_roots` and/or the
30 | * `BeaconState.historical_summaries` elements, we can verifiably access beacon block root
31 | * since the Capella hardfork.
32 | *
33 | * To reduce redundancy, this contract supports caching each value of the
34 | * `historical_sumaries` list. Given this cached root, block proofs can be generated
35 | * using only `BeaconBlock` roots (and data), which are easily accessible.
36 | *
37 | * Execution block information can then be verified with merkle proofs of
38 | * the `BeaconBlock.body.execution_payload.block_{number,hash}` fields.
39 | *
40 | * @dev Note: this contract should be extended to provide a mechanism to import an L1 blockhash
41 | * in a trustless way. This could be via a trustless L1 -> L2 message, or by a built-in
42 | * L1 Block oracle query, depending on the L2 network.
43 | */
44 | contract ProxyBeaconBlockHistory is AccessControl, BeaconBlockHistoryBase, IBlockHistory, IBeaconBlockHistory {
45 | bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
46 | bytes32 public constant QUERY_ROLE = keccak256("QUERY_ROLE");
47 |
48 | /// @dev address of the reliquary, immutable
49 | address public immutable reliquary;
50 |
51 | /// @dev mapping of precomitted execution block hashes
52 | mapping(uint256 => bytes32) private precomittedBlockHashes;
53 |
54 | /// @dev the blockHistory which stores the data before the Dencnun fork
55 | address public immutable preDencunBlockHistory;
56 |
57 | /// @dev the first block number handled by this contract
58 | uint256 public immutable UPGRADE_BLOCK;
59 |
60 | event PrecomittedBlock(uint256 indexed blockNum, bytes32 blockHash);
61 |
62 | /// @dev types of block proofs supported by this and prior contracts
63 | enum ProofType {
64 | Merkle, // legacy, not supported in this contract
65 | SNARK, // legacy, not supported in this contract
66 | Precomitted,
67 | Beacon
68 | }
69 |
70 | constructor(
71 | address _reliquary,
72 | address _preDencunBlockHistory,
73 | uint256 _CAPELLA_SLOT,
74 | uint256 _DENEB_SLOT,
75 | uint256 _UPGRADE_BLOCK
76 | ) BeaconBlockHistoryBase(_CAPELLA_SLOT, _DENEB_SLOT) {
77 | _setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
78 | _setupRole(ADMIN_ROLE, msg.sender);
79 | _setupRole(QUERY_ROLE, msg.sender);
80 |
81 | reliquary = _reliquary;
82 | preDencunBlockHistory = _preDencunBlockHistory;
83 | UPGRADE_BLOCK = _UPGRADE_BLOCK;
84 | }
85 |
86 | /**
87 | * @notice Checks if the block is a valid precomitted block.
88 | *
89 | * @param hash the alleged block hash
90 | * @param num the block number
91 | */
92 | function _validPrecomittedBlock(bytes32 hash, uint256 num) internal view returns (bool) {
93 | bytes32 stored = precomittedBlockHashes[num];
94 | return stored != bytes32(0) && stored == hash;
95 | }
96 |
97 | function _storeCommittedBlock(uint256 blockNum, bytes32 blockHash) internal {
98 | require(blockHash != bytes32(0), "invalid blockhash");
99 | precomittedBlockHashes[blockNum] = blockHash;
100 | emit PrecomittedBlock(blockNum, blockHash);
101 | }
102 |
103 | /**
104 | * @dev implements beacon block root verification for L2
105 | * @dev supports all proof types except oracle queries
106 | */
107 | function _verifyBeaconBlockRoot(
108 | bytes calldata proof
109 | ) internal override view returns (bytes32 blockRoot) {
110 | BeaconProofType typ;
111 | (typ, proof) = parseBeaconProofType(proof);
112 |
113 | if (typ == BeaconProofType.Summary) {
114 | return _verifySummaryBlockRoot(proof);
115 | } else if (typ == BeaconProofType.Relative) {
116 | return _verifyRelativeBlockRoot(proof);
117 | } else if (typ == BeaconProofType.Header) {
118 | return _verifyHeaderBlockRoot(proof);
119 | } else {
120 | revert("unsupported proof type");
121 | }
122 | }
123 |
124 | /**
125 | * @notice verifies a beacon block root
126 | * @param proof the proof of the beacon blcok
127 | * @return blockRoot the `BeaconBlock` root
128 | */
129 | function verifyBeaconBlockRoot(
130 | bytes calldata proof
131 | ) external view onlyRole(QUERY_ROLE) returns (bytes32 blockRoot) {
132 | blockRoot = _verifyBeaconBlockRoot(proof);
133 | }
134 |
135 | /**
136 | * @notice Parses a proof type and proof from the encoded proof
137 | *
138 | * @param encodedProof the encoded proof
139 | * @return typ the proof type
140 | * @return proof the remaining encoded proof
141 | */
142 | function parseProofType(bytes calldata encodedProof)
143 | internal
144 | pure
145 | returns (ProofType typ, bytes calldata proof)
146 | {
147 | require(encodedProof.length > 0, "cannot parse proof type");
148 | typ = ProofType(uint8(encodedProof[0]));
149 | proof = encodedProof[1:];
150 | }
151 |
152 | /**
153 | * @notice Checks if an execution block hash is valid. A proof is required.
154 | * @notice if the target block is before the Dencun fork, the query will be propogated
155 | * to the pre-Dencun BlockHistory contract.
156 | *
157 | * @param hash the hash to check
158 | * @param num the block number for the alleged hash
159 | * @param proof the proof (if needed)
160 | * @return the validity
161 | */
162 | function _validBlockHash(
163 | bytes32 hash,
164 | uint256 num,
165 | bytes calldata proof
166 | ) internal override view returns (bool) {
167 | // if attempting to verify an unhandled block,
168 | // propogate the call to the legacy BlockHistory
169 | if (num < UPGRADE_BLOCK) {
170 | Propogate.staticcall(preDencunBlockHistory); // does not return
171 | }
172 |
173 | ProofType typ;
174 | (typ, proof) = parseProofType(proof);
175 | if (typ == ProofType.Precomitted) {
176 | return _validPrecomittedBlock(hash, num);
177 | } else if (typ == ProofType.Beacon) {
178 | return _validBlockHashWithBeacon(hash, num, proof);
179 | } else {
180 | revert("unsupported proof type");
181 | }
182 | }
183 |
184 | /**
185 | * @notice Checks if a block hash is correct. A proof is required unless the
186 | * block is current (accesible in the EVM) or precomitted.
187 | * Reverts if proof is invalid.
188 | *
189 | * @param hash the hash to check
190 | * @param num the block number for the alleged hash
191 | * @param proof the merkle witness or SNARK proof (if needed)
192 | */
193 | function validBlockHash(
194 | bytes32 hash,
195 | uint256 num,
196 | bytes calldata proof
197 | ) external view returns (bool) {
198 | // optimization: check if sender is reliquary first,
199 | // so we don't touch storage in the typical path
200 | require(msg.sender == reliquary || hasRole(QUERY_ROLE, msg.sender));
201 | return _validBlockHash(hash, num, proof);
202 | }
203 |
204 | function getBlockSummary(uint256 slot) external view returns (bytes32 result) {
205 | require(hasRole(QUERY_ROLE, msg.sender) || msg.sender == address(0));
206 | result = _getBlockSummary(slot);
207 | }
208 | }
209 |
--------------------------------------------------------------------------------