From 3a7523f0008d58bee3e839bed37d62161aa39b36 Mon Sep 17 00:00:00 2001 From: jeff <113397187+cyberhorsey@users.noreply.github.com> Date: Mon, 6 Feb 2023 00:39:15 -0800 Subject: [PATCH] feat(protocol): add more protocol/tokenomics tests (#12988) Co-authored-by: Daniel Wang <99078276+dantaik@users.noreply.github.com> Co-authored-by: Roger <50648015+RogerLamTd@users.noreply.github.com> Co-authored-by: David --- .github/workflows/protocol.yml | 6 +- packages/protocol/contracts/L1/TaikoData.sol | 1 + packages/protocol/contracts/L1/TaikoL1.sol | 19 +- .../contracts/L1/libs/LibProposing.sol | 1 + .../protocol/contracts/L1/libs/LibProving.sol | 58 +-- .../contracts/L1/libs/LibVerifying.sol | 6 +- .../contracts/libs/LibSharedConfig.sol | 3 +- .../contracts/test/L1/TestTaikoL1.sol | 8 +- .../test/L1/TestTaikoL1EnableTokenomics.sol | 7 +- .../contracts/test/L1/TestTaikoL2.sol | 1 + .../L1/TestTaikoL2EnablePublicInputsCheck.sol | 1 + .../contracts/test/libs/TestLibProving.sol | 400 ++++++++++++++++++ packages/protocol/package.json | 4 +- .../test/L1/TaikoL1.integration.test.ts | 277 ++++++++---- packages/protocol/test/L1/TaikoL1.test.ts | 13 +- .../test/tokenomics/Tokenomics.test.ts | 271 ------------ .../protocol/test/tokenomics/blockFee.test.ts | 117 +++++ .../test/tokenomics/proofReward.test.ts | 232 ++++++++++ packages/protocol/test/tokenomics/utils.ts | 70 --- .../protocol/test/utils/addressManager.ts | 2 +- packages/protocol/test/utils/array.ts | 5 + packages/protocol/test/utils/blockListener.ts | 22 + .../protocol/test/utils/block_metadata.ts | 21 +- packages/protocol/test/utils/commit.ts | 47 +- packages/protocol/test/utils/encoding.ts | 2 +- packages/protocol/test/utils/evidence.ts | 1 + packages/protocol/test/utils/fixture.ts | 134 ++++++ packages/protocol/test/utils/halt.ts | 11 + packages/protocol/test/utils/onNewL2Block.ts | 55 +++ packages/protocol/test/utils/propose.ts | 1 - packages/protocol/test/utils/proposer.ts | 64 +-- packages/protocol/test/utils/prove.ts | 55 +-- packages/protocol/test/utils/prover.ts | 49 ++- packages/protocol/test/utils/seed.ts | 14 +- packages/protocol/test/utils/taikoL1.ts | 18 +- packages/protocol/test/utils/verify.ts | 180 ++++++++ .../common/ConfigManager.md | 29 -- pnpm-lock.yaml | 42 +- 38 files changed, 1620 insertions(+), 627 deletions(-) create mode 100644 packages/protocol/contracts/test/libs/TestLibProving.sol delete mode 100644 packages/protocol/test/tokenomics/Tokenomics.test.ts create mode 100644 packages/protocol/test/tokenomics/blockFee.test.ts create mode 100644 packages/protocol/test/tokenomics/proofReward.test.ts delete mode 100644 packages/protocol/test/tokenomics/utils.ts create mode 100644 packages/protocol/test/utils/array.ts create mode 100644 packages/protocol/test/utils/blockListener.ts create mode 100644 packages/protocol/test/utils/fixture.ts create mode 100644 packages/protocol/test/utils/halt.ts create mode 100644 packages/protocol/test/utils/onNewL2Block.ts create mode 100644 packages/protocol/test/utils/verify.ts delete mode 100644 packages/website/pages/docs/reference/contract-documentation/common/ConfigManager.md diff --git a/.github/workflows/protocol.yml b/.github/workflows/protocol.yml index 7dd2ecbe2a..835cf9e914 100644 --- a/.github/workflows/protocol.yml +++ b/.github/workflows/protocol.yml @@ -32,9 +32,9 @@ jobs: working-directory: ./packages/protocol run: pnpm test:integration - # - name: protocol - Tokenomics Tests - # working-directory: ./packages/protocol - # run: pnpm test:tokenomics + - name: protocol - Tokenomics Tests + working-directory: ./packages/protocol + run: pnpm test:tokenomics - name: protocol - Test Coverage working-directory: ./packages/protocol diff --git a/packages/protocol/contracts/L1/TaikoData.sol b/packages/protocol/contracts/L1/TaikoData.sol index 87ab8a4523..a95b0edaca 100644 --- a/packages/protocol/contracts/L1/TaikoData.sol +++ b/packages/protocol/contracts/L1/TaikoData.sol @@ -40,6 +40,7 @@ library TaikoData { uint64 initialUncleDelay; bool enableTokenomics; bool enablePublicInputsCheck; + bool enableProofValidation; bool enableOracleProver; } diff --git a/packages/protocol/contracts/L1/TaikoL1.sol b/packages/protocol/contracts/L1/TaikoL1.sol index 5116f71aef..9ba0b2be63 100644 --- a/packages/protocol/contracts/L1/TaikoL1.sol +++ b/packages/protocol/contracts/L1/TaikoL1.sol @@ -279,11 +279,11 @@ contract TaikoL1 is EssentialContract, IHeaderSync, TaikoEvents { return LibAnchorSignature.signTransaction(hash, k); } - function getBlockProvers( + function getForkChoice( uint256 id, bytes32 parentHash - ) public view returns (address[] memory) { - return state.forkChoices[id][parentHash].provers; + ) public view returns (TaikoData.ForkChoice memory) { + return state.forkChoices[id][parentHash]; } function getUncleProofDelay(uint256 blockId) public view returns (uint64) { @@ -293,4 +293,17 @@ contract TaikoL1 is EssentialContract, IHeaderSync, TaikoEvents { function getConfig() public pure virtual returns (TaikoData.Config memory) { return LibSharedConfig.getConfig(); } + + function isBlockVerifiable( + uint256 blockId, + bytes32 parentHash + ) public view returns (bool) { + return + LibVerifying.isVerifiable({ + state: state, + config: getConfig(), + fc: state.forkChoices[blockId][parentHash], + blockId: blockId + }); + } } diff --git a/packages/protocol/contracts/L1/libs/LibProposing.sol b/packages/protocol/contracts/L1/libs/LibProposing.sol index fd513ef417..fb7116ff75 100644 --- a/packages/protocol/contracts/L1/libs/LibProposing.sol +++ b/packages/protocol/contracts/L1/libs/LibProposing.sol @@ -88,6 +88,7 @@ library LibProposing { meta.txListHash == txList.hashTxList(), "L1:txList" ); + require( state.nextBlockId < state.latestVerifiedId + config.maxNumBlocks, diff --git a/packages/protocol/contracts/L1/libs/LibProving.sol b/packages/protocol/contracts/L1/libs/LibProving.sol index b3e903d0de..62b366ced3 100644 --- a/packages/protocol/contracts/L1/libs/LibProving.sol +++ b/packages/protocol/contracts/L1/libs/LibProving.sol @@ -77,7 +77,11 @@ library LibProving { "L1:circuits:size" ); - { + IProofVerifier proofVerifier = IProofVerifier( + resolver.resolve("proof_verifier", false) + ); + + if (config.enableProofValidation) { // Check anchor tx is valid LibTxDecoder.Tx memory _tx = LibTxDecoder.decodeTx( config.chainId, @@ -109,38 +113,34 @@ library LibProving { ), "L1:anchor:calldata" ); - } - IProofVerifier proofVerifier = IProofVerifier( - resolver.resolve("proof_verifier", false) - ); - - // Check anchor tx is the 1st tx in the block - require( - proofVerifier.verifyMKP({ - key: LibRLPWriter.writeUint(0), - value: anchorTx, - proof: evidence.proofs[zkProofsPerBlock], - root: evidence.header.transactionsRoot - }), - "L1:tx:proof" - ); + // Check anchor tx is the 1st tx in the block + require( + proofVerifier.verifyMKP({ + key: LibRLPWriter.writeUint(0), + value: anchorTx, + proof: evidence.proofs[zkProofsPerBlock], + root: evidence.header.transactionsRoot + }), + "L1:tx:proof" + ); - // Check anchor tx does not throw + // Check anchor tx does not throw - LibReceiptDecoder.Receipt memory receipt = LibReceiptDecoder - .decodeReceipt(anchorReceipt); + LibReceiptDecoder.Receipt memory receipt = LibReceiptDecoder + .decodeReceipt(anchorReceipt); - require(receipt.status == 1, "L1:receipt:status"); - require( - proofVerifier.verifyMKP({ - key: LibRLPWriter.writeUint(0), - value: anchorReceipt, - proof: evidence.proofs[zkProofsPerBlock + 1], - root: evidence.header.receiptsRoot - }), - "L1:receipt:proof" - ); + require(receipt.status == 1, "L1:receipt:status"); + require( + proofVerifier.verifyMKP({ + key: LibRLPWriter.writeUint(0), + value: anchorReceipt, + proof: evidence.proofs[zkProofsPerBlock + 1], + root: evidence.header.receiptsRoot + }), + "L1:receipt:proof" + ); + } // ZK-prove block and mark block proven to be valid. _proveBlock({ diff --git a/packages/protocol/contracts/L1/libs/LibVerifying.sol b/packages/protocol/contracts/L1/libs/LibVerifying.sol index 720dc471c6..a844c8c435 100644 --- a/packages/protocol/contracts/L1/libs/LibVerifying.sol +++ b/packages/protocol/contracts/L1/libs/LibVerifying.sol @@ -77,7 +77,7 @@ library LibVerifying { // Uncle proof can not take more than 2x time the first proof did. if ( - !_isVerifiable({ + !isVerifiable({ state: state, config: config, fc: fc, @@ -247,12 +247,12 @@ library LibVerifying { delete fc.provers; } - function _isVerifiable( + function isVerifiable( TaikoData.State storage state, TaikoData.Config memory config, TaikoData.ForkChoice storage fc, uint256 blockId - ) private view returns (bool) { + ) public view returns (bool) { return // TODO(daniel): remove the next line. (!config.enableOracleProver || fc.provers.length > 1) && diff --git a/packages/protocol/contracts/libs/LibSharedConfig.sol b/packages/protocol/contracts/libs/LibSharedConfig.sol index 514ca49701..fefbc3d8fa 100644 --- a/packages/protocol/contracts/libs/LibSharedConfig.sol +++ b/packages/protocol/contracts/libs/LibSharedConfig.sol @@ -41,8 +41,9 @@ library LibSharedConfig { proofTimeCap: 60 minutes, bootstrapDiscountHalvingPeriod: 180 days, initialUncleDelay: 60 minutes, - enableTokenomics: true, + enableTokenomics: false, enablePublicInputsCheck: true, + enableProofValidation: false, enableOracleProver: true }); } diff --git a/packages/protocol/contracts/test/L1/TestTaikoL1.sol b/packages/protocol/contracts/test/L1/TestTaikoL1.sol index a15b8523f0..86921b823a 100644 --- a/packages/protocol/contracts/test/L1/TestTaikoL1.sol +++ b/packages/protocol/contracts/test/L1/TestTaikoL1.sol @@ -23,7 +23,7 @@ contract TestTaikoL1 is TaikoL1, IProofVerifier { // This number is calculated from maxNumBlocks to make // the 'the maximum value of the multiplier' close to 20.0 config.zkProofsPerBlock = 1; - config.maxVerificationsPerTx = 2; + config.maxVerificationsPerTx = 0; config.commitConfirmations = 1; config.maxProofsPerForkChoice = 5; config.blockMaxGasLimit = 30000000; // TODO @@ -44,11 +44,11 @@ contract TestTaikoL1 is TaikoL1, IProofVerifier { config.feeGracePeriodPctg = 125; // 125% config.feeMaxPeriodPctg = 375; // 375% config.blockTimeCap = 48 seconds; - config.proofTimeCap = 60 minutes; + config.proofTimeCap = 4 seconds; config.bootstrapDiscountHalvingPeriod = 180 days; - config.initialUncleDelay = 1 minutes; + config.initialUncleDelay = 1 seconds; config.enableTokenomics = false; - config.enablePublicInputsCheck = true; + config.enablePublicInputsCheck = false; config.enableOracleProver = false; } diff --git a/packages/protocol/contracts/test/L1/TestTaikoL1EnableTokenomics.sol b/packages/protocol/contracts/test/L1/TestTaikoL1EnableTokenomics.sol index 3c12444856..1aca5730f3 100644 --- a/packages/protocol/contracts/test/L1/TestTaikoL1EnableTokenomics.sol +++ b/packages/protocol/contracts/test/L1/TestTaikoL1EnableTokenomics.sol @@ -18,12 +18,12 @@ contract TestTaikoL1EnableTokenomics is TaikoL1, IProofVerifier { { config.chainId = 167; // up to 2048 pending blocks - config.maxNumBlocks = 2048; - config.blockHashHistory = 3; + config.maxNumBlocks = 6; + config.blockHashHistory = 10; // This number is calculated from maxNumBlocks to make // the 'the maximum value of the multiplier' close to 20.0 config.zkProofsPerBlock = 1; - config.maxVerificationsPerTx = 2; + config.maxVerificationsPerTx = 0; // dont verify blocks automatically config.commitConfirmations = 1; config.maxProofsPerForkChoice = 5; config.blockMaxGasLimit = 30000000; // TODO @@ -49,6 +49,7 @@ contract TestTaikoL1EnableTokenomics is TaikoL1, IProofVerifier { config.initialUncleDelay = 1 seconds; config.enableTokenomics = true; config.enablePublicInputsCheck = false; + config.enableProofValidation = false; config.enableOracleProver = false; } diff --git a/packages/protocol/contracts/test/L1/TestTaikoL2.sol b/packages/protocol/contracts/test/L1/TestTaikoL2.sol index 1465bb3334..2904fbb95a 100644 --- a/packages/protocol/contracts/test/L1/TestTaikoL2.sol +++ b/packages/protocol/contracts/test/L1/TestTaikoL2.sol @@ -50,6 +50,7 @@ contract TestTaikoL2 is TaikoL2 { config.initialUncleDelay = 1 minutes; config.enableTokenomics = true; config.enablePublicInputsCheck = false; + config.enableProofValidation = false; config.enableOracleProver = false; } } diff --git a/packages/protocol/contracts/test/L1/TestTaikoL2EnablePublicInputsCheck.sol b/packages/protocol/contracts/test/L1/TestTaikoL2EnablePublicInputsCheck.sol index 7e97abe64d..e5f4affec4 100644 --- a/packages/protocol/contracts/test/L1/TestTaikoL2EnablePublicInputsCheck.sol +++ b/packages/protocol/contracts/test/L1/TestTaikoL2EnablePublicInputsCheck.sol @@ -50,6 +50,7 @@ contract TestTaikoL2EnablePublicInputsCheck is TaikoL2 { config.initialUncleDelay = 1 minutes; config.enableTokenomics = true; config.enablePublicInputsCheck = true; + config.enableProofValidation = true; config.enableOracleProver = false; } } diff --git a/packages/protocol/contracts/test/libs/TestLibProving.sol b/packages/protocol/contracts/test/libs/TestLibProving.sol new file mode 100644 index 0000000000..54ffc2d83a --- /dev/null +++ b/packages/protocol/contracts/test/libs/TestLibProving.sol @@ -0,0 +1,400 @@ +// SPDX-License-Identifier: MIT +// _____ _ _ _ _ +// |_ _|_ _(_) |_____ | | __ _| |__ ___ +// | |/ _` | | / / _ \ | |__/ _` | '_ (_-< +// |_|\__,_|_|_\_\___/ |____\__,_|_.__/__/ + +// This file is an exact copy of LibProving.sol except the implementation of the following methods are empty: + +// _validateAnchorTxSignature +// _checkMetadata +// _validateHeaderForMetadata + +// @dev we need to update this when we update LibProving.sol + +pragma solidity ^0.8.9; + +import "../../L1/libs/LibProving.sol"; +import "../../common/AddressResolver.sol"; +import "../../libs/LibAnchorSignature.sol"; +import "../../libs/LibBlockHeader.sol"; +import "../../libs/LibReceiptDecoder.sol"; +import "../../libs/LibTxDecoder.sol"; +import "../../libs/LibTxUtils.sol"; +import "../../thirdparty/LibBytesUtils.sol"; +import "../../thirdparty/LibRLPWriter.sol"; +import "../../L1/libs/LibUtils.sol"; + +library TestLibProving { + using LibBlockHeader for BlockHeader; + using LibUtils for TaikoData.BlockMetadata; + using LibUtils for TaikoData.State; + + struct Evidence { + TaikoData.BlockMetadata meta; + BlockHeader header; + address prover; + bytes[] proofs; // The first zkProofsPerBlock are ZKPs, + // followed by MKPs. + uint16[] circuits; // The circuits IDs (size === zkProofsPerBlock) + } + + bytes32 public constant INVALIDATE_BLOCK_LOG_TOPIC = + keccak256("BlockInvalidated(bytes32)"); + + bytes4 public constant ANCHOR_TX_SELECTOR = + bytes4(keccak256("anchor(uint256,bytes32)")); + + event BlockProven( + uint256 indexed id, + bytes32 parentHash, + bytes32 blockHash, + uint64 timestamp, + uint64 provenAt, + address prover + ); + + function proveBlock( + TaikoData.State storage state, + TaikoData.Config memory config, + AddressResolver resolver, + uint256 blockId, + bytes[] calldata inputs + ) public { + assert(!LibUtils.isHalted(state)); + + // Check and decode inputs + require(inputs.length == 3, "L1:inputs:size"); + Evidence memory evidence = abi.decode(inputs[0], (Evidence)); + + bytes calldata anchorTx = inputs[1]; + bytes calldata anchorReceipt = inputs[2]; + + // Check evidence + require(evidence.meta.id == blockId, "L1:id"); + + uint256 zkProofsPerBlock = config.zkProofsPerBlock; + require( + evidence.proofs.length == 2 + zkProofsPerBlock, + "L1:proof:size" + ); + require( + evidence.circuits.length == zkProofsPerBlock, + "L1:circuits:size" + ); + + IProofVerifier proofVerifier = IProofVerifier( + resolver.resolve("proof_verifier", false) + ); + + if (config.enableProofValidation) { + // Check anchor tx is valid + LibTxDecoder.Tx memory _tx = LibTxDecoder.decodeTx( + config.chainId, + anchorTx + ); + require(_tx.txType == 0, "L1:anchor:type"); + require( + _tx.destination == + resolver.resolve(config.chainId, "taiko", false), + "L1:anchor:dest" + ); + require( + _tx.gasLimit == config.anchorTxGasLimit, + "L1:anchor:gasLimit" + ); + + // Check anchor tx's signature is valid and deterministic + _validateAnchorTxSignature(config.chainId, _tx); + + // Check anchor tx's calldata is valid + require( + LibBytesUtils.equal( + _tx.data, + bytes.concat( + ANCHOR_TX_SELECTOR, + bytes32(evidence.meta.l1Height), + evidence.meta.l1Hash + ) + ), + "L1:anchor:calldata" + ); + + // Check anchor tx is the 1st tx in the block + require( + proofVerifier.verifyMKP({ + key: LibRLPWriter.writeUint(0), + value: anchorTx, + proof: evidence.proofs[zkProofsPerBlock], + root: evidence.header.transactionsRoot + }), + "L1:tx:proof" + ); + + // Check anchor tx does not throw + + LibReceiptDecoder.Receipt memory receipt = LibReceiptDecoder + .decodeReceipt(anchorReceipt); + + require(receipt.status == 1, "L1:receipt:status"); + require( + proofVerifier.verifyMKP({ + key: LibRLPWriter.writeUint(0), + value: anchorReceipt, + proof: evidence.proofs[zkProofsPerBlock + 1], + root: evidence.header.receiptsRoot + }), + "L1:receipt:proof" + ); + } + + // ZK-prove block and mark block proven to be valid. + _proveBlock({ + state: state, + config: config, + resolver: resolver, + proofVerifier: proofVerifier, + evidence: evidence, + target: evidence.meta, + blockHashOverride: 0 + }); + } + + function proveBlockInvalid( + TaikoData.State storage state, + TaikoData.Config memory config, + AddressResolver resolver, + uint256 blockId, + bytes[] calldata inputs + ) public { + assert(!LibUtils.isHalted(state)); + + // Check and decode inputs + require(inputs.length == 3, "L1:inputs:size"); + Evidence memory evidence = abi.decode(inputs[0], (Evidence)); + TaikoData.BlockMetadata memory target = abi.decode( + inputs[1], + (TaikoData.BlockMetadata) + ); + bytes calldata invalidateBlockReceipt = inputs[2]; + + // Check evidence + require(evidence.meta.id == blockId, "L1:id"); + require( + evidence.proofs.length == 1 + config.zkProofsPerBlock, + "L1:proof:size" + ); + + IProofVerifier proofVerifier = IProofVerifier( + resolver.resolve("proof_verifier", false) + ); + + // Check the event is the first one in the throw-away block + require( + proofVerifier.verifyMKP({ + key: LibRLPWriter.writeUint(0), + value: invalidateBlockReceipt, + proof: evidence.proofs[config.zkProofsPerBlock], + root: evidence.header.receiptsRoot + }), + "L1:receipt:proof" + ); + + // Check the 1st receipt is for an InvalidateBlock tx with + // a BlockInvalidated event + LibReceiptDecoder.Receipt memory receipt = LibReceiptDecoder + .decodeReceipt(invalidateBlockReceipt); + require(receipt.status == 1, "L1:receipt:status"); + require(receipt.logs.length == 1, "L1:receipt:logsize"); + + { + LibReceiptDecoder.Log memory log = receipt.logs[0]; + require( + log.contractAddress == + resolver.resolve(config.chainId, "taiko", false), + "L1:receipt:addr" + ); + require(log.data.length == 0, "L1:receipt:data"); + require( + log.topics.length == 2 && + log.topics[0] == INVALIDATE_BLOCK_LOG_TOPIC && + log.topics[1] == target.txListHash, + "L1:receipt:topics" + ); + } + + // ZK-prove block and mark block proven as invalid. + _proveBlock({ + state: state, + config: config, + resolver: resolver, + proofVerifier: proofVerifier, + evidence: evidence, + target: target, + blockHashOverride: LibUtils.BLOCK_DEADEND_HASH + }); + } + + function _proveBlock( + TaikoData.State storage state, + TaikoData.Config memory config, + AddressResolver resolver, + IProofVerifier proofVerifier, + Evidence memory evidence, + TaikoData.BlockMetadata memory target, + bytes32 blockHashOverride + ) private { + require(evidence.meta.id == target.id, "L1:height"); + require(evidence.prover != address(0), "L1:prover"); + + _checkMetadata({state: state, config: config, meta: target}); + _validateHeaderForMetadata({ + config: config, + header: evidence.header, + meta: evidence.meta + }); + + // For alpha-2 testnet, the network allows any address to submit ZKP, + // but a special prover can skip ZKP verification if the ZKP is empty. + + bool skipZKPVerification; + + // TODO(daniel): remove this special address. + if (config.enableOracleProver) { + bytes32 _blockHash = state + .forkChoices[target.id][evidence.header.parentHash].blockHash; + + if (msg.sender == resolver.resolve("oracle_prover", false)) { + require(_blockHash == 0, "L1:mustBeFirstProver"); + skipZKPVerification = true; + } else { + require(_blockHash != 0, "L1:mustNotBeFirstProver"); + } + } + + bytes32 blockHash = evidence.header.hashBlockHeader(); + + if (!skipZKPVerification) { + for (uint256 i = 0; i < config.zkProofsPerBlock; ++i) { + require( + proofVerifier.verifyZKP({ + verifierId: string( + abi.encodePacked( + "plonk_verifier_", + i, + "_", + evidence.circuits[i] + ) + ), + zkproof: evidence.proofs[i], + blockHash: blockHash, + prover: evidence.prover, + txListHash: evidence.meta.txListHash + }), + "L1:zkp" + ); + } + } + + _markBlockProven({ + state: state, + config: config, + prover: evidence.prover, + target: target, + parentHash: evidence.header.parentHash, + blockHash: blockHashOverride == 0 ? blockHash : blockHashOverride + }); + } + + function _markBlockProven( + TaikoData.State storage state, + TaikoData.Config memory config, + address prover, + TaikoData.BlockMetadata memory target, + bytes32 parentHash, + bytes32 blockHash + ) private { + TaikoData.ForkChoice storage fc = state.forkChoices[target.id][ + parentHash + ]; + + if (fc.blockHash == 0) { + // This is the first proof for this block. + fc.blockHash = blockHash; + + if (!config.enableOracleProver) { + // If the oracle prover is not enabled + // we use the first prover's timestamp + fc.provenAt = uint64(block.timestamp); + } else { + // We keep fc.provenAt as 0. + } + } else { + require( + fc.provers.length < config.maxProofsPerForkChoice, + "L1:proof:tooMany" + ); + + require( + fc.provenAt == 0 || + block.timestamp < + LibUtils.getUncleProofDeadline({ + state: state, + config: config, + fc: fc, + blockId: target.id + }), + "L1:tooLate" + ); + + for (uint256 i = 0; i < fc.provers.length; ++i) { + require(fc.provers[i] != prover, "L1:prover:dup"); + } + + if (fc.blockHash != blockHash) { + // We have a problem here: two proofs are both valid but claims + // the new block has different hashes. + if (config.enableOracleProver) { + revert("L1:proof:conflict"); + } else { + LibUtils.halt(state, true); + return; + } + } + + if (config.enableOracleProver && fc.provenAt == 0) { + // If the oracle prover is enabled, we + // use the second prover's timestamp. + fc.provenAt = uint64(block.timestamp); + } + } + + fc.provers.push(prover); + + emit BlockProven({ + id: target.id, + parentHash: parentHash, + blockHash: blockHash, + timestamp: target.timestamp, + provenAt: fc.provenAt, + prover: prover + }); + } + + function _validateAnchorTxSignature( + uint256 chainId, + LibTxDecoder.Tx memory _tx + ) private view {} + + function _checkMetadata( + TaikoData.State storage state, + TaikoData.Config memory config, + TaikoData.BlockMetadata memory meta + ) private view {} + + function _validateHeaderForMetadata( + TaikoData.Config memory config, + BlockHeader memory header, + TaikoData.BlockMetadata memory meta + ) private pure {} +} diff --git a/packages/protocol/package.json b/packages/protocol/package.json index c9e2c4fa44..643841aca7 100644 --- a/packages/protocol/package.json +++ b/packages/protocol/package.json @@ -18,6 +18,7 @@ "test:genesis": "./test/genesis/generate_genesis.test.sh", "test:integration": "TEST_TYPE=integration ./test/test_integration.sh", "test:tokenomics": "TEST_TYPE=tokenomics ./test/test_integration.sh", + "test:all": "pnpm run test && pnpm run test:integration && pnpm run test:tokenomics", "deploy:hardhat": "./scripts/download_solc.sh && LOG_LEVEL=debug pnpm hardhat deploy_L1 --network hardhat --dao-vault 0xdf08f82de32b8d460adbe8d72043e3a7e25a3b39 --team-vault 0xdf08f82de32b8d460adbe8d72043e3a7e25a3b39 --l2-genesis-block-hash 0xee1950562d42f0da28bd4550d88886bc90894c77c9c9eaefef775d4c8223f259 --bridge-funder-private-key ddbf12f72c946bb1e6de5eaf580c51db51828ba198d9b0dba9c7d48ec748dc04 --bridge-fund 0xff --oracle-prover 0xdf08f82de32b8d460adbe8d72043e3a7e25a3b39 --confirmations 1", "lint-staged": "lint-staged --allow-empty" }, @@ -82,6 +83,7 @@ "dependencies": { "@gnosis.pm/zodiac": "^1.0.7", "@openzeppelin/contracts": "^4.5.0", - "@openzeppelin/contracts-upgradeable": "^4.5.1" + "@openzeppelin/contracts-upgradeable": "^4.5.1", + "channel-ts": "^0.1.2" } } diff --git a/packages/protocol/test/L1/TaikoL1.integration.test.ts b/packages/protocol/test/L1/TaikoL1.integration.test.ts index 38dc69c855..b6a11db754 100644 --- a/packages/protocol/test/L1/TaikoL1.integration.test.ts +++ b/packages/protocol/test/L1/TaikoL1.integration.test.ts @@ -1,54 +1,68 @@ import { expect } from "chai"; +import { SimpleChannel } from "channel-ts"; import { BigNumber, ethers as ethersLib } from "ethers"; import { ethers } from "hardhat"; -import { TaikoL1, TaikoL2 } from "../../typechain"; -import deployAddressManager from "../utils/addressManager"; +import { TaikoL1, TestTkoToken } from "../../typechain"; +import blockListener from "../utils/blockListener"; import { BlockMetadata } from "../utils/block_metadata"; -import { commitBlock, generateCommitHash } from "../utils/commit"; -import { buildProposeBlockInputs, proposeBlock } from "../utils/propose"; -import { getDefaultL2Signer, getL1Provider } from "../utils/provider"; -import { defaultFeeBase, deployTaikoL1 } from "../utils/taikoL1"; -import { deployTaikoL2 } from "../utils/taikoL2"; +import { + commitAndProposeLatestBlock, + commitBlock, + generateCommitHash, +} from "../utils/commit"; +import { initIntegrationFixture } from "../utils/fixture"; +import halt from "../utils/halt"; +import { onNewL2Block } from "../utils/onNewL2Block"; +import { buildProposeBlockInputs } from "../utils/propose"; +import Proposer from "../utils/proposer"; +import { proveBlock } from "../utils/prove"; +import Prover from "../utils/prover"; +import { sendTinyEtherToZeroAddress } from "../utils/seed"; +import { commitProposeProveAndVerify, verifyBlocks } from "../utils/verify"; describe("integration:TaikoL1", function () { let taikoL1: TaikoL1; - let taikoL2: TaikoL2; let l2Provider: ethersLib.providers.JsonRpcProvider; - let l2Signer: ethersLib.Signer; + let l1Signer: any; + let proposerSigner: any; + let genesisHeight: number; + let tkoTokenL1: TestTkoToken; + let chan: SimpleChannel; + let interval: any; + let proverSigner: any; + let proposer: Proposer; + let prover: Prover; + /* eslint-disable-next-line */ + let config: Awaited>; beforeEach(async function () { - l2Provider = new ethers.providers.JsonRpcProvider( - "http://localhost:28545" + ({ + taikoL1, + l2Provider, + l1Signer, + genesisHeight, + proposerSigner, + proverSigner, + interval, + chan, + config, + } = await initIntegrationFixture(false, false)); + proposer = new Proposer( + taikoL1.connect(proposerSigner), + l2Provider, + config.commitConfirmations.toNumber(), + config.maxNumBlocks.toNumber(), + 0, + proposerSigner ); - l2Signer = await getDefaultL2Signer(); - - const l2AddressManager = await deployAddressManager(l2Signer); - taikoL2 = await deployTaikoL2(l2Signer, l2AddressManager); - - const genesisHash = taikoL2.deployTransaction.blockHash as string; - - const l1Provider = getL1Provider(); - - l1Provider.pollingInterval = 100; - - const signers = await ethers.getSigners(); - - const l1AddressManager = await deployAddressManager(signers[0]); - - taikoL1 = await deployTaikoL1( - l1AddressManager, - genesisHash, - false, - defaultFeeBase - ); - - const { chainId: l2ChainId } = await l2Provider.getNetwork(); + prover = new Prover(taikoL1, l2Provider, proverSigner); + }); - await l1AddressManager.setAddress( - `${l2ChainId}.taiko`, - taikoL2.address - ); + afterEach(() => { + clearInterval(interval); + l2Provider.off("block"); + chan.close(); }); describe("isCommitValid()", async function () { @@ -64,6 +78,29 @@ describe("integration:TaikoL1", function () { expect(isCommitValid).to.be.eq(false); }); + + it("should be valid if it has been committed", async function () { + const block = await l2Provider.getBlock("latest"); + const commitSlot = 0; + const { commit, blockCommittedEvent } = await commitBlock( + taikoL1, + block, + commitSlot + ); + expect(blockCommittedEvent).not.to.be.undefined; + + for (let i = 0; i < config.commitConfirmations.toNumber(); i++) { + await sendTinyEtherToZeroAddress(l1Signer); + } + + const isCommitValid = await taikoL1.isCommitValid( + commitSlot, + blockCommittedEvent!.blockNumber, + commit.hash + ); + + expect(isCommitValid).to.be.eq(true); + }); }); describe("getProposedBlock()", function () { @@ -72,6 +109,64 @@ describe("integration:TaikoL1", function () { "L1:id" ); }); + + it("should return valid block if it's been commmited and proposed", async function () { + const commitSlot = 0; + const { proposedEvent } = await commitAndProposeLatestBlock( + taikoL1, + l1Signer, + l2Provider, + commitSlot + ); + expect(proposedEvent).not.to.be.undefined; + expect(proposedEvent.args.meta.commitSlot).to.be.eq(commitSlot); + + const proposedBlock = await taikoL1.getProposedBlock( + proposedEvent.args.meta.id + ); + expect(proposedBlock).not.to.be.undefined; + expect(proposedBlock.proposer).to.be.eq( + await l1Signer.getAddress() + ); + }); + }); + + describe("getForkChoice", function () { + it("returns no empty fork choice for un-proposed, un-proven and un-verified block", async function () { + const forkChoice = await taikoL1.getForkChoice( + 1, + ethers.constants.HashZero + ); + expect(forkChoice.blockHash).to.be.eq(ethers.constants.HashZero); + expect(forkChoice.provenAt).to.be.eq(0); + }); + + it("returns populated data for submitted fork choice", async function () { + const { proposedEvent, block } = await commitAndProposeLatestBlock( + taikoL1, + l1Signer, + l2Provider, + 0 + ); + + expect(proposedEvent).not.to.be.undefined; + const proveEvent = await proveBlock( + taikoL1, + l2Provider, + await l1Signer.getAddress(), + proposedEvent.args.id.toNumber(), + block.number, + proposedEvent.args.meta as any as BlockMetadata + ); + expect(proveEvent).not.to.be.undefined; + + const forkChoice = await taikoL1.getForkChoice( + proposedEvent.args.id.toNumber(), + block.parentHash + ); + expect(forkChoice.blockHash).to.be.eq(block.hash); + expect(forkChoice.provers[0]).to.be.eq(await l1Signer.getAddress()); + }); }); describe("commitBlock() -> proposeBlock() integration", async function () { it("should fail if a proposed block's placeholder field values are not default", async function () { @@ -161,26 +256,7 @@ describe("integration:TaikoL1", function () { }); it("should commit and be able to propose", async function () { - const block = await l2Provider.getBlock("latest"); - const commitSlot = 0; - const { tx, commit } = await commitBlock( - taikoL1, - block, - commitSlot - ); - - const { commitConfirmations } = await taikoL1.getConfig(); - - await tx.wait(commitConfirmations.toNumber()); - const receipt = await proposeBlock( - taikoL1, - block, - commit.txListHash, - tx.blockNumber as number, - block.gasLimit, - commitSlot - ); - expect(receipt.status).to.be.eq(1); + await commitAndProposeLatestBlock(taikoL1, l1Signer, l2Provider, 0); const stateVariables = await taikoL1.getStateVariables(); const nextBlockId = stateVariables[4]; @@ -198,24 +274,16 @@ describe("integration:TaikoL1", function () { }); it("should commit and be able to propose for all available slots, then revert when all slots are taken", async function () { - const { maxNumBlocks } = await taikoL1.getConfig(); // propose blocks and fill up maxNumBlocks number of slots, // expect each one to be successful. - for (let i = 0; i < maxNumBlocks.toNumber() - 1; i++) { - const block = await l2Provider.getBlock("latest"); - const { tx, commit } = await commitBlock(taikoL1, block, i); - - const receipt = await proposeBlock( + for (let i = 0; i < config.maxNumBlocks.toNumber() - 1; i++) { + await commitAndProposeLatestBlock( taikoL1, - block, - commit.txListHash, - tx.blockNumber as number, - block.gasLimit, - i + l1Signer, + l2Provider, + 0 ); - expect(receipt.status).to.be.eq(1); - const stateVariables = await taikoL1.getStateVariables(); const nextBlockId = stateVariables[4]; const proposedBlock = await taikoL1.getProposedBlock( @@ -235,18 +303,69 @@ describe("integration:TaikoL1", function () { // now expect another proposed block to be invalid since all slots are full and none have // been proven. - const block = await l2Provider.getBlock("latest"); - const { tx, commit } = await commitBlock(taikoL1, block); + await expect( + commitAndProposeLatestBlock(taikoL1, l1Signer, l2Provider) + ).to.be.revertedWith("L1:tooMany"); + }); + }); + + describe("getLatestSyncedHeader", function () { + it("iterates through blockHashHistory length and asserts getLatestsyncedHeader returns correct value", async function () { + l2Provider.on("block", blockListener(chan, genesisHeight)); + + let blocks: number = 0; + // iterate through blockHashHistory twice and try to get latest synced header each time. + // we modulo the header height by blockHashHistory in the protocol, so + // this test ensures that logic is sound. + /* eslint-disable-next-line */ + for await (const blockNumber of chan) { + if (blocks > config.blockHashHistory.toNumber() * 2 + 1) { + chan.close(); + return; + } + + const { verifyEvent } = await commitProposeProveAndVerify( + taikoL1, + l2Provider, + blockNumber, + proposer, + tkoTokenL1, + prover + ); + + expect(verifyEvent).not.to.be.undefined; + + const header = await taikoL1.getLatestSyncedHeader(); + expect(header).to.be.eq(verifyEvent.args.blockHash); + blocks++; + } + }); + }); + + describe("proposeBlock", function () { + it("can not propose if chain is halted", async function () { + await halt(taikoL1.connect(l1Signer), true); await expect( - proposeBlock( + onNewL2Block( + l2Provider, + await l2Provider.getBlockNumber(), + proposer, taikoL1, - block, - commit.txListHash, - tx.blockNumber as number, - block.gasLimit + proposerSigner, + tkoTokenL1 ) - ).to.be.revertedWith("L1:tooMany"); + ).to.be.reverted; + }); + }); + + describe("verifyBlocks", function () { + it("can not be called manually to verify block if chain is halted", async function () { + await halt(taikoL1.connect(l1Signer), true); + + await expect(verifyBlocks(taikoL1, 1)).to.be.revertedWith( + "L1:halted" + ); }); }); }); diff --git a/packages/protocol/test/L1/TaikoL1.test.ts b/packages/protocol/test/L1/TaikoL1.test.ts index 3c7c5fb34c..6ec06952af 100644 --- a/packages/protocol/test/L1/TaikoL1.test.ts +++ b/packages/protocol/test/L1/TaikoL1.test.ts @@ -36,17 +36,6 @@ describe("TaikoL1", function () { }); }); - describe("getBlockProvers()", async function () { - it("should return empty list when there is no proof for that block", async function () { - const provers = await taikoL1.getBlockProvers( - Math.ceil(Math.random() * 1024), - randomBytes32() - ); - - expect(provers).to.be.empty; - }); - }); - describe("halt()", async function () { it("should revert called by nonOwner", async function () { const initiallyHalted = await taikoL1.isHalted(); @@ -104,7 +93,7 @@ describe("TaikoL1", function () { const constants = await taikoL1.getConfig(); const maxNumBlocks = constants[1]; const delay = await taikoL1.getUncleProofDelay(maxNumBlocks.mul(2)); - const initialUncleDelay = 60; + const initialUncleDelay = 1; expect(delay).to.be.eq(initialUncleDelay); }); diff --git a/packages/protocol/test/tokenomics/Tokenomics.test.ts b/packages/protocol/test/tokenomics/Tokenomics.test.ts deleted file mode 100644 index 8f29a34073..0000000000 --- a/packages/protocol/test/tokenomics/Tokenomics.test.ts +++ /dev/null @@ -1,271 +0,0 @@ -import { expect } from "chai"; -import { BigNumber, ethers } from "ethers"; -import { ethers as hardhatEthers } from "hardhat"; -import { TaikoL1, TaikoL2 } from "../../typechain"; -import { TestTkoToken } from "../../typechain/TestTkoToken"; -import deployAddressManager from "../utils/addressManager"; -import Proposer from "../utils/proposer"; -// import Prover from "../utils/prover"; -import { - getDefaultL2Signer, - getL1Provider, - getL2Provider, -} from "../utils/provider"; -import createAndSeedWallets from "../utils/seed"; -import sleep from "../utils/sleep"; -import { defaultFeeBase, deployTaikoL1 } from "../utils/taikoL1"; -import { deployTaikoL2 } from "../utils/taikoL2"; -import deployTkoToken from "../utils/tkoToken"; -import { onNewL2Block, sendTinyEtherToZeroAddress } from "./utils"; - -describe("tokenomics", function () { - let taikoL1: TaikoL1; - let taikoL2: TaikoL2; - let l1Provider: ethers.providers.JsonRpcProvider; - let l2Provider: ethers.providers.JsonRpcProvider; - let l1Signer: any; - let l2Signer: any; - let proposerSigner: any; - let proverSigner: any; - let genesisHeight: number; - let genesisHash: string; - let tkoTokenL1: TestTkoToken; - - beforeEach(async () => { - l1Provider = getL1Provider(); - - l1Provider.pollingInterval = 100; - - const signers = await hardhatEthers.getSigners(); - l1Signer = signers[0]; - - l2Provider = getL2Provider(); - - l2Signer = await getDefaultL2Signer(); - - const l2AddressManager = await deployAddressManager(l2Signer); - taikoL2 = await deployTaikoL2(l2Signer, l2AddressManager, false); - - genesisHash = taikoL2.deployTransaction.blockHash as string; - genesisHeight = taikoL2.deployTransaction.blockNumber as number; - - const l1AddressManager = await deployAddressManager(l1Signer); - taikoL1 = await deployTaikoL1( - l1AddressManager, - genesisHash, - true, - defaultFeeBase - ); - const { chainId } = await l1Provider.getNetwork(); - - [proposerSigner, proverSigner] = await createAndSeedWallets( - 2, - l1Signer - ); - - tkoTokenL1 = await deployTkoToken( - l1Signer, - l1AddressManager, - taikoL1.address - ); - - await l1AddressManager.setAddress( - `${chainId}.tko_token`, - tkoTokenL1.address - ); - - const { chainId: l2ChainId } = await l2Provider.getNetwork(); - - await l1AddressManager.setAddress( - `${l2ChainId}.taiko`, - taikoL2.address - ); - - await l1AddressManager.setAddress( - `${chainId}.proof_verifier`, - taikoL1.address - ); - - await tkoTokenL1 - .connect(l1Signer) - .mintAnyone( - await proposerSigner.getAddress(), - ethers.utils.parseEther("100") - ); - - expect( - await tkoTokenL1.balanceOf(await proposerSigner.getAddress()) - ).to.be.eq(ethers.utils.parseEther("100")); - - // set up interval mining so we always get new blocks - await l2Provider.send("evm_setAutomine", [true]); - - // send transactions to L1 so we always get new blocks - setInterval( - async () => await sendTinyEtherToZeroAddress(l1Signer), - 1 * 1000 - ); - - const tx = await l2Signer.sendTransaction({ - to: proverSigner.address, - value: ethers.utils.parseUnits("1", "ether"), - }); - await tx.wait(1); - }); - - it("proposes blocks on interval, blockFee should increase, proposer's balance for TKOToken should decrease as it pays proposer fee, proofReward should increase since slots are growing and no proofs have been submitted", async function () { - const { maxNumBlocks, commitConfirmations } = await taikoL1.getConfig(); - // wait for one period of halving to occur, so fee is not 0. - const blockIdsToNumber: any = {}; - - // set up a proposer to continually propose new blocks - const proposer = new Proposer( - taikoL1.connect(proposerSigner), - l2Provider, - commitConfirmations.toNumber(), - maxNumBlocks.toNumber(), - 0 - ); - - // get the initiaal tkoBalance, which should decrease every block proposal - let lastProposerTkoBalance = await tkoTokenL1.balanceOf( - await proposerSigner.getAddress() - ); - - // do the same for the blockFee, which should increase every block proposal - // with proofs not being submitted. - let lastBlockFee = await taikoL1.getBlockFee(); - while (lastBlockFee.eq(0)) { - await sleep(500); - lastBlockFee = await taikoL1.getBlockFee(); - } - - let lastProofReward = BigNumber.from(0); - - let hasFailedAssertions: boolean = false; - // every time a l2 block is created, we should try to propose it on L1. - l2Provider.on("block", async (blockNumber) => { - if (blockNumber <= genesisHeight) return; - try { - const { newProposerTkoBalance, newBlockFee, newProofReward } = - await onNewL2Block( - l2Provider, - blockNumber, - proposer, - blockIdsToNumber, - taikoL1, - proposerSigner, - tkoTokenL1 - ); - - expect( - newProposerTkoBalance.lt(lastProposerTkoBalance) - ).to.be.eq(true); - expect(newBlockFee.gt(lastBlockFee)).to.be.eq(true); - expect(newProofReward.gt(lastProofReward)).to.be.eq(true); - - lastBlockFee = newBlockFee; - lastProofReward = newProofReward; - lastProposerTkoBalance = newProposerTkoBalance; - } catch (e) { - hasFailedAssertions = true; - console.error(e); - throw e; - } - }); - - await sleep(20 * 1000); - expect(hasFailedAssertions).to.be.eq(false); - }); - - it("block fee should increase as the halving period passes, while no blocks are proposed", async function () { - const { bootstrapDiscountHalvingPeriod } = await taikoL1.getConfig(); - - const iterations: number = 5; - const period: number = bootstrapDiscountHalvingPeriod - .mul(1000) - .toNumber(); - - let lastBlockFee: BigNumber = await taikoL1.getBlockFee(); - - for (let i = 0; i < iterations; i++) { - await sleep(period); - const blockFee = await taikoL1.getBlockFee(); - expect(blockFee.gt(lastBlockFee)).to.be.eq(true); - lastBlockFee = blockFee; - } - }); - - // TODO(jeff): re-enable this test. It is disabled because it randomly fails. - - // it("expects the blockFee to go be 0 when no periods have passed", async function () { - // const blockFee = await taikoL1.getBlockFee(); - // expect(blockFee.eq(0)).to.be.eq(true); - // }); - - // it("propose blocks and prove blocks on interval, proverReward should decline and blockFee should increase", async function () { - // const { maxNumBlocks, commitConfirmations } = await taikoL1.getConfig(); - // const blockIdsToNumber: any = {}; - - // const proposer = new Proposer( - // taikoL1.connect(proposerSigner), - // l2Provider, - // commitConfirmations.toNumber(), - // maxNumBlocks.toNumber(), - // 0 - // ); - - // const prover = new Prover( - // taikoL1, - // taikoL2, - // l1Provider, - // l2Provider, - // proverSigner - // ); - - // let hasFailedAssertions: boolean = false; - // l2Provider.on("block", async (blockNumber) => { - // if (blockNumber <= genesisHeight) return; - // try { - // await expect( - // onNewL2Block( - // l2Provider, - // blockNumber, - // proposer, - // blockIdsToNumber, - // taikoL1, - // proposerSigner, - // tkoTokenL1 - // ) - // ).not.to.throw; - // } catch (e) { - // hasFailedAssertions = true; - // console.error(e); - // throw e; - // } - // }); - - // taikoL1.on( - // "BlockProposed", - // async (id: BigNumber, meta: BlockMetadata) => { - // console.log("proving block: id", id.toString()); - // try { - // await prover.prove( - // await proverSigner.getAddress(), - // id.toNumber(), - // blockIdsToNumber[id.toString()], - // meta - // ); - // } catch (e) { - // hasFailedAssertions = true; - // console.error(e); - // throw e; - // } - // } - // ); - - // await sleep(20 * 1000); - - // expect(hasFailedAssertions).to.be.eq(false); - // }); -}); diff --git a/packages/protocol/test/tokenomics/blockFee.test.ts b/packages/protocol/test/tokenomics/blockFee.test.ts new file mode 100644 index 0000000000..1ea98bec61 --- /dev/null +++ b/packages/protocol/test/tokenomics/blockFee.test.ts @@ -0,0 +1,117 @@ +import { expect } from "chai"; +import { SimpleChannel } from "channel-ts"; +import { BigNumber, ethers } from "ethers"; +import { AddressManager, TaikoL1 } from "../../typechain"; +import { TestTkoToken } from "../../typechain/TestTkoToken"; +import blockListener from "../utils/blockListener"; +import { onNewL2Block } from "../utils/onNewL2Block"; +import Proposer from "../utils/proposer"; + +import sleep from "../utils/sleep"; +import { deployTaikoL1 } from "../utils/taikoL1"; +import { initIntegrationFixture } from "../utils/fixture"; + +describe("tokenomics: blockFee", function () { + let taikoL1: TaikoL1; + let l2Provider: ethers.providers.JsonRpcProvider; + let proposerSigner: any; + let genesisHeight: number; + let genesisHash: string; + let tkoTokenL1: TestTkoToken; + let l1AddressManager: AddressManager; + let interval: any; + let chan: SimpleChannel; + /* eslint-disable-next-line */ + let config: Awaited>; + let proposer: Proposer; + + beforeEach(async () => { + ({ + taikoL1, + l2Provider, + proposerSigner, + genesisHeight, + genesisHash, + tkoTokenL1, + l1AddressManager, + interval, + chan, + config, + proposer, + } = await initIntegrationFixture(true, true)); + }); + + afterEach(() => clearInterval(interval)); + + it("expects getBlockFee to return the initial feeBase at time of contract deployment", async function () { + // deploy a new instance of TaikoL1 so no blocks have passed. + const tL1 = await deployTaikoL1(l1AddressManager, genesisHash, true); + const blockFee = await tL1.getBlockFee(); + expect(blockFee).to.be.eq(0); + }); + + it("block fee should increase as the halving period passes, while no blocks are proposed", async function () { + const iterations: number = 5; + const period: number = config.bootstrapDiscountHalvingPeriod + .mul(1000) + .toNumber(); + + let lastBlockFee: BigNumber = await taikoL1.getBlockFee(); + + for (let i = 0; i < iterations; i++) { + await sleep(period); + const blockFee = await taikoL1.getBlockFee(); + expect(blockFee.gt(lastBlockFee)).to.be.eq(true); + lastBlockFee = blockFee; + } + }); + + it("proposes blocks on interval, blockFee should increase, proposer's balance for TKOToken should decrease as it pays proposer fee, proofReward should increase since more slots are used and no proofs have been submitted", async function () { + // get the initial tkoBalance, which should decrease every block proposal + let lastProposerTkoBalance = await tkoTokenL1.balanceOf( + await proposerSigner.getAddress() + ); + + // do the same for the blockFee, which should increase every block proposal + // with proofs not being submitted. + // we want to wait for enough blocks until the blockFee is no longer 0, then run our + // tests. + let lastBlockFee = await taikoL1.getBlockFee(); + while (lastBlockFee.eq(0)) { + await sleep(500); + lastBlockFee = await taikoL1.getBlockFee(); + } + + let lastProofReward = BigNumber.from(0); + + l2Provider.on("block", blockListener(chan, genesisHeight)); + /* eslint-disable-next-line */ + for await (const blockNumber of chan) { + if ( + blockNumber > + genesisHeight + (config.maxNumBlocks.toNumber() - 1) + ) { + break; + } + const { newProposerTkoBalance, newBlockFee, newProofReward } = + await onNewL2Block( + l2Provider, + blockNumber, + proposer, + taikoL1, + proposerSigner, + tkoTokenL1 + ); + + expect(newProposerTkoBalance.lt(lastProposerTkoBalance)).to.be.eq( + true + ); + expect(newBlockFee.gt(lastBlockFee)).to.be.eq(true); + expect(newProofReward.gt(lastProofReward)).to.be.eq(true); + + lastBlockFee = newBlockFee; + lastProofReward = newProofReward; + lastProposerTkoBalance = newProposerTkoBalance; + } + }); +}); diff --git a/packages/protocol/test/tokenomics/proofReward.test.ts b/packages/protocol/test/tokenomics/proofReward.test.ts new file mode 100644 index 0000000000..0b349e659a --- /dev/null +++ b/packages/protocol/test/tokenomics/proofReward.test.ts @@ -0,0 +1,232 @@ +import { expect } from "chai"; +import { SimpleChannel } from "channel-ts"; +import { ethers } from "ethers"; +import { TaikoL1 } from "../../typechain"; +import { TestTkoToken } from "../../typechain/TestTkoToken"; +import { pickRandomElement } from "../utils/array"; +import blockListener from "../utils/blockListener"; +import Proposer from "../utils/proposer"; +import Prover from "../utils/prover"; +import { createAndSeedWallets } from "../utils/seed"; +import { commitProposeProveAndVerify } from "../utils/verify"; +import { initIntegrationFixture } from "../utils/fixture"; + +describe("tokenomics: proofReward", function () { + let taikoL1: TaikoL1; + let l2Provider: ethers.providers.JsonRpcProvider; + let l1Signer: any; + let proverSigner: any; + let genesisHeight: number; + let tkoTokenL1: TestTkoToken; + let interval: any; + let chan: SimpleChannel; + let proposer: Proposer; + let prover: Prover; + + /* eslint-disable-next-line */ + let config: Awaited>; + + beforeEach(async () => { + ({ + taikoL1, + l2Provider, + l1Signer, + proverSigner, + genesisHeight, + tkoTokenL1, + interval, + chan, + config, + proposer, + prover, + } = await initIntegrationFixture(true, true)); + }); + + afterEach(() => { + clearInterval(interval); + l2Provider.off("block"); + chan.close(); + }); + + it(`proofReward is 1 wei if the prover does not hold any tkoTokens on L1`, async function () { + let proposed: boolean = false; + l2Provider.on("block", function (blockNumber: number) { + if (proposed) { + chan.close(); + l2Provider.off("block"); + return; + } + proposed = true; + + chan.send(blockNumber); + }); + + /* eslint-disable-next-line */ + for await (const blockNumber of chan) { + const proverTkoBalanceBeforeVerification = + await tkoTokenL1.balanceOf(await prover.getSigner().address); + + await commitProposeProveAndVerify( + taikoL1, + l2Provider, + blockNumber, + proposer, + tkoTokenL1, + prover + ); + + const proverTkoBalanceAfterVerification = + await tkoTokenL1.balanceOf(await prover.getSigner().address); + + // prover should have given 1 TKO token, since they + // held no TKO balance. + expect(proverTkoBalanceAfterVerification.sub(1)).to.be.eq( + proverTkoBalanceBeforeVerification + ); + } + }); + + it(`single prover, single proposer. + propose blocks, wait til maxNumBlocks is filled. + proverReward should decline should increase as blocks are proved then verified. + the provers TKO balance should increase as the blocks are verified and + they receive the proofReward. + the proposer should receive a refund on his deposit because he holds a tkoBalance > 0 at time of verification.`, async function () { + // prover needs TKO or their reward will be cut down to 1 wei. + await ( + await tkoTokenL1 + .connect(l1Signer) + .mintAnyone( + await proverSigner.getAddress(), + ethers.utils.parseEther("100") + ) + ).wait(1); + + l2Provider.on("block", blockListener(chan, genesisHeight)); + + /* eslint-disable-next-line */ + for await (const blockNumber of chan) { + if ( + blockNumber > + genesisHeight + (config.maxNumBlocks.toNumber() - 1) + ) { + break; + } + const proverTkoBalanceBeforeVerification = + await tkoTokenL1.balanceOf(await prover.getSigner().address); + + await commitProposeProveAndVerify( + taikoL1, + l2Provider, + blockNumber, + proposer, + tkoTokenL1, + prover + ); + + const proverTkoBalanceAfterVerification = + await tkoTokenL1.balanceOf(await prover.getSigner().address); + + expect( + proverTkoBalanceAfterVerification.gt( + proverTkoBalanceBeforeVerification + ) + ).to.be.eq(true); + } + }); + + it(`multiple provers, multiple proposers. + propose blocks, wait til maxNumBlocks is filled. + proverReward should decline should increase as blocks are proved then verified. + the provers TKO balance should increase as the blocks are verified and + they receive the proofReward. + the proposer should receive a refund on his deposit because he holds a tkoBalance > 0 at time of verification.`, async function () { + const proposers = (await createAndSeedWallets(3, l1Signer)).map( + (p: ethers.Wallet) => + new Proposer( + taikoL1.connect(p), + l2Provider, + config.commitConfirmations.toNumber(), + config.maxNumBlocks.toNumber(), + 0, + p + ) + ); + + const provers = (await createAndSeedWallets(3, l1Signer)).map( + (p: ethers.Wallet) => new Prover(taikoL1, l2Provider, p) + ); + + for (const prover of provers) { + await ( + await tkoTokenL1 + .connect(l1Signer) + .mintAnyone( + await prover.getSigner().getAddress(), + ethers.utils.parseEther("10000") + ) + ).wait(1); + } + for (const proposer of proposers) { + await ( + await tkoTokenL1 + .connect(l1Signer) + .mintAnyone( + await proposer.getSigner().getAddress(), + ethers.utils.parseEther("10000") + ) + ).wait(1); + } + + // prover needs TKO or their reward will be cut down to 1 wei. + await ( + await tkoTokenL1 + .connect(l1Signer) + .mintAnyone( + await proverSigner.getAddress(), + ethers.utils.parseEther("100") + ) + ).wait(1); + + l2Provider.on("block", blockListener(chan, genesisHeight)); + + /* eslint-disable-next-line */ + for await (const blockNumber of chan) { + if (blockNumber > genesisHeight + config.maxNumBlocks.toNumber()) { + break; + } + const prover = pickRandomElement(provers); + const proposer = pickRandomElement(proposers); + const proverTkoBalanceBefore = await tkoTokenL1.balanceOf( + await prover.getSigner().getAddress() + ); + + const proposerTkoBalanceBefore = await tkoTokenL1.balanceOf( + await proposer.getSigner().getAddress() + ); + + await commitProposeProveAndVerify( + taikoL1, + l2Provider, + blockNumber, + proposer, + tkoTokenL1, + prover + ); + + const proverTkoBalanceAfter = await tkoTokenL1.balanceOf( + await prover.getSigner().getAddress() + ); + + const proposerTkoBalanceAfter = await tkoTokenL1.balanceOf( + await proposer.getSigner().getAddress() + ); + + expect(proposerTkoBalanceAfter.lt(proposerTkoBalanceBefore)); + + expect(proverTkoBalanceAfter.gt(proverTkoBalanceBefore)).to.be.eq( + true + ); + } + }); +}); diff --git a/packages/protocol/test/tokenomics/utils.ts b/packages/protocol/test/tokenomics/utils.ts deleted file mode 100644 index b2d56776b4..0000000000 --- a/packages/protocol/test/tokenomics/utils.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { BigNumber, ethers } from "ethers"; -import { TaikoL1, TkoToken } from "../../typechain"; -import Proposer from "../utils/proposer"; - -async function onNewL2Block( - l2Provider: ethers.providers.JsonRpcProvider, - blockNumber: number, - proposer: Proposer, - blockIdsToNumber: any, - taikoL1: TaikoL1, - proposerSigner: any, - tkoTokenL1: TkoToken -): Promise<{ - newProposerTkoBalance: BigNumber; - newBlockFee: BigNumber; - newProofReward: BigNumber; -}> { - const block = await l2Provider.getBlock(blockNumber); - const receipt = await proposer.commitThenProposeBlock(block); - const proposedEvent = (receipt.events as any[]).find( - (e) => e.event === "BlockProposed" - ); - - const { id, meta } = (proposedEvent as any).args; - - console.log("-----------PROPOSED---------------", block.number, id); - - blockIdsToNumber[id.toString()] = block.number; - - const newProofReward = await taikoL1.getProofReward( - new Date().getMilliseconds(), - meta.timestamp - ); - - console.log( - "NEW PROOF REWARD", - ethers.utils.formatEther(newProofReward.toString()), - " TKO" - ); - - const newProposerTkoBalance = await tkoTokenL1.balanceOf( - await proposerSigner.getAddress() - ); - - console.log( - "NEW PROPOSER TKO BALANCE", - ethers.utils.formatEther(newProposerTkoBalance.toString()), - " TKO" - ); - - const newBlockFee = await taikoL1.getBlockFee(); - - console.log( - "NEW BLOCK FEE", - ethers.utils.formatEther(newBlockFee.toString()), - " TKO" - ); - return { newProposerTkoBalance, newBlockFee, newProofReward }; -} - -const sendTinyEtherToZeroAddress = async (signer: any) => { - await signer - .sendTransaction({ - to: ethers.constants.AddressZero, - value: BigNumber.from(1), - }) - .wait(1); -}; - -export { sendTinyEtherToZeroAddress, onNewL2Block }; diff --git a/packages/protocol/test/utils/addressManager.ts b/packages/protocol/test/utils/addressManager.ts index 47e577c116..0b0ad048c8 100644 --- a/packages/protocol/test/utils/addressManager.ts +++ b/packages/protocol/test/utils/addressManager.ts @@ -8,7 +8,7 @@ const deployAddressManager = async (signer: ethers.Signer) => { ) .connect(signer) .deploy(); - await addressManager.init(); + await (await addressManager.init()).wait(1); return addressManager; }; diff --git a/packages/protocol/test/utils/array.ts b/packages/protocol/test/utils/array.ts new file mode 100644 index 0000000000..3455de9d2f --- /dev/null +++ b/packages/protocol/test/utils/array.ts @@ -0,0 +1,5 @@ +function pickRandomElement(arr: T[]): T { + return arr[Math.floor(Math.random() * arr.length)]; +} + +export { pickRandomElement }; diff --git a/packages/protocol/test/utils/blockListener.ts b/packages/protocol/test/utils/blockListener.ts new file mode 100644 index 0000000000..363663ad39 --- /dev/null +++ b/packages/protocol/test/utils/blockListener.ts @@ -0,0 +1,22 @@ +import type { SimpleChannel } from "channel-ts"; + +// blockListener should be called as follows: +// `l2Provider.on("block", blockListener(chan, genesisHeight)` +// it will send incoming blockNumbers, generated from the l2 provider on a new block, +// through a Golang-style channel, which can then be waited on like such: +// for await (const blockNumber of chan) +// so we can then run a commit, propose, prove, and verify flow in our test cases +// in the main javascript event loop, instead of in the event handler of the l2Provider +// itself. + +const blockListener = function ( + chan: SimpleChannel, + genesisHeight: number +) { + return function (blockNumber: number) { + if (blockNumber < genesisHeight) return; + chan.send(blockNumber); + }; +}; + +export default blockListener; diff --git a/packages/protocol/test/utils/block_metadata.ts b/packages/protocol/test/utils/block_metadata.ts index 74532a6ebd..c1ee3b2865 100644 --- a/packages/protocol/test/utils/block_metadata.ts +++ b/packages/protocol/test/utils/block_metadata.ts @@ -1,4 +1,4 @@ -import { BigNumberish } from "ethers"; +import { BigNumber, BigNumberish } from "ethers"; type BlockMetadata = { id: number; @@ -14,4 +14,21 @@ type BlockMetadata = { commitHeight: number; }; -export { BlockMetadata }; +type ForkChoice = { + provenAt: BigNumber; + provers: string[]; + blockHash: string; +}; + +type BlockInfo = { + proposedAt: number; + provenAt: number; + id: number; + parentHash: string; + blockHash: string; + forkChoice: ForkChoice; + deposit: BigNumber; + proposer: string; +}; + +export { BlockMetadata, ForkChoice, BlockInfo }; diff --git a/packages/protocol/test/utils/commit.ts b/packages/protocol/test/utils/commit.ts index f22f366c0c..4ccfb65aaf 100644 --- a/packages/protocol/test/utils/commit.ts +++ b/packages/protocol/test/utils/commit.ts @@ -1,6 +1,10 @@ import { ethers } from "ethers"; import RLP from "rlp"; import { TaikoL1 } from "../../typechain"; +import { BlockProposedEvent } from "../../typechain/LibProposing"; +import { BlockCommittedEvent } from "../../typechain/TaikoEvents"; +import { proposeBlock } from "./propose"; +import { sendTinyEtherToZeroAddress } from "./seed"; const generateCommitHash = ( block: ethers.providers.Block @@ -23,10 +27,49 @@ const commitBlock = async ( ): Promise<{ tx: ethers.ContractTransaction; commit: { hash: string; txListHash: string }; + blockCommittedEvent: BlockCommittedEvent | undefined; + receipt: ethers.ContractReceipt; }> => { const commit = generateCommitHash(block); const tx = await taikoL1.commitBlock(commitSlot, commit.hash); - return { tx, commit }; + const receipt = await tx.wait(1); + const blockCommittedEvent = receipt.events!.find( + (e) => e.event === "BlockCommitted" + ) as any as BlockCommittedEvent; + return { tx, commit, blockCommittedEvent, receipt }; }; -export { generateCommitHash, commitBlock }; +const commitAndProposeLatestBlock = async ( + taikoL1: TaikoL1, + l1Signer: any, + l2Provider: ethers.providers.JsonRpcProvider, + commitSlot: number = 0 +) => { + const { commitConfirmations } = await taikoL1.getConfig(); + const block = await l2Provider.getBlock("latest"); + const { tx, commit } = await commitBlock( + taikoL1.connect(l1Signer), + block, + commitSlot + ); + const commitReceipt = await tx.wait(1); + + for (let i = 0; i < commitConfirmations.toNumber(); i++) { + await sendTinyEtherToZeroAddress(l1Signer); + } + + const proposeReceipt = await proposeBlock( + taikoL1.connect(l1Signer), + block, + commit.txListHash, + commitReceipt.blockNumber as number, + block.gasLimit, + commitSlot + ); + const proposedEvent: BlockProposedEvent = ( + proposeReceipt.events as any[] + ).find((e) => e.event === "BlockProposed"); + return { proposedEvent, proposeReceipt, commitReceipt, commit, block }; +}; + +export { generateCommitHash, commitBlock, commitAndProposeLatestBlock }; diff --git a/packages/protocol/test/utils/encoding.ts b/packages/protocol/test/utils/encoding.ts index 7984bb22e7..4661733e7c 100644 --- a/packages/protocol/test/utils/encoding.ts +++ b/packages/protocol/test/utils/encoding.ts @@ -14,7 +14,7 @@ function encodeBlockMetadata(meta: BlockMetadata) { function encodeEvidence(evidence: Evidence) { return ethers.utils.defaultAbiCoder.encode( [ - "tuple(tuple(uint256 id, uint256 l1Height, bytes32 l1Hash, address beneficiary, bytes32 txListHash, bytes32 mixHash, bytes extraData, uint64 gasLimit, uint64 timestamp, uint64 commitHeight, uint64 commitSlot) meta, tuple(bytes32 parentHash, bytes32 ommersHash, address beneficiary, bytes32 stateRoot, bytes32 transactionsRoot, bytes32 receiptsRoot, bytes32[8] logsBloom, uint256 difficulty, uint128 height, uint64 gasLimit, uint64 gasUsed, uint64 timestamp, bytes extraData, bytes32 mixHash, uint64 nonce, uint256 baseFeePerGas) header, address prover, bytes[] proofs)", + "tuple(tuple(uint256 id, uint256 l1Height, bytes32 l1Hash, address beneficiary, bytes32 txListHash, bytes32 mixHash, bytes extraData, uint64 gasLimit, uint64 timestamp, uint64 commitHeight, uint64 commitSlot) meta, tuple(bytes32 parentHash, bytes32 ommersHash, address beneficiary, bytes32 stateRoot, bytes32 transactionsRoot, bytes32 receiptsRoot, bytes32[8] logsBloom, uint256 difficulty, uint128 height, uint64 gasLimit, uint64 gasUsed, uint64 timestamp, bytes extraData, bytes32 mixHash, uint64 nonce, uint256 baseFeePerGas) header, address prover, bytes[] proofs, uint16[] circuits)", ], [evidence] ); diff --git a/packages/protocol/test/utils/evidence.ts b/packages/protocol/test/utils/evidence.ts index 58e39c1641..2bc68269da 100644 --- a/packages/protocol/test/utils/evidence.ts +++ b/packages/protocol/test/utils/evidence.ts @@ -6,6 +6,7 @@ type Evidence = { header: BlockHeader; prover: string; proofs: string[]; + circuits: number[]; }; export default Evidence; diff --git a/packages/protocol/test/utils/fixture.ts b/packages/protocol/test/utils/fixture.ts new file mode 100644 index 0000000000..dd47b00130 --- /dev/null +++ b/packages/protocol/test/utils/fixture.ts @@ -0,0 +1,134 @@ +import { ethers } from "ethers"; +import deployAddressManager from "./addressManager"; +import { getDefaultL2Signer, getL1Provider, getL2Provider } from "./provider"; +import { defaultFeeBase, deployTaikoL1 } from "./taikoL1"; +import { deployTaikoL2 } from "./taikoL2"; +import deployTkoToken from "./tkoToken"; +import { ethers as hardhatEthers } from "hardhat"; +import { createAndSeedWallets, sendTinyEtherToZeroAddress } from "./seed"; +import { SimpleChannel } from "channel-ts"; +import Proposer from "./proposer"; +import Prover from "./prover"; + +async function initIntegrationFixture( + mintTkoToProposer: boolean, + enableTokenomics: boolean = true +) { + const l1Provider = getL1Provider(); + + l1Provider.pollingInterval = 100; + + const signers = await hardhatEthers.getSigners(); + const l1Signer = signers[0]; + + const l2Provider = getL2Provider(); + + const l2Signer = await getDefaultL2Signer(); + + const l2AddressManager = await deployAddressManager(l2Signer); + const taikoL2 = await deployTaikoL2(l2Signer, l2AddressManager, false); + + const genesisHash = taikoL2.deployTransaction.blockHash as string; + const genesisHeight = taikoL2.deployTransaction.blockNumber as number; + + const l1AddressManager = await deployAddressManager(l1Signer); + const taikoL1 = await deployTaikoL1( + l1AddressManager, + genesisHash, + enableTokenomics, + defaultFeeBase + ); + const { chainId } = await l1Provider.getNetwork(); + + const [proposerSigner, proverSigner] = await createAndSeedWallets( + 2, + l1Signer + ); + + const tkoTokenL1 = await deployTkoToken( + l1Signer, + l1AddressManager, + taikoL1.address + ); + + await ( + await l1AddressManager.setAddress( + `${chainId}.tko_token`, + tkoTokenL1.address + ) + ).wait(1); + + const { chainId: l2ChainId } = await l2Provider.getNetwork(); + + await ( + await l1AddressManager.setAddress(`${l2ChainId}.taiko`, taikoL2.address) + ).wait(1); + + await ( + await l1AddressManager.setAddress( + `${chainId}.proof_verifier`, + taikoL1.address + ) + ).wait(1); + + if (mintTkoToProposer) { + const mintTx = await tkoTokenL1 + .connect(l1Signer) + .mintAnyone( + await proposerSigner.getAddress(), + ethers.utils.parseEther("100") + ); + + await mintTx.wait(1); + } + + // set up interval mining so we always get new blocks + await l2Provider.send("evm_setAutomine", [true]); + + // send transactions to L1 so we always get new blocks + const interval = setInterval( + async () => await sendTinyEtherToZeroAddress(l1Signer), + 1 * 1000 + ); + + const tx = await l2Signer.sendTransaction({ + to: proverSigner.address, + value: ethers.utils.parseUnits("1", "ether"), + }); + await tx.wait(1); + + const chan = new SimpleChannel(); + const config = await taikoL1.getConfig(); + + const proposer = new Proposer( + taikoL1.connect(proposerSigner), + l2Provider, + config.commitConfirmations.toNumber(), + config.maxNumBlocks.toNumber(), + 0, + proposerSigner + ); + + const prover = new Prover(taikoL1, l2Provider, proverSigner); + return { + taikoL1, + taikoL2, + l1Provider, + l2Provider, + l1Signer, + l2Signer, + proposerSigner, + proverSigner, + genesisHeight, + genesisHash, + tkoTokenL1, + l1AddressManager, + interval, + chan, + config, + proposer, + prover, + }; +} + +export { initIntegrationFixture }; diff --git a/packages/protocol/test/utils/halt.ts b/packages/protocol/test/utils/halt.ts new file mode 100644 index 0000000000..1f0efb86c4 --- /dev/null +++ b/packages/protocol/test/utils/halt.ts @@ -0,0 +1,11 @@ +import { expect } from "chai"; +import { TaikoL1 } from "../../typechain"; + +async function halt(taikoL1: TaikoL1, halt: boolean) { + await (await taikoL1.halt(true)).wait(1); + const isHalted = await taikoL1.isHalted(); + expect(isHalted).to.be.eq(halt); + return isHalted; +} + +export default halt; diff --git a/packages/protocol/test/utils/onNewL2Block.ts b/packages/protocol/test/utils/onNewL2Block.ts new file mode 100644 index 0000000000..e1691da027 --- /dev/null +++ b/packages/protocol/test/utils/onNewL2Block.ts @@ -0,0 +1,55 @@ +import { BigNumber, ethers } from "ethers"; +import { TaikoL1, TkoToken } from "../../typechain"; +import { BlockProposedEvent } from "../../typechain/LibProposing"; +import Proposer from "./proposer"; + +// onNewL2Block should be called from a tokenomics test case when a new block +// is generated from the l2Provider. +// It will commit then propose the block to the TaikoL1 contract, +// and then return the latest fee information and the proposal event, +// which can then be asserted on depending on your test case. +async function onNewL2Block( + l2Provider: ethers.providers.JsonRpcProvider, + blockNumber: number, + proposer: Proposer, + taikoL1: TaikoL1, + proposerSigner: any, + tkoTokenL1: TkoToken +): Promise<{ + proposedEvent: BlockProposedEvent; + newProposerTkoBalance: BigNumber; + newBlockFee: BigNumber; + newProofReward: BigNumber; +}> { + const block = await l2Provider.getBlock(blockNumber); + const receipt = await proposer.commitThenProposeBlock(block); + const proposedEvent: BlockProposedEvent = (receipt.events as any[]).find( + (e) => e.event === "BlockProposed" + ); + + const { id, meta } = proposedEvent.args; + + const { enableTokenomics } = await taikoL1.getConfig(); + + const newProofReward = await taikoL1.getProofReward( + new Date().getMilliseconds(), + meta.timestamp + ); + + const newProposerTkoBalance = enableTokenomics + ? await tkoTokenL1.balanceOf(await proposerSigner.getAddress()) + : BigNumber.from(0); + + const newBlockFee = await taikoL1.getBlockFee(); + + console.log("-------------------proposed----------", id); + + return { + proposedEvent, + newProposerTkoBalance, + newBlockFee, + newProofReward, + }; +} + +export { onNewL2Block }; diff --git a/packages/protocol/test/utils/propose.ts b/packages/protocol/test/utils/propose.ts index 2f718fc761..f5655ccf54 100644 --- a/packages/protocol/test/utils/propose.ts +++ b/packages/protocol/test/utils/propose.ts @@ -40,7 +40,6 @@ const proposeBlock = async ( const inputs = buildProposeBlockInputs(block, meta); const tx = await taikoL1.proposeBlock(inputs); - // console.log("Proposed block", tx.hash); const receipt = await tx.wait(1); return receipt; }; diff --git a/packages/protocol/test/utils/proposer.ts b/packages/protocol/test/utils/proposer.ts index 1bb2c98f10..fc74286dd3 100644 --- a/packages/protocol/test/utils/proposer.ts +++ b/packages/protocol/test/utils/proposer.ts @@ -10,6 +10,7 @@ class Proposer { private readonly commitConfirms: number; private readonly maxNumBlocks: number; private nextCommitSlot: number; + private signer: ethers.Wallet; private proposingMutex: boolean = false; @@ -18,44 +19,51 @@ class Proposer { l2Provider: ethers.providers.JsonRpcProvider, commitConfirms: number, maxNumBlocks: number, - initialCommitSlot: number + initialCommitSlot: number, + signer: ethers.Wallet ) { this.taikoL1 = taikoL1; this.l2Provider = l2Provider; this.commitConfirms = commitConfirms; this.maxNumBlocks = maxNumBlocks; this.nextCommitSlot = initialCommitSlot; + this.signer = signer; + } + + getSigner() { + return this.signer; } async commitThenProposeBlock(block?: ethers.providers.Block) { - while (this.proposingMutex) { - await sleep(100); + try { + while (this.proposingMutex) { + await sleep(500); + } + this.proposingMutex = true; + if (!block) block = await this.l2Provider.getBlock("latest"); + const commitSlot = this.nextCommitSlot++; + const { tx, commit } = await commitBlock( + this.taikoL1, + block, + commitSlot + ); + const commitReceipt = await tx.wait(this.commitConfirms ?? 1); + + const receipt = await proposeBlock( + this.taikoL1, + block, + commit.txListHash, + commitReceipt.blockNumber as number, + block.gasLimit, + commitSlot + ); + + this.proposingMutex = false; + + return receipt; + } finally { + this.proposingMutex = false; } - this.proposingMutex = true; - if (!block) block = await this.l2Provider.getBlock("latest"); - const commitSlot = this.nextCommitSlot++; - console.log("commiting ", block.number, "with commit slot", commitSlot); - const { tx, commit } = await commitBlock( - this.taikoL1, - block, - commitSlot - ); - const commitReceipt = await tx.wait(this.commitConfirms ?? 1); - - console.log("proposing", block.number, "with commit slot", commitSlot); - - const receipt = await proposeBlock( - this.taikoL1, - block, - commit.txListHash, - commitReceipt.blockNumber as number, - block.gasLimit, - commitSlot - ); - - this.proposingMutex = false; - - return receipt; } } diff --git a/packages/protocol/test/utils/prove.ts b/packages/protocol/test/utils/prove.ts index 3ee00ec7e8..86805455fd 100644 --- a/packages/protocol/test/utils/prove.ts +++ b/packages/protocol/test/utils/prove.ts @@ -1,6 +1,6 @@ import { ethers } from "ethers"; -import RLP from "rlp"; -import { TaikoL1, TaikoL2 } from "../../typechain"; +import { TaikoL1 } from "../../typechain"; +import { BlockProvenEvent } from "../../typechain/LibProving"; import { BlockMetadata } from "./block_metadata"; import { encodeEvidence } from "./encoding"; import Evidence from "./evidence"; @@ -19,8 +19,10 @@ const buildProveBlockInputs = ( meta: meta, header: header, prover: prover, - proofs: [], // TODO + proofs: [], + circuits: [], }; + // we have mkp + zkp returnign true in testing, so can just push 0xff // instead of actually making proofs for anchor tx, anchor receipt, and // zkp @@ -28,65 +30,40 @@ const buildProveBlockInputs = ( evidence.proofs.push("0xff"); } + for (let i = 0; i < zkProofsPerBlock; i++) { + evidence.circuits.push(1); + } + inputs[0] = encodeEvidence(evidence); inputs[1] = anchorTx; inputs[2] = anchorReceipt; return inputs; }; -// TODO const proveBlock = async ( taikoL1: TaikoL1, - taikoL2: TaikoL2, - l2Signer: ethers.Signer, l2Provider: ethers.providers.JsonRpcProvider, proverAddress: string, blockId: number, blockNumber: number, meta: BlockMetadata -) => { +): Promise => { const config = await taikoL1.getConfig(); const header = await getBlockHeader(l2Provider, blockNumber); - - const anchorTxPopulated = await taikoL2.populateTransaction.anchor( - meta.l1Height, - meta.l1Hash, - { - gasPrice: ethers.utils.parseUnits("5", "gwei"), - gasLimit: config.anchorTxGasLimit, - } - ); - - delete anchorTxPopulated.from; - - const anchorTxSigned = await l2Signer.signTransaction(anchorTxPopulated); - - const anchorTx = await l2Provider.sendTransaction(anchorTxSigned); - - await anchorTx.wait(); - - const anchorReceipt = await anchorTx.wait(1); - - const anchorTxRLPEncoded = RLP.encode( - ethers.utils.serializeTransaction(anchorTxPopulated) - ); - - const anchorReceiptRLPEncoded = RLP.encode( - ethers.utils.serializeTransaction(anchorReceipt) - ); - const inputs = buildProveBlockInputs( meta, header.blockHeader, proverAddress, - anchorTxRLPEncoded, - anchorReceiptRLPEncoded, + "0x", + "0x", config.zkProofsPerBlock.toNumber() ); const tx = await taikoL1.proveBlock(blockId, inputs); - console.log("Proved block tx", tx.hash); const receipt = await tx.wait(1); - return receipt; + const event: BlockProvenEvent = (receipt.events as any[]).find( + (e) => e.event === "BlockProven" + ); + return event; }; export { buildProveBlockInputs, proveBlock }; diff --git a/packages/protocol/test/utils/prover.ts b/packages/protocol/test/utils/prover.ts index 4ca2f2c713..1c9960d173 100644 --- a/packages/protocol/test/utils/prover.ts +++ b/packages/protocol/test/utils/prover.ts @@ -1,29 +1,28 @@ import { ethers } from "ethers"; -import { TaikoL1, TaikoL2 } from "../../typechain"; +import { TaikoL1 } from "../../typechain"; +import { BlockProvenEvent } from "../../typechain/LibProving"; import { BlockMetadata } from "./block_metadata"; import { proveBlock } from "./prove"; import sleep from "./sleep"; class Prover { private readonly taikoL1: TaikoL1; - private readonly taikoL2: TaikoL2; - private readonly l1Provider: ethers.providers.JsonRpcProvider; private readonly l2Provider: ethers.providers.JsonRpcProvider; - private readonly l2Signer: ethers.Signer; private provingMutex: boolean = false; + private readonly signer: ethers.Wallet; constructor( taikoL1: TaikoL1, - taikoL2: TaikoL2, - l1Provider: ethers.providers.JsonRpcProvider, l2Provider: ethers.providers.JsonRpcProvider, - l2Signer: ethers.Signer + signer: ethers.Wallet ) { this.taikoL1 = taikoL1; - this.taikoL2 = taikoL2; - this.l1Provider = l1Provider; this.l2Provider = l2Provider; - this.l2Signer = l2Signer; + this.signer = signer; + } + + getSigner() { + return this.signer; } async prove( @@ -31,22 +30,30 @@ class Prover { blockId: number, blockNumber: number, meta: BlockMetadata - ) { + ): Promise { while (this.provingMutex) { await sleep(100); } this.provingMutex = true; - await proveBlock( - this.taikoL1, - this.taikoL2, - this.l2Signer, - this.l2Provider, - proverAddress, - blockId, - blockNumber, - meta - ); + let blockProvenEvent: BlockProvenEvent; + try { + blockProvenEvent = await proveBlock( + this.taikoL1, + this.l2Provider, + proverAddress, + blockId, + blockNumber, + meta + ); + } catch (e) { + console.error("prove error", e); + throw e; + } finally { + this.provingMutex = false; + } + + return blockProvenEvent; } } diff --git a/packages/protocol/test/utils/seed.ts b/packages/protocol/test/utils/seed.ts index 83665b3ba1..ba4431c5fb 100644 --- a/packages/protocol/test/utils/seed.ts +++ b/packages/protocol/test/utils/seed.ts @@ -7,7 +7,9 @@ const createAndSeedWallets = async ( ): Promise => { const wallets: ethers.Wallet[] = []; for (let i = 0; i < len; i++) { - const wallet = ethers.Wallet.createRandom().connect(signer.provider); + const wallet = ethers.Wallet.createRandom({ + extraEntropy: ethers.utils.randomBytes(32), + }).connect(signer.provider); const tx = await signer.sendTransaction({ to: await wallet.getAddress(), value: amount, @@ -20,4 +22,12 @@ const createAndSeedWallets = async ( return wallets; }; -export default createAndSeedWallets; +const sendTinyEtherToZeroAddress = async (signer: any) => { + const tx = await signer.sendTransaction({ + to: ethers.constants.AddressZero, + value: BigNumber.from(1), + }); + await tx.wait(1); +}; + +export { createAndSeedWallets, sendTinyEtherToZeroAddress }; diff --git a/packages/protocol/test/utils/taikoL1.ts b/packages/protocol/test/utils/taikoL1.ts index f6191c61a5..c5ded6ffca 100644 --- a/packages/protocol/test/utils/taikoL1.ts +++ b/packages/protocol/test/utils/taikoL1.ts @@ -22,8 +22,8 @@ async function deployTaikoL1( await ethers.getContractFactory("LibProposing") ).deploy(); - const libProving = await ( - await ethers.getContractFactory("LibProving", { + const testLibProving = await ( + await ethers.getContractFactory("TestLibProving", { libraries: { LibReceiptDecoder: libReceiptDecoder.address, LibTxDecoder: libTxDecoder.address, @@ -42,17 +42,19 @@ async function deployTaikoL1( libraries: { LibVerifying: libVerifying.address, LibProposing: libProposing.address, - LibProving: libProving.address, + LibProving: testLibProving.address, }, } ) ).deploy(); - await taikoL1.init( - addressManager.address, - genesisHash, - feeBase ?? defaultFeeBase - ); + await ( + await taikoL1.init( + addressManager.address, + genesisHash, + feeBase ?? defaultFeeBase + ) + ).wait(1); return taikoL1 as TaikoL1; } diff --git a/packages/protocol/test/utils/verify.ts b/packages/protocol/test/utils/verify.ts new file mode 100644 index 0000000000..873ab8cfc1 --- /dev/null +++ b/packages/protocol/test/utils/verify.ts @@ -0,0 +1,180 @@ +import { expect } from "chai"; +import { BigNumber, ethers as ethersLib } from "ethers"; +import { ethers } from "hardhat"; +import { TaikoL1, TkoToken } from "../../typechain"; +import { BlockVerifiedEvent } from "../../typechain/LibVerifying"; +import { BlockInfo, BlockMetadata } from "./block_metadata"; +import { onNewL2Block } from "./onNewL2Block"; +import Proposer from "./proposer"; +import Prover from "./prover"; +import sleep from "./sleep"; + +async function verifyBlocks(taikoL1: TaikoL1, maxBlocks: number) { + const verifyTx = await taikoL1.verifyBlocks(maxBlocks); + const verifyReceipt = await verifyTx.wait(1); + const verifiedEvent: BlockVerifiedEvent = ( + verifyReceipt.events as any[] + ).find((e) => e.event === "BlockVerified"); + return verifiedEvent; +} + +async function sleepUntilBlockIsVerifiable( + taikoL1: TaikoL1, + id: number, + provenAt: number +) { + const delay = await taikoL1.getUncleProofDelay(id); + const delayInMs = delay.mul(1000); + await sleep(5 * delayInMs.toNumber()); // TODO: use provenAt, calc difference, etc +} + +async function verifyBlockAndAssert( + taikoL1: TaikoL1, + tkoTokenL1: TkoToken, + block: BlockInfo, + lastProofReward: BigNumber +): Promise<{ newProofReward: BigNumber }> { + await sleepUntilBlockIsVerifiable(taikoL1, block.id, block.provenAt); + + const isVerifiable = await taikoL1.isBlockVerifiable( + block.id, + block.parentHash + ); + + expect(isVerifiable).to.be.eq(true); + + const prover = block.forkChoice.provers[0]; + + const proverTkoBalanceBeforeVerification = await tkoTokenL1.balanceOf( + prover + ); + + const proposerTkoBalanceBeforeVerification = await tkoTokenL1.balanceOf( + block.proposer + ); + + expect(proposerTkoBalanceBeforeVerification.gt(0)).to.be.eq(true); + const verifiedEvent = await verifyBlocks(taikoL1, 1); + expect(verifiedEvent).to.be.not.undefined; + + expect(verifiedEvent.args.blockHash).to.be.eq(block.blockHash); + expect(verifiedEvent.args.id.eq(block.id)).to.be.eq(true); + + const proverTkoBalanceAfterVerification = await tkoTokenL1.balanceOf( + prover + ); + + // prover should have increased in balance as he received the proof reward. + expect( + proverTkoBalanceAfterVerification.gt(proverTkoBalanceBeforeVerification) + ).to.be.eq(true); + + const newProofReward = await taikoL1.getProofReward( + block.proposedAt, + block.provenAt + ); + + // last proof reward should be larger than the new proof reward, + // since we have stopped proposing, and slots are growing as we verify. + if (lastProofReward.gt(0)) { + expect(newProofReward).to.be.lt(lastProofReward); + } + + // latest synced header should be our just-verified block hash. + const latestHash = await taikoL1.getLatestSyncedHeader(); + expect(latestHash).to.be.eq(block.blockHash); + + // fork choice should be nullified via _cleanUp in LibVerifying + const forkChoice = await taikoL1.getForkChoice(block.id, block.parentHash); + expect(forkChoice.provenAt).to.be.eq(BigNumber.from(0)); + expect(forkChoice.provers).to.be.empty; + expect(forkChoice.blockHash).to.be.eq(ethers.constants.HashZero); + + // proposer should be minted their refund of their deposit back after + // verification, as long as their balance is > 0; + return { newProofReward }; +} + +async function commitProposeProveAndVerify( + taikoL1: TaikoL1, + l2Provider: ethersLib.providers.JsonRpcProvider, + blockNumber: number, + proposer: Proposer, + tkoTokenL1: TkoToken, + prover: Prover +) { + console.log("proposing", blockNumber); + const { proposedEvent } = await onNewL2Block( + l2Provider, + blockNumber, + proposer, + taikoL1, + proposer.getSigner(), + tkoTokenL1 + ); + expect(proposedEvent).not.to.be.undefined; + + console.log("proving", blockNumber); + const provedEvent = await prover.prove( + await prover.getSigner().getAddress(), + proposedEvent.args.id.toNumber(), + blockNumber, + proposedEvent.args.meta as any as BlockMetadata + ); + + const { args } = provedEvent; + const { blockHash, id: blockId, parentHash, provenAt } = args; + + const proposedBlock = await taikoL1.getProposedBlock( + proposedEvent.args.id.toNumber() + ); + + const forkChoice = await taikoL1.getForkChoice( + blockId.toNumber(), + parentHash + ); + + expect(forkChoice.blockHash).to.be.eq(blockHash); + + expect(forkChoice.provers[0]).to.be.eq( + await prover.getSigner().getAddress() + ); + + const blockInfo = { + proposedAt: proposedBlock.proposedAt.toNumber(), + provenAt: provenAt.toNumber(), + id: proposedEvent.args.id.toNumber(), + parentHash: parentHash, + blockHash: blockHash, + forkChoice: forkChoice, + deposit: proposedBlock.deposit, + proposer: proposedBlock.proposer, + }; + + // make sure block is verifiable before we processe + await sleepUntilBlockIsVerifiable( + taikoL1, + blockInfo.id, + blockInfo.provenAt + ); + + const isVerifiable = await taikoL1.isBlockVerifiable( + blockInfo.id, + blockInfo.parentHash + ); + expect(isVerifiable).to.be.eq(true); + + console.log("verifying", blockNumber); + const verifyEvent = await verifyBlocks(taikoL1, 1); + expect(verifyEvent).not.to.be.eq(undefined); + console.log("verified", blockNumber); + + return { verifyEvent, proposedEvent, provedEvent, proposedBlock }; +} + +export { + verifyBlocks, + verifyBlockAndAssert, + sleepUntilBlockIsVerifiable, + commitProposeProveAndVerify, +}; diff --git a/packages/website/pages/docs/reference/contract-documentation/common/ConfigManager.md b/packages/website/pages/docs/reference/contract-documentation/common/ConfigManager.md deleted file mode 100644 index 4d91d70344..0000000000 --- a/packages/website/pages/docs/reference/contract-documentation/common/ConfigManager.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -title: ConfigManager ---- - -## ConfigManager - -### Updated - -```solidity -event Updated(string name, bytes newVal, bytes oldVal) -``` - -### init - -```solidity -function init() external -``` - -### setValue - -```solidity -function setValue(string name, bytes val) external -``` - -### getValue - -```solidity -function getValue(string name) public view returns (bytes) -``` diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 408e9f9301..0b7fb6edf8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -137,6 +137,7 @@ importers: '@typescript-eslint/parser': ^4.33.0 chai: ^4.2.0 chalk: 4.1.2 + channel-ts: ^0.1.2 dotenv: ^10.0.0 eslint: ^7.32.0 eslint-config-prettier: ^8.3.0 @@ -167,15 +168,16 @@ importers: '@gnosis.pm/zodiac': 1.1.9 '@openzeppelin/contracts': 4.8.0 '@openzeppelin/contracts-upgradeable': 4.8.0 + channel-ts: 0.1.2 devDependencies: - '@defi-wonderland/smock': 2.3.4_z67mwh6jmxdufyguqgtbmr6rba + '@defi-wonderland/smock': 2.3.4_d44p6lx7t3c2oeudc2zxbd5d54 '@nomicfoundation/hardhat-network-helpers': 1.0.6_hardhat@2.12.2 '@nomiclabs/hardhat-ethers': 2.2.1_3uaf6nt3qt6cyh5fx3fc5i4mn4 '@nomiclabs/hardhat-etherscan': 3.1.2_hardhat@2.12.2 '@nomiclabs/hardhat-waffle': 2.0.3_wzgqb2xh4egjkdzuv52bfmbi24 '@openzeppelin/hardhat-upgrades': 1.21.0_43thxqwvphwusq7frx7sx2tjf4 - '@typechain/ethers-v5': 7.2.0_67wzblpxnm7h4zc6jlzarjupbm - '@typechain/hardhat': 2.3.1_kqlws2vjqn6dp4diaajzvmozky + '@typechain/ethers-v5': 7.2.0_rqgsou2umo6ifhsmtcb3krjbfm + '@typechain/hardhat': 2.3.1_i3sbpo4srhduw7xvdsryjfwk5e '@types/chai': 4.3.4 '@types/glob': 8.0.1 '@types/mocha': 9.1.1 @@ -197,7 +199,7 @@ importers: glob: 8.1.0 hardhat: 2.12.2_2dtigtkb225m7ii7q45utxqwgi hardhat-abi-exporter: 2.10.1_hardhat@2.12.2 - hardhat-docgen: 1.3.0_hardhat@2.12.2 + hardhat-docgen: 1.3.0_lfwxduevmqdtswpjkbv2koayie hardhat-gas-reporter: 1.0.9_hardhat@2.12.2 lint-staged: 12.5.0 merkle-patricia-tree: 4.2.4 @@ -1580,7 +1582,7 @@ packages: '@jridgewell/trace-mapping': 0.3.9 dev: true - /@defi-wonderland/smock/2.3.4_z67mwh6jmxdufyguqgtbmr6rba: + /@defi-wonderland/smock/2.3.4_d44p6lx7t3c2oeudc2zxbd5d54: resolution: {integrity: sha512-VYJbsoCOdFRyGkAwvaQhQRrU6V8AjK3five8xdbo41DEE9n3qXzUNBUxyD9HhXB/dWWPFWT21IGw5Ztl6Qw3Ew==} peerDependencies: '@ethersproject/abi': ^5 @@ -1590,6 +1592,9 @@ packages: ethers: ^5 hardhat: ^2 dependencies: + '@ethersproject/abi': 5.7.0 + '@ethersproject/abstract-provider': 5.7.0 + '@ethersproject/abstract-signer': 5.7.0 '@nomicfoundation/ethereumjs-evm': 1.0.0 '@nomicfoundation/ethereumjs-util': 8.0.0 '@nomicfoundation/ethereumjs-vm': 6.0.0 @@ -3439,7 +3444,7 @@ packages: typechain: 3.0.0_typescript@4.9.3 dev: true - /@typechain/ethers-v5/7.2.0_67wzblpxnm7h4zc6jlzarjupbm: + /@typechain/ethers-v5/7.2.0_rqgsou2umo6ifhsmtcb3krjbfm: resolution: {integrity: sha512-jfcmlTvaaJjng63QsT49MT6R1HFhtO/TBMWbyzPFSzMmVIqb2tL6prnKBs4ZJrSvmgIXWy+ttSjpaxCTq8D/Tw==} peerDependencies: '@ethersproject/abi': ^5.0.0 @@ -3449,6 +3454,9 @@ packages: typechain: ^5.0.0 typescript: '>=4.0.0' dependencies: + '@ethersproject/abi': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/providers': 5.7.2 ethers: 5.7.2 lodash: 4.17.21 ts-essentials: 7.0.3_typescript@4.9.3 @@ -3456,7 +3464,7 @@ packages: typescript: 4.9.3 dev: true - /@typechain/hardhat/2.3.1_kqlws2vjqn6dp4diaajzvmozky: + /@typechain/hardhat/2.3.1_i3sbpo4srhduw7xvdsryjfwk5e: resolution: {integrity: sha512-BQV8OKQi0KAzLXCdsPO0pZBNQQ6ra8A2ucC26uFX/kquRBtJu1yEyWnVSmtr07b5hyRoJRpzUeINLnyqz4/MAw==} peerDependencies: hardhat: ^2.0.10 @@ -3465,6 +3473,7 @@ packages: dependencies: fs-extra: 9.1.0 hardhat: 2.12.2_2dtigtkb225m7ii7q45utxqwgi + lodash: 4.17.21 typechain: 5.2.0_typescript@4.9.3 dev: true @@ -4116,10 +4125,10 @@ packages: source-map: 0.6.1 dev: true - /@vue/component-compiler-utils/3.3.0: + /@vue/component-compiler-utils/3.3.0_lodash@4.17.21: resolution: {integrity: sha512-97sfH2mYNU+2PzGrmK2haqffDpVASuib9/w2/noxiFi31Z54hW+q3izKQXXQZSNhtiUpAI36uSuYepeBe4wpHQ==} dependencies: - consolidate: 0.15.1 + consolidate: 0.15.1_lodash@4.17.21 hash-sum: 1.0.2 lru-cache: 4.1.5 merge-source-map: 1.1.0 @@ -6521,6 +6530,10 @@ packages: supports-color: 7.2.0 dev: true + /channel-ts/0.1.2: + resolution: {integrity: sha512-cI/XiDF+jB0v95Xup8xlM7k93lT3xwPl0WdjEZ9w9aUMf5N+3GQevspK2EDYfMyxcKcXdN1F6PDpuYRpUfaZmg==} + dev: false + /char-regex/1.0.2: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} @@ -6940,7 +6953,7 @@ packages: resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} dev: true - /consolidate/0.15.1: + /consolidate/0.15.1_lodash@4.17.21: resolution: {integrity: sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw==} engines: {node: '>= 0.10.0'} peerDependencies: @@ -7106,6 +7119,7 @@ packages: optional: true dependencies: bluebird: 3.7.2 + lodash: 4.17.21 dev: true /content-disposition/0.5.4: @@ -10426,7 +10440,7 @@ packages: hardhat: 2.12.2_2dtigtkb225m7ii7q45utxqwgi dev: true - /hardhat-docgen/1.3.0_hardhat@2.12.2: + /hardhat-docgen/1.3.0_lfwxduevmqdtswpjkbv2koayie: resolution: {integrity: sha512-paaiOHjJFLCLz2/qM1TQ7ZEG+Vy+LBvJL+SW4A64ZhBnVnyoZ/zv9DvEuawaWhqP5P7AOM6r22reVz4ecWgW7A==} engines: {node: '>=14.14.0'} peerDependencies: @@ -10436,7 +10450,7 @@ packages: hardhat: 2.12.2_2dtigtkb225m7ii7q45utxqwgi html-webpack-plugin: 5.5.0_webpack@5.75.0 vue: 2.7.14 - vue-loader: 15.10.1_z2a3irye3y56jaq776fqxbsiea + vue-loader: 15.10.1_36bk4haztx5nmdoe37hieqy74q vue-router: 3.6.5_vue@2.7.14 vue-template-compiler: 2.7.14 webpack: 5.75.0 @@ -19141,7 +19155,7 @@ packages: resolution: {integrity: sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==} dev: true - /vue-loader/15.10.1_z2a3irye3y56jaq776fqxbsiea: + /vue-loader/15.10.1_36bk4haztx5nmdoe37hieqy74q: resolution: {integrity: sha512-SaPHK1A01VrNthlix6h1hq4uJu7S/z0kdLUb6klubo738NeQoLbS6V9/d8Pv19tU0XdQKju3D1HSKuI8wJ5wMA==} peerDependencies: '@vue/compiler-sfc': ^3.0.8 @@ -19157,7 +19171,7 @@ packages: vue-template-compiler: optional: true dependencies: - '@vue/component-compiler-utils': 3.3.0 + '@vue/component-compiler-utils': 3.3.0_lodash@4.17.21 css-loader: 6.7.2_webpack@5.75.0 hash-sum: 1.0.2 loader-utils: 1.4.2