├── .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 | --------------------------------------------------------------------------------