diff --git a/l1-contracts/.env b/l1-contracts/.env index d02962c279..948937a082 100644 --- a/l1-contracts/.env +++ b/l1-contracts/.env @@ -11,6 +11,7 @@ CONTRACTS_DIAMOND_INIT_ADDR=0x0000000000000000000000000000000000000000 CONTRACTS_DIAMOND_UPGRADE_INIT_ADDR=0x0000000000000000000000000000000000000000 CONTRACTS_DIAMOND_PROXY_ADDR=0x0000000000000000000000000000000000000000 CONTRACTS_VERIFIER_ADDR=0x0000000000000000000000000000000000000000 +CONTRACTS_DA_VERIFIER_ADDR=0x0000000000000000000000000000000000000000 CONTRACTS_L1_ERC20_BRIDGE_IMPL_ADDR=0x0000000000000000000000000000000000000000 CONTRACTS_L1_ERC20_BRIDGE_PROXY_ADDR=0x0000000000000000000000000000000000000000 CONTRACTS_L1_ALLOW_LIST_ADDR=0x0000000000000000000000000000000000000000 diff --git a/l1-contracts/contracts/dev-contracts/test/DummyExecutor.sol b/l1-contracts/contracts/dev-contracts/test/DummyExecutor.sol index e3ef2f3331..76da5f459a 100644 --- a/l1-contracts/contracts/dev-contracts/test/DummyExecutor.sol +++ b/l1-contracts/contracts/dev-contracts/test/DummyExecutor.sol @@ -90,6 +90,10 @@ contract DummyExecutor is IExecutor { return true; } + function isBatchDataAvailable(bytes32) external pure returns (bool) { + return true; + } + function executeBatches(StoredBatchInfo[] calldata _batchesData) external { require(!shouldRevertOnExecuteBatches, "DummyExecutor: shouldRevertOnExecuteBatches"); uint256 nBatches = _batchesData.length; diff --git a/l1-contracts/contracts/zksync/Storage.sol b/l1-contracts/contracts/zksync/Storage.sol index 15691d5d77..b2b1f17e39 100644 --- a/l1-contracts/contracts/zksync/Storage.sol +++ b/l1-contracts/contracts/zksync/Storage.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.19; import {IVerifier} from "./../zksync/interfaces/IVerifier.sol"; +import {IDAVerifier} from "./../zksync/interfaces/IDAVerifier.sol"; import {PriorityQueue} from "./libraries/PriorityQueue.sol"; import {IL2Gateway} from "./interfaces/IL2Gateway.sol"; @@ -138,12 +139,14 @@ struct AppStorage { mapping(address secondaryChainGateway => mapping(uint256 secondaryChainPriorityOpId => SecondaryChainSyncStatus syncStatus)) secondaryChainSyncStatus; mapping(bytes32 canonicalTxHash => SecondaryChainOp secondaryChainOp) canonicalTxToSecondaryChainOp; mapping(bytes32 secondaryChainCanonicalTxHash => bytes32 canonicalTxHash) secondaryToCanonicalTxHash; + /// @dev DA verifier contract. Used to verify commitment for batches + IDAVerifier daVerifier; /** * @dev This empty reserved space is put in place to allow future versions to add new * variables without shifting down storage in the inheritance chain. * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps */ - uint256[45] __gap; + uint256[44] __gap; /// @dev Storage of variables needed for deprecated diamond cut facet uint256[7] __DEPRECATED_diamondCutStorage; /// @notice Address which will exercise critical changes to the Diamond Proxy (upgrades, freezing & unfreezing) diff --git a/l1-contracts/contracts/zksync/ValidatorTimelock.sol b/l1-contracts/contracts/zksync/ValidatorTimelock.sol index 1d2b5444ee..d802c52dd2 100644 --- a/l1-contracts/contracts/zksync/ValidatorTimelock.sol +++ b/l1-contracts/contracts/zksync/ValidatorTimelock.sol @@ -113,6 +113,11 @@ contract ValidatorTimelock is IExecutor, Ownable2Step { return IExecutor(zkSyncContract).isBatchesSynced(_batchesData); } + /// @dev Make a call to the zkSync contract with the same calldata. + function isBatchDataAvailable(bytes32 _commitment) external view returns (bool) { + return IExecutor(zkSyncContract).isBatchDataAvailable(_commitment); + } + /// @dev Check that batches were committed at least X time ago and /// make a call to the zkSync contract with the same calldata. function executeBatches(StoredBatchInfo[] calldata _newBatchesData) external onlyValidator { diff --git a/l1-contracts/contracts/zksync/da/CelestiaDAVerifier.sol b/l1-contracts/contracts/zksync/da/CelestiaDAVerifier.sol new file mode 100644 index 0000000000..5827f64acc --- /dev/null +++ b/l1-contracts/contracts/zksync/da/CelestiaDAVerifier.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import "../interfaces/IDAVerifier.sol"; + +/// @notice A tuple of data root with metadata. Each data root is associated +/// with a Celestia block height. +/// @dev `availableDataRoot` in +/// https://github.com/celestiaorg/celestia-specs/blob/master/src/specs/data_structures.md#header +struct DataRootTuple { + // Celestia block height the data root was included in. + // Genesis block is height = 0. + // First queryable block is height = 1. + uint256 height; + // Data root. + bytes32 dataRoot; +} + +/// @notice Merkle Tree Proof structure. +struct BinaryMerkleProof { + // List of side nodes to verify and calculate tree. + bytes32[] sideNodes; + // The key of the leaf to verify. + uint256 key; + // The number of leaves in the tree + uint256 numLeaves; +} + +/// @notice Data Availability Oracle interface. +interface IDAOracle { + /// @notice Verify a Data Availability attestation. + /// @param _tupleRootNonce Nonce of the tuple root to prove against. + /// @param _tuple Data root tuple to prove inclusion of. + /// @param _proof Binary Merkle tree proof that `tuple` is in the root at `_tupleRootNonce`. + /// @return `true` is proof is valid, `false` otherwise. + function verifyAttestation( + uint256 _tupleRootNonce, + DataRootTuple memory _tuple, + BinaryMerkleProof memory _proof + ) external view returns (bool); +} + +/// @author zk.link +/// @notice The celestia verifier that integrate with BlockStream. +/// @dev https://docs.celestia.org/developers/blobstream-contracts +contract CelestiaDAVerifier is IDAVerifier { + IDAOracle public immutable DA_ORACLE; + + mapping(bytes32 commitment => bool) public validCommitment; + + constructor(IDAOracle _daOracle) { + DA_ORACLE = _daOracle; + } + + function isValidCommitment(bytes32 _commitment) external view returns (bool) { + return validCommitment[_commitment]; + } + + function verifyCommitment( + uint256 _tupleRootNonce, + DataRootTuple calldata _tuple, + BinaryMerkleProof calldata _proof + ) external { + require(DA_ORACLE.verifyAttestation(_tupleRootNonce, _tuple, _proof), "Invalid attestation"); + // The `dataRoot` of tuple is `commitment` of batch + validCommitment[_tuple.dataRoot] = true; + } +} diff --git a/l1-contracts/contracts/zksync/facets/Admin.sol b/l1-contracts/contracts/zksync/facets/Admin.sol index 0fbd9bc492..17f10e58ec 100644 --- a/l1-contracts/contracts/zksync/facets/Admin.sol +++ b/l1-contracts/contracts/zksync/facets/Admin.sol @@ -11,6 +11,7 @@ import {IL2Gateway} from "../interfaces/IL2Gateway.sol"; // While formally the following import is not used, it is needed to inherit documentation from it import {IBase} from "../interfaces/IBase.sol"; +import {IDAVerifier} from "../interfaces/IDAVerifier.sol"; /// @title Admin Contract controls access rights for contract management. /// @author Matter Labs @@ -35,6 +36,15 @@ contract AdminFacet is Base, IAdmin { } } + /// @inheritdoc IAdmin + function setDAVerifier(IDAVerifier _daVerifier) external onlyGovernor { + require(address(_daVerifier) != address(0), "gb"); + if (s.daVerifier != _daVerifier) { + s.daVerifier = _daVerifier; + emit DAVerifierUpdate(_daVerifier); + } + } + /// @inheritdoc IAdmin function setPendingGovernor(address _newPendingGovernor) external onlyGovernor { // Save previous value into the stack to put it into the event later diff --git a/l1-contracts/contracts/zksync/facets/Executor.sol b/l1-contracts/contracts/zksync/facets/Executor.sol index 72a37c0b06..01ba3e8832 100644 --- a/l1-contracts/contracts/zksync/facets/Executor.sol +++ b/l1-contracts/contracts/zksync/facets/Executor.sol @@ -286,6 +286,9 @@ contract ExecutorFacet is Base, IExecutor { bytes32 priorityOperationsHash = _collectOperationsFromPriorityQueue(_storedBatch.numberOfLayer1Txs); require(priorityOperationsHash == _storedBatch.priorityOperationsHash, "x"); // priority operations hash does not match to expected + // Public data should be available + require(isBatchDataAvailable(_storedBatch.commitment), "x1"); + // Save root hash of L2 -> L1 logs tree s.l2LogsRootHashes[currentBatchNumber] = _storedBatch.l2LogsTreeRoot; } @@ -311,6 +314,14 @@ contract ExecutorFacet is Base, IExecutor { return true; } + /// @inheritdoc IExecutor + function isBatchDataAvailable(bytes32 _commitment) public view returns (bool) { + if (address(s.daVerifier) == address(0)) { + return true; + } + return s.daVerifier.isValidCommitment(_commitment); + } + /// @inheritdoc IExecutor function executeBatches(StoredBatchInfo[] calldata _batchesData) external nonReentrant onlyValidator { uint256 nBatches = _batchesData.length; diff --git a/l1-contracts/contracts/zksync/interfaces/IAdmin.sol b/l1-contracts/contracts/zksync/interfaces/IAdmin.sol index ef0c7976ca..a1bbab1681 100644 --- a/l1-contracts/contracts/zksync/interfaces/IAdmin.sol +++ b/l1-contracts/contracts/zksync/interfaces/IAdmin.sol @@ -6,6 +6,7 @@ import {IBase} from "./IBase.sol"; import {Diamond} from "../libraries/Diamond.sol"; import {FeeParams} from "../Storage.sol"; import {IL2Gateway} from "./IL2Gateway.sol"; +import {IDAVerifier} from "./IDAVerifier.sol"; /// @title The interface of the Admin Contract that controls access rights for contract management. /// @author Matter Labs @@ -20,6 +21,10 @@ interface IAdmin is IBase { /// @param _active Active flag function setSecondaryChainGateway(address _gateway, bool _active) external; + /// @notice Update da verifier + /// @param _daVerifier The da verifier + function setDAVerifier(IDAVerifier _daVerifier) external; + /// @notice Starts the transfer of governor rights. Only the current governor can propose a new pending one. /// @notice New governor can accept governor rights by calling `acceptGovernor` function. /// @param _newPendingGovernor Address of the new governor @@ -72,6 +77,9 @@ interface IAdmin is IBase { /// @notice SecondaryChain's status changed event SecondaryChainStatusUpdate(address indexed gateway, bool isActive); + /// @notice DAVerifier updated + event DAVerifierUpdate(IDAVerifier indexed daVerifier); + /// @notice Porter availability status changes event IsPorterAvailableStatusUpdate(bool isPorterAvailable); diff --git a/l1-contracts/contracts/zksync/interfaces/IDAVerifier.sol b/l1-contracts/contracts/zksync/interfaces/IDAVerifier.sol new file mode 100644 index 0000000000..8523627da1 --- /dev/null +++ b/l1-contracts/contracts/zksync/interfaces/IDAVerifier.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +/// @title The interface of the DAVerifier contract, responsible for the commitment verification of batch. +/// @author zk.link +/// @custom:security-contact security@matterlabs.dev +interface IDAVerifier { + /// @notice Is a valid commitment. + function isValidCommitment(bytes32 commitment) external view returns (bool); +} diff --git a/l1-contracts/contracts/zksync/interfaces/IExecutor.sol b/l1-contracts/contracts/zksync/interfaces/IExecutor.sol index e53332ffda..e56c0a119f 100644 --- a/l1-contracts/contracts/zksync/interfaces/IExecutor.sol +++ b/l1-contracts/contracts/zksync/interfaces/IExecutor.sol @@ -105,6 +105,10 @@ interface IExecutor is IBase { /// @param _batchesData Data of the batches to be executed. function isBatchesSynced(StoredBatchInfo[] calldata _batchesData) external view returns (bool); + /// @notice Check if data is public + /// @param _commitment Commitment of the batch. + function isBatchDataAvailable(bytes32 _commitment) external view returns (bool); + /// @notice The function called by the operator to finalize (execute) batches. It is responsible for: /// - Processing all pending operations (commpleting priority requests). /// - Finalizing this batch (i.e. allowing to withdraw funds from the system) diff --git a/l1-contracts/package.json b/l1-contracts/package.json index 7f383136ba..64837a0043 100644 --- a/l1-contracts/package.json +++ b/l1-contracts/package.json @@ -84,7 +84,8 @@ "upgrade-system": "ts-node upgrade-system/index.ts", "set-gateway": "ts-node scripts/set-gateway.ts", "set-admin": "ts-node scripts/set-admin.ts", - "deploy-l1-erc20-bridge-imple": "ts-node scripts/deploy-l1-erc20-bridge-impl.ts" + "deploy-l1-erc20-bridge-imple": "ts-node scripts/deploy-l1-erc20-bridge-impl.ts", + "deploy-celestia-da-verifier": "ts-node scripts/deploy-celestia-da-verifier.ts" }, "dependencies": { "dotenv": "^16.0.3" diff --git a/l1-contracts/scripts/deploy-celestia-da-verifier.ts b/l1-contracts/scripts/deploy-celestia-da-verifier.ts new file mode 100644 index 0000000000..dd8f5d996f --- /dev/null +++ b/l1-contracts/scripts/deploy-celestia-da-verifier.ts @@ -0,0 +1,60 @@ +import { Command } from "commander"; +import { Wallet, ethers } from "ethers"; +import { Deployer } from "../src.ts/deploy"; +import { formatUnits, parseUnits } from "ethers/lib/utils"; +import * as fs from "fs"; +import * as path from "path"; +import { web3Provider } from "./utils"; + +const provider = web3Provider(); +const testConfigPath = path.join(process.env.ZKSYNC_HOME as string, "etc/test_config/constant"); +const ethTestConfig = JSON.parse(fs.readFileSync(`${testConfigPath}/eth.json`, { encoding: "utf-8" })); + +async function main() { + const program = new Command(); + + program.version("0.1.0").name("deploy-celestia-da-verifier"); + + program + .option("--private-key ") + .option("--gas-price ") + .option("--nonce ") + .option("--create2-salt ") + .requiredOption("--da-oracle ") + .action(async (cmd) => { + const deployWallet = cmd.privateKey + ? new Wallet(cmd.privateKey, provider) + : Wallet.fromMnemonic( + process.env.MNEMONIC ? process.env.MNEMONIC : ethTestConfig.mnemonic, + "m/44'/60'/0'/0/1" + ).connect(provider); + console.log(`Using deployer wallet: ${deployWallet.address}`); + + const gasPrice = cmd.gasPrice ? parseUnits(cmd.gasPrice, "gwei") : await provider.getGasPrice(); + console.log(`Using gas price: ${formatUnits(gasPrice, "gwei")} gwei`); + + let nonce = cmd.nonce ? parseInt(cmd.nonce) : await deployWallet.getTransactionCount(); + console.log(`Using nonce: ${nonce}`); + + const create2Salt = cmd.create2Salt ? cmd.create2Salt : ethers.utils.hexlify(ethers.utils.randomBytes(32)); + + const daOracle = cmd.daOracle; + console.log(`DA oracle: ${daOracle}`); + + const deployer = new Deployer({ + deployWallet, + verbose: true, + }); + + await deployer.deployCelestiaDAVerifier(daOracle, create2Salt, { gasPrice, nonce }); + }); + + await program.parseAsync(process.argv); +} + +main() + .then(() => process.exit(0)) + .catch((err) => { + console.error("Error:", err); + process.exit(1); + }); diff --git a/l1-contracts/scripts/utils.ts b/l1-contracts/scripts/utils.ts index dacf1e1a9a..ced085096a 100644 --- a/l1-contracts/scripts/utils.ts +++ b/l1-contracts/scripts/utils.ts @@ -177,6 +177,7 @@ export interface DeployedAddresses { ExecutorFacet: string; GettersFacet: string; Verifier: string; + DAVerifier: string; DiamondInit: string; DiamondUpgradeInit: string; DefaultUpgrade: string; @@ -205,6 +206,7 @@ export function deployedAddressesFromEnv(): DeployedAddresses { DefaultUpgrade: getAddressFromEnv("CONTRACTS_DEFAULT_UPGRADE_ADDR"), DiamondProxy: getAddressFromEnv("CONTRACTS_DIAMOND_PROXY_ADDR"), Verifier: getAddressFromEnv("CONTRACTS_VERIFIER_ADDR"), + DAVerifier: getAddressFromEnv("CONTRACTS_DA_VERIFIER_ADDR"), }, Bridges: { ERC20BridgeImplementation: getAddressFromEnv("CONTRACTS_L1_ERC20_BRIDGE_IMPL_ADDR"), diff --git a/l1-contracts/src.ts/deploy.ts b/l1-contracts/src.ts/deploy.ts index 8839818bde..f6fe0056c6 100644 --- a/l1-contracts/src.ts/deploy.ts +++ b/l1-contracts/src.ts/deploy.ts @@ -226,6 +226,21 @@ export class Deployer { this.addresses.ZkSync.Verifier = contractAddress; } + public async deployCelestiaDAVerifier( + daOracle: string, + create2Salt: string, + ethTxOptions: ethers.providers.TransactionRequest + ) { + ethTxOptions.gasLimit ??= L1_GAS_LIMIT; + const contractAddress = await this.deployViaCreate2("CelestiaDAVerifier", [daOracle], create2Salt, ethTxOptions); + + if (this.verbose) { + console.log(`CONTRACTS_DA_VERIFIER_ADDR=${contractAddress}`); + } + + this.addresses.ZkSync.DAVerifier = contractAddress; + } + public async deployERC20BridgeImplementation(create2Salt: string, ethTxOptions: ethers.providers.TransactionRequest) { ethTxOptions.gasLimit ??= L1_GAS_LIMIT; const contractAddress = await this.deployViaCreate2( diff --git a/l1-contracts/upgrade-system/facets.ts b/l1-contracts/upgrade-system/facets.ts index d6b554d78f..3ad0f4d95f 100644 --- a/l1-contracts/upgrade-system/facets.ts +++ b/l1-contracts/upgrade-system/facets.ts @@ -21,7 +21,7 @@ async function deployFacetCut( ) { create2Salt = create2Salt ?? ethers.constants.HashZero; - ethTxOptions["gasLimit"] = 10_000_000; + ethTxOptions["gasLimit"] = 3_000_000; const [address, txHash] = await deployViaCreate2(wallet, name, [], create2Salt, ethTxOptions, create2Address, true); console.log(`Deployed ${name} at ${address} with txHash ${txHash}`);