From 5fdfdfad4207792411f5e92dcee5c603dbeaeee3 Mon Sep 17 00:00:00 2001 From: dantaik <99078276+dantaik@users.noreply.github.com> Date: Tue, 22 Nov 2022 23:18:20 +0800 Subject: [PATCH] feat(protocol): enhance ZKP handling & change proofs order (#288) --- packages/protocol/contracts/L1/LibData.sol | 1 + packages/protocol/contracts/L1/TaikoL1.sol | 48 ++++++++++----- .../protocol/contracts/L1/v1/V1Events.sol | 2 + .../protocol/contracts/L1/v1/V1Finalizing.sol | 27 +++++++-- .../protocol/contracts/L1/v1/V1Proposing.sol | 11 +++- .../protocol/contracts/L1/v1/V1Proving.sol | 58 ++++++++++++------- packages/protocol/contracts/L1/v1/V1Utils.sol | 42 ++++++++++++++ .../protocol/contracts/libs/LibConstants.sol | 2 + .../test/thirdparty/TestMessageSender.sol | 2 +- packages/protocol/tasks/deploy_L1.ts | 12 +++- packages/protocol/test/L1/TaikoL1.test.ts | 18 +++++- 11 files changed, 176 insertions(+), 47 deletions(-) create mode 100644 packages/protocol/contracts/L1/v1/V1Utils.sol diff --git a/packages/protocol/contracts/L1/LibData.sol b/packages/protocol/contracts/L1/LibData.sol index 0c5ec06617..51c9cebc4c 100644 --- a/packages/protocol/contracts/L1/LibData.sol +++ b/packages/protocol/contracts/L1/LibData.sol @@ -44,6 +44,7 @@ library LibData { mapping(uint256 => mapping(bytes32 => ForkChoice)) forkChoices; mapping(bytes32 => uint256) commits; mapping(address => bool) provers; // Whitelisted provers + uint64 statusBits; uint64 genesisHeight; uint64 latestVerifiedHeight; uint64 latestVerifiedId; diff --git a/packages/protocol/contracts/L1/TaikoL1.sol b/packages/protocol/contracts/L1/TaikoL1.sol index 0b5debb79b..fefbb69b2c 100644 --- a/packages/protocol/contracts/L1/TaikoL1.sol +++ b/packages/protocol/contracts/L1/TaikoL1.sol @@ -8,6 +8,8 @@ // ╱╱╰╯╰╯╰┻┻╯╰┻━━╯╰━━━┻╯╰┻━━┻━━╯ pragma solidity ^0.8.9; +import "@openzeppelin/contracts-upgradeable/utils/math/SafeCastUpgradeable.sol"; + import "../common/ConfigManager.sol"; import "../common/EssentialContract.sol"; import "../common/IHeaderSync.sol"; @@ -17,7 +19,7 @@ import "./v1/V1Events.sol"; import "./v1/V1Finalizing.sol"; import "./v1/V1Proposing.sol"; import "./v1/V1Proving.sol"; -import "@openzeppelin/contracts-upgradeable/utils/math/SafeCastUpgradeable.sol"; +import "./v1/V1Utils.sol"; /** * @author dantaik @@ -28,7 +30,7 @@ contract TaikoL1 is EssentialContract, IHeaderSync, V1Events { using SafeCastUpgradeable for uint256; LibData.State public state; - uint256[44] private __gap; + uint256[43] private __gap; function init( address _addressManager, @@ -73,7 +75,8 @@ contract TaikoL1 is EssentialContract, IHeaderSync, V1Events { V1Proposing.proposeBlock(state, inputs); V1Finalizing.verifyBlocks( state, - LibConstants.TAIKO_MAX_VERIFICATIONS_PER_TX + LibConstants.TAIKO_MAX_VERIFICATIONS_PER_TX, + false ); } @@ -99,7 +102,8 @@ contract TaikoL1 is EssentialContract, IHeaderSync, V1Events { V1Proving.proveBlock(state, AddressResolver(this), blockIndex, inputs); V1Finalizing.verifyBlocks( state, - LibConstants.TAIKO_MAX_VERIFICATIONS_PER_TX + LibConstants.TAIKO_MAX_VERIFICATIONS_PER_TX, + false ); } @@ -117,7 +121,6 @@ contract TaikoL1 is EssentialContract, IHeaderSync, V1Events { * on L2. Note that the `invalidBlock` transaction is supposed to * be the only transaction in the L2 block. */ - function proveBlockInvalid( uint256 blockIndex, bytes[] calldata inputs @@ -130,12 +133,21 @@ contract TaikoL1 is EssentialContract, IHeaderSync, V1Events { ); V1Finalizing.verifyBlocks( state, - LibConstants.TAIKO_MAX_VERIFICATIONS_PER_TX + LibConstants.TAIKO_MAX_VERIFICATIONS_PER_TX, + false ); } /** - * Add or remove a prover from the whitelist. + * Verify up to N blocks. + * @param maxBlocks Max number of blocks to verify. + */ + function verifyBlocks(uint256 maxBlocks) external nonReentrant { + require(maxBlocks > 0, "L1:maxBlocks"); + V1Finalizing.verifyBlocks(state, maxBlocks, true); + } + + /* Add or remove a prover from the whitelist. * * @param prover The prover to be added or removed. * @param whitelisted True to add; remove otherwise. @@ -148,7 +160,15 @@ contract TaikoL1 is EssentialContract, IHeaderSync, V1Events { } /** - * Return whether a prover is whitelisted. + * Halt or resume the chain. + * @param toHalt True to halt, false to resume. + */ + function halt(bool toHalt) public onlyOwner { + V1Utils.halt(state, toHalt); + } + + /** + * Check whether a prover is whitelisted. * * @param prover The prover. * @return True if the prover is whitelisted, false otherwise. @@ -157,14 +177,12 @@ contract TaikoL1 is EssentialContract, IHeaderSync, V1Events { return V1Proving.isProverWhitelisted(state, prover); } - - /** - * Verify up to N blocks. - * @param maxBlocks Max number of blocks to verify. + /** + * Check if the L1 is halted. + * @return True if halted, false otherwise. */ - function verifyBlocks(uint256 maxBlocks) external nonReentrant { - require(maxBlocks > 0, "L1:maxBlocks"); - V1Finalizing.verifyBlocks(state, maxBlocks); + function isHalted() public view returns (bool) { + return V1Utils.isHalted(state); } function isCommitValid(bytes32 hash) public view returns (bool) { diff --git a/packages/protocol/contracts/L1/v1/V1Events.sol b/packages/protocol/contracts/L1/v1/V1Events.sol index 484207a34c..e16d921ad1 100644 --- a/packages/protocol/contracts/L1/v1/V1Events.sol +++ b/packages/protocol/contracts/L1/v1/V1Events.sol @@ -29,4 +29,6 @@ abstract contract V1Events { ); event ProverWhitelisted(address indexed prover, bool whitelisted); + + event Halted(bool halted); } diff --git a/packages/protocol/contracts/L1/v1/V1Finalizing.sol b/packages/protocol/contracts/L1/v1/V1Finalizing.sol index 3ed62c9c86..0c2f3b17d6 100644 --- a/packages/protocol/contracts/L1/v1/V1Finalizing.sol +++ b/packages/protocol/contracts/L1/v1/V1Finalizing.sol @@ -8,9 +8,7 @@ // ╱╱╰╯╰╯╰┻┻╯╰┻━━╯╰━━━┻╯╰┻━━┻━━╯ pragma solidity ^0.8.9; -import "@openzeppelin/contracts-upgradeable/utils/math/SafeCastUpgradeable.sol"; - -import "../LibData.sol"; +import "./V1Utils.sol"; /// @author dantaik library V1Finalizing { @@ -31,7 +29,19 @@ library V1Finalizing { emit HeaderSynced(block.number, 0, _genesisBlockHash); } - function verifyBlocks(LibData.State storage s, uint256 maxBlocks) public { + function verifyBlocks( + LibData.State storage s, + uint256 maxBlocks, + bool checkHalt + ) public { + bool halted = V1Utils.isHalted(s); + if (checkHalt) { + require(!halted, "L1:halted"); + } else if (halted) { + // skip finalizing blocks + return; + } + uint64 latestL2Height = s.latestVerifiedHeight; bytes32 latestL2Hash = s.l2Hashes[latestL2Height]; uint64 processed = 0; @@ -43,6 +53,15 @@ library V1Finalizing { ) { LibData.ForkChoice storage fc = s.forkChoices[i][latestL2Hash]; + // TODO(daniel): use the average proof-time. + if ( + block.timestamp <= + fc.provenAt + LibConstants.K_VERIFICATION_DELAY + ) { + // This block is proven but still needs to wait for verificaiton. + break; + } + if (fc.blockHash == LibConstants.TAIKO_BLOCK_DEADEND_HASH) { emit BlockVerified(i, 0); } else if (fc.blockHash != 0) { diff --git a/packages/protocol/contracts/L1/v1/V1Proposing.sol b/packages/protocol/contracts/L1/v1/V1Proposing.sol index 6201aac38a..47bab04c07 100644 --- a/packages/protocol/contracts/L1/v1/V1Proposing.sol +++ b/packages/protocol/contracts/L1/v1/V1Proposing.sol @@ -8,12 +8,10 @@ // ╱╱╰╯╰╯╰┻┻╯╰┻━━╯╰━━━┻╯╰┻━━┻━━╯ pragma solidity ^0.8.9; -import "@openzeppelin/contracts-upgradeable/utils/math/SafeCastUpgradeable.sol"; - import "../../common/ConfigManager.sol"; import "../../libs/LibConstants.sol"; import "../../libs/LibTxDecoder.sol"; -import "../LibData.sol"; +import "./V1Utils.sol"; /// @author dantaik library V1Proposing { @@ -25,6 +23,11 @@ library V1Proposing { event BlockProposed(uint256 indexed id, LibData.BlockMetadata meta); function commitBlock(LibData.State storage s, bytes32 commitHash) public { + // It's OK to allow committing block when the system is halt. + // By not checking the halt status, this method will be cheaper. + // + // require(!V1Utils.isHalted(s), "L1:halt"); + require(commitHash != 0, "L1:hash"); require(s.commits[commitHash] == 0, "L1:committed"); s.commits[commitHash] = block.number; @@ -39,6 +42,8 @@ library V1Proposing { LibData.State storage s, bytes[] calldata inputs ) public { + require(!V1Utils.isHalted(s), "L1:halt"); + require(inputs.length == 2, "L1:inputs:size"); LibData.BlockMetadata memory meta = abi.decode( inputs[0], diff --git a/packages/protocol/contracts/L1/v1/V1Proving.sol b/packages/protocol/contracts/L1/v1/V1Proving.sol index 046d01e9be..97df84ec84 100644 --- a/packages/protocol/contracts/L1/v1/V1Proving.sol +++ b/packages/protocol/contracts/L1/v1/V1Proving.sol @@ -8,8 +8,6 @@ // ╱╱╰╯╰╯╰┻┻╯╰┻━━╯╰━━━┻╯╰┻━━┻━━╯ pragma solidity ^0.8.9; -import "@openzeppelin/contracts-upgradeable/utils/math/SafeCastUpgradeable.sol"; - import "../../common/AddressResolver.sol"; import "../../common/ConfigManager.sol"; import "../../libs/LibAnchorSignature.sol"; @@ -22,7 +20,7 @@ import "../../libs/LibZKP.sol"; import "../../thirdparty/LibBytesUtils.sol"; import "../../thirdparty/LibMerkleTrie.sol"; import "../../thirdparty/LibRLPWriter.sol"; -import "../LibData.sol"; +import "./V1Utils.sol"; /// @author dantaik /// @author david @@ -34,7 +32,7 @@ library V1Proving { LibData.BlockMetadata meta; BlockHeader header; address prover; - bytes[] proofs; + bytes[] proofs; // The first K_ZKPROOFS_PER_BLOCK are ZKPs } event BlockProven( @@ -61,6 +59,8 @@ library V1Proving { uint256 blockIndex, bytes[] calldata inputs ) public onlyWhitelistedProver(s) { + require(!V1Utils.isHalted(s), "L1:halt"); + // Check and decode inputs require(inputs.length == 3, "L1:inputs:size"); Evidence memory evidence = abi.decode(inputs[0], (Evidence)); @@ -69,7 +69,10 @@ library V1Proving { // Check evidence require(evidence.meta.id == blockIndex, "L1:id"); - require(evidence.proofs.length == 3, "L1:proof:size"); + require( + evidence.proofs.length == 2 + LibConstants.K_ZKPROOFS_PER_BLOCK, + "L1:proof:size" + ); // Check anchor tx is valid LibTxDecoder.Tx memory _tx = LibTxDecoder.decodeTx(anchorTx); @@ -105,7 +108,7 @@ library V1Proving { LibMerkleTrie.verifyInclusionProof( LibRLPWriter.writeUint(0), anchorTx, - evidence.proofs[1], + evidence.proofs[LibConstants.K_ZKPROOFS_PER_BLOCK], evidence.header.transactionsRoot ), "L1:tx:proof" @@ -120,7 +123,7 @@ library V1Proving { LibMerkleTrie.verifyInclusionProof( LibRLPWriter.writeUint(0), anchorReceipt, - evidence.proofs[2], + evidence.proofs[LibConstants.K_ZKPROOFS_PER_BLOCK + 1], evidence.header.receiptsRoot ), "L1:receipt:proof" @@ -136,6 +139,8 @@ library V1Proving { uint256 blockIndex, bytes[] calldata inputs ) public onlyWhitelistedProver(s) { + require(!V1Utils.isHalted(s), "L1:halt"); + // Check and decode inputs require(inputs.length == 3, "L1:inputs:size"); Evidence memory evidence = abi.decode(inputs[0], (Evidence)); @@ -147,7 +152,10 @@ library V1Proving { // Check evidence require(evidence.meta.id == blockIndex, "L1:id"); - require(evidence.proofs.length == 2, "L1:proof:size"); + require( + evidence.proofs.length == 1 + LibConstants.K_ZKPROOFS_PER_BLOCK, + "L1:proof:size" + ); // Check the 1st receipt is for an InvalidateBlock tx with // a BlockInvalidated event @@ -175,7 +183,7 @@ library V1Proving { LibMerkleTrie.verifyInclusionProof( LibRLPWriter.writeUint(0), invalidateBlockReceipt, - evidence.proofs[1], + evidence.proofs[LibConstants.K_ZKPROOFS_PER_BLOCK], evidence.header.receiptsRoot ), "L1:receipt:proof" @@ -229,15 +237,17 @@ library V1Proving { bytes32 blockHash = evidence.header.hashBlockHeader(); - LibZKP.verify( - ConfigManager(resolver.resolve("config_manager")).getValue( - "zk_vkey" - ), - evidence.proofs[0], - blockHash, - evidence.prover, - evidence.meta.txListHash - ); + for (uint i = 0; i < LibConstants.K_ZKPROOFS_PER_BLOCK; i++) { + LibZKP.verify( + ConfigManager(resolver.resolve("config_manager")).getValue( + string(abi.encodePacked("zk_vkey_", i)) + ), + evidence.proofs[i], + blockHash, + evidence.prover, + evidence.meta.txListHash + ); + } _markBlockProven( s, @@ -263,9 +273,17 @@ library V1Proving { fc.provenAt = uint64(block.timestamp); } else { require( - fc.blockHash == blockHash && fc.proposedAt == target.timestamp, - "L1:proof:conflict" + fc.proposedAt == target.timestamp, + "L1:proposedAt:conflict" ); + + if (fc.blockHash != blockHash) { + // We have a problem here: two proofs are both valid but claims + // the new block has different hashes. + V1Utils.halt(s, true); + return; + } + require( fc.provers.length < LibConstants.TAIKO_MAX_PROOFS_PER_FORK_CHOICE, diff --git a/packages/protocol/contracts/L1/v1/V1Utils.sol b/packages/protocol/contracts/L1/v1/V1Utils.sol new file mode 100644 index 0000000000..caab538f41 --- /dev/null +++ b/packages/protocol/contracts/L1/v1/V1Utils.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT +// +// ╭━━━━╮╱╱╭╮╱╱╱╱╱╭╮╱╱╱╱╱╭╮ +// ┃╭╮╭╮┃╱╱┃┃╱╱╱╱╱┃┃╱╱╱╱╱┃┃ +// ╰╯┃┃┣┻━┳┫┃╭┳━━╮┃┃╱╱╭━━┫╰━┳━━╮ +// ╱╱┃┃┃╭╮┣┫╰╯┫╭╮┃┃┃╱╭┫╭╮┃╭╮┃━━┫ +// ╱╱┃┃┃╭╮┃┃╭╮┫╰╯┃┃╰━╯┃╭╮┃╰╯┣━━┃ +// ╱╱╰╯╰╯╰┻┻╯╰┻━━╯╰━━━┻╯╰┻━━┻━━╯ +pragma solidity ^0.8.9; + +import "@openzeppelin/contracts-upgradeable/utils/math/SafeCastUpgradeable.sol"; + +import "../../libs/LibMath.sol"; +import "../LibData.sol"; + +/// @author dantaik +library V1Utils { + uint64 public constant MASK_HALT = 1 << 0; + + event Halted(bool halted); + + function halt(LibData.State storage s, bool toHalt) public { + require(isHalted(s) != toHalt, "L1:precondition"); + setBit(s, MASK_HALT, toHalt); + emit Halted(toHalt); + } + + function isHalted(LibData.State storage s) public view returns (bool) { + return isBitOne(s, MASK_HALT); + } + + function setBit(LibData.State storage s, uint64 mask, bool one) private { + s.statusBits = one ? s.statusBits | mask : s.statusBits & ~mask; + } + + function isBitOne( + LibData.State storage s, + uint64 mask + ) private view returns (bool) { + return s.statusBits & mask != 0; + } +} diff --git a/packages/protocol/contracts/libs/LibConstants.sol b/packages/protocol/contracts/libs/LibConstants.sol index 2fb3322f8b..f1beabd67a 100644 --- a/packages/protocol/contracts/libs/LibConstants.sol +++ b/packages/protocol/contracts/libs/LibConstants.sol @@ -11,7 +11,9 @@ pragma solidity ^0.8.9; /// @author dantaik library LibConstants { // https://github.com/ethereum-lists/chains/pull/1611 + uint256 public constant K_ZKPROOFS_PER_BLOCK = 1; uint256 public constant TAIKO_CHAIN_ID = 167; + uint256 public constant K_VERIFICATION_DELAY = 60 minutes; uint256 public constant TAIKO_MAX_PROPOSED_BLOCKS = 2048; uint256 public constant TAIKO_MAX_VERIFICATIONS_PER_TX = 20; uint256 public constant TAIKO_COMMIT_DELAY_CONFIRMATIONS = 4; diff --git a/packages/protocol/contracts/test/thirdparty/TestMessageSender.sol b/packages/protocol/contracts/test/thirdparty/TestMessageSender.sol index a5fd7697b4..29a3f24a62 100644 --- a/packages/protocol/contracts/test/thirdparty/TestMessageSender.sol +++ b/packages/protocol/contracts/test/thirdparty/TestMessageSender.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.9; import "../../bridge/IBridge.sol"; contract TestMessageSender { - bytes32 signal = + bytes32 public signal = 0x3fd54831f488a22b28398de0c567a3b064b937f54f81739ae9bd545967f3abab; function sendMessage( diff --git a/packages/protocol/tasks/deploy_L1.ts b/packages/protocol/tasks/deploy_L1.ts index 83c8204601..da0787961c 100644 --- a/packages/protocol/tasks/deploy_L1.ts +++ b/packages/protocol/tasks/deploy_L1.ts @@ -164,19 +164,27 @@ async function deployBaseLibs(hre: any) { const libTxDecoder = await utils.deployContract(hre, "LibTxDecoder") const libUint512 = await utils.deployContract(hre, "Uint512") - const v1Finalizing = await utils.deployContract(hre, "V1Finalizing") - const v1Proposing = await utils.deployContract(hre, "V1Proposing") + const v1Utils = await utils.deployContract(hre, "V1Utils") + const v1Finalizing = await utils.deployContract(hre, "V1Finalizing", { + V1Utils: v1Utils.address, + }) + const v1Proposing = await utils.deployContract(hre, "V1Proposing", { + V1Utils: v1Utils.address, + }) + const v1Proving = await utils.deployContract(hre, "V1Proving", { LibZKP: libZKP.address, LibReceiptDecoder: libReceiptDecoder.address, LibTxDecoder: libTxDecoder.address, Uint512: libUint512.address, + V1Utils: v1Utils.address, }) return { V1Finalizing: v1Finalizing.address, V1Proposing: v1Proposing.address, V1Proving: v1Proving.address, + V1Utils: v1Utils.address, Uint512: libUint512.address, } } diff --git a/packages/protocol/test/L1/TaikoL1.test.ts b/packages/protocol/test/L1/TaikoL1.test.ts index 78da1380bb..652618b3e7 100644 --- a/packages/protocol/test/L1/TaikoL1.test.ts +++ b/packages/protocol/test/L1/TaikoL1.test.ts @@ -25,8 +25,16 @@ describe("TaikoL1", function () { await ethers.getContractFactory("LibZKP") ).deploy() + const v1Utils = await ( + await ethers.getContractFactory("V1Utils") + ).deploy() + const v1Proposing = await ( - await ethers.getContractFactory("V1Proposing") + await ethers.getContractFactory("V1Proposing", { + libraries: { + V1Utils: v1Utils.address, + }, + }) ).deploy() const v1Proving = await ( @@ -36,12 +44,17 @@ describe("TaikoL1", function () { LibTxDecoder: libTxDecoder.address, LibZKP: libZKP.address, Uint512: uint512.address, + V1Utils: v1Utils.address, }, }) ).deploy() const v1Finalizing = await ( - await ethers.getContractFactory("V1Finalizing") + await ethers.getContractFactory("V1Finalizing", { + libraries: { + V1Utils: v1Utils.address, + }, + }) ).deploy() const TaikoL1Factory = await ethers.getContractFactory("TaikoL1", { @@ -50,6 +63,7 @@ describe("TaikoL1", function () { V1Proposing: v1Proposing.address, V1Proving: v1Proving.address, Uint512: uint512.address, + V1Utils: v1Utils.address, }, })