diff --git a/.github/workflows/solidity.yml b/.github/workflows/solidity.yml index 72a56c8e47..e6406e5dcd 100644 --- a/.github/workflows/solidity.yml +++ b/.github/workflows/solidity.yml @@ -32,6 +32,10 @@ jobs: working-directory: ./packages/protocol run: pnpm test:integration + - name: protocol - Tokenomics Tests + working-directory: ./packages/protocol + run: pnpm test:tokenomics + - name: protocol - Test Coverage working-directory: ./packages/protocol run: pnpm test:coverage diff --git a/packages/protocol/contracts/L1/TaikoData.sol b/packages/protocol/contracts/L1/TaikoData.sol index 795a3380b9..d4eb72cf57 100644 --- a/packages/protocol/contracts/L1/TaikoData.sol +++ b/packages/protocol/contracts/L1/TaikoData.sol @@ -38,9 +38,10 @@ library TaikoData { uint64 feeMaxPeriodPctg; uint64 blockTimeCap; uint64 proofTimeCap; - uint64 boostrapDiscountHalvingPeriod; + uint64 bootstrapDiscountHalvingPeriod; uint64 initialUncleDelay; bool enableTokenomics; + bool enablePublicInputsCheck; } struct BlockMetadata { diff --git a/packages/protocol/contracts/L1/libs/LibUtils.sol b/packages/protocol/contracts/L1/libs/LibUtils.sol index d581a94359..0753caf2e9 100644 --- a/packages/protocol/contracts/L1/libs/LibUtils.sol +++ b/packages/protocol/contracts/L1/libs/LibUtils.sol @@ -138,7 +138,7 @@ library LibUtils { uint256 feeBase ) internal view returns (uint256) { uint256 halves = uint256(block.timestamp - state.genesisTimestamp) / - config.boostrapDiscountHalvingPeriod; + config.bootstrapDiscountHalvingPeriod; uint256 gamma = 1024 - (1024 >> halves); return (feeBase * gamma) / 1024; } diff --git a/packages/protocol/contracts/L2/TaikoL2.sol b/packages/protocol/contracts/L2/TaikoL2.sol index 620697c88b..9d51e7569a 100644 --- a/packages/protocol/contracts/L2/TaikoL2.sol +++ b/packages/protocol/contracts/L2/TaikoL2.sol @@ -76,7 +76,10 @@ contract TaikoL2 is AddressResolver, ReentrancyGuard, IHeaderSync { * @param l1Hash The latest L1 block hash when this block was proposed. */ function anchor(uint256 l1Height, bytes32 l1Hash) external { - _checkPublicInputs(); + TaikoData.Config memory config = getConfig(); + if (config.enablePublicInputsCheck) { + _checkPublicInputs(); + } l1Hashes[l1Height] = l1Hash; latestSyncedHeader = l1Hash; @@ -102,15 +105,18 @@ contract TaikoL2 is AddressResolver, ReentrancyGuard, IHeaderSync { ); require(tx.gasprice == 0, "L2:gasPrice"); + TaikoData.Config memory config = getConfig(); LibInvalidTxList.Reason reason = LibInvalidTxList.isTxListInvalid({ - config: getConfig(), + config: config, encoded: txList, hint: hint, txIdx: txIdx }); require(reason != LibInvalidTxList.Reason.OK, "L2:reason"); - _checkPublicInputs(); + if (config.enablePublicInputsCheck) { + _checkPublicInputs(); + } emit BlockInvalidated(txList.hashTxList()); } @@ -159,6 +165,7 @@ contract TaikoL2 is AddressResolver, ReentrancyGuard, IHeaderSync { uint256 number = block.number; uint256 chainId = block.chainid; + // from 2 to 256, while nnumber is greater than that number for (uint256 i = 2; i <= 256 && number >= i; ++i) { ancestors[(number - i) % 255] = blockhash(number - i); } diff --git a/packages/protocol/contracts/bridge/README.md b/packages/protocol/contracts/bridge/README.md index 4afb604e8b..634460d899 100644 --- a/packages/protocol/contracts/bridge/README.md +++ b/packages/protocol/contracts/bridge/README.md @@ -18,7 +18,7 @@ Let's go deeper into the steps that occur when bridging ETH from srcChain to des User initiates a bridge transaction with `sendMessage` on the source chain which includes: - `depositValue`, `callValue`, and `processingFee` -- these must sum to `msg.value`. -- The destination chain's ID (must be enabled via `Bridge.enableDestChain()`). +- The destination chain's ID (must be enabled via setting `addressResolver` for `${chainID}.bridge`). Inside the `sendMessage` call, the `msg.value` amount of Ether is sent to the srcChain `EtherVault` contract. Next, a `signal` is created from the message, and a `key` is stored on the srcChain bridge contract address. The `key` is a hash of the `signal` and the srcChain bridge contract address. The `key` is stored on the `Bridge` contract with a value of `1`, and a `MessageSent` event is emitted for the relayer to pick up. diff --git a/packages/protocol/contracts/libs/LibSharedConfig.sol b/packages/protocol/contracts/libs/LibSharedConfig.sol index 80e83fcf61..100b4034a5 100644 --- a/packages/protocol/contracts/libs/LibSharedConfig.sol +++ b/packages/protocol/contracts/libs/LibSharedConfig.sol @@ -41,9 +41,10 @@ library LibSharedConfig { feeMaxPeriodPctg: 375, // 375% blockTimeCap: 48 seconds, proofTimeCap: 60 minutes, - boostrapDiscountHalvingPeriod: 180 days, + bootstrapDiscountHalvingPeriod: 180 days, initialUncleDelay: 60 minutes, - enableTokenomics: false + enableTokenomics: false, + enablePublicInputsCheck: true }); } } diff --git a/packages/protocol/contracts/test/L1/TestTaikoL1.sol b/packages/protocol/contracts/test/L1/TestTaikoL1.sol index 2a4e431779..ec8c6b9a6b 100644 --- a/packages/protocol/contracts/test/L1/TestTaikoL1.sol +++ b/packages/protocol/contracts/test/L1/TestTaikoL1.sol @@ -47,9 +47,10 @@ contract TestTaikoL1 is TaikoL1, IProofVerifier { config.feeMaxPeriodPctg = 375; // 375% config.blockTimeCap = 48 seconds; config.proofTimeCap = 60 minutes; - config.boostrapDiscountHalvingPeriod = 180 days; + config.bootstrapDiscountHalvingPeriod = 180 days; config.initialUncleDelay = 1 minutes; config.enableTokenomics = false; + config.enablePublicInputsCheck = true; } function verifyZKP( diff --git a/packages/protocol/contracts/test/L1/TestTaikoL1EnableTokenomics.sol b/packages/protocol/contracts/test/L1/TestTaikoL1EnableTokenomics.sol new file mode 100644 index 0000000000..abb263791a --- /dev/null +++ b/packages/protocol/contracts/test/L1/TestTaikoL1EnableTokenomics.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +// +// ╭━━━━╮╱╱╭╮╱╱╱╱╱╭╮╱╱╱╱╱╭╮ +// ┃╭╮╭╮┃╱╱┃┃╱╱╱╱╱┃┃╱╱╱╱╱┃┃ +// ╰╯┃┃┣┻━┳┫┃╭┳━━╮┃┃╱╱╭━━┫╰━┳━━╮ +// ╱╱┃┃┃╭╮┣┫╰╯┫╭╮┃┃┃╱╭┫╭╮┃╭╮┃━━┫ +// ╱╱┃┃┃╭╮┃┃╭╮┫╰╯┃┃╰━╯┃╭╮┃╰╯┣━━┃ +// ╱╱╰╯╰╯╰┻┻╯╰┻━━╯╰━━━┻╯╰┻━━┻━━╯ +pragma solidity ^0.8.9; + +import {IProofVerifier} from "../../L1/ProofVerifier.sol"; +import "../../L1/TaikoL1.sol"; + +contract TestTaikoL1EnableTokenomics is TaikoL1, IProofVerifier { + function getConfig() + public + pure + override + returns (TaikoData.Config memory config) + { + config.chainId = 167; + // up to 2048 pending blocks + config.maxNumBlocks = 2048; + config.blockHashHistory = 3; + // 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.commitConfirmations = 1; + config.maxProofsPerForkChoice = 5; + config.blockMaxGasLimit = 30000000; // TODO + config.maxTransactionsPerBlock = 20; // TODO + config.maxBytesPerTxList = 10240; // TODO + config.minTxGasLimit = 21000; // TODO + config.anchorTxGasLimit = 250000; + config.feePremiumLamda = 590; + config.rewardBurnBips = 100; // 100 basis points or 1% + config.proposerDepositPctg = 25; // 25% + + // Moving average factors + config.feeBaseMAF = 1024; + config.blockTimeMAF = 64; + config.proofTimeMAF = 64; + + config.rewardMultiplierPctg = 400; // 400% + config.feeGracePeriodPctg = 125; // 125% + config.feeMaxPeriodPctg = 375; // 375% + config.blockTimeCap = 48 seconds; + config.proofTimeCap = 5 seconds; + config.bootstrapDiscountHalvingPeriod = 1 seconds; + config.initialUncleDelay = 1 seconds; + config.enableTokenomics = true; + config.enablePublicInputsCheck = false; + } + + function verifyZKP( + bytes memory /*verificationKey*/, + bytes calldata /*zkproof*/, + bytes32 /*blockHash*/, + address /*prover*/, + bytes32 /*txListHash*/ + ) public pure override returns (bool) { + return true; + } + + function verifyMKP( + bytes memory /*key*/, + bytes memory /*value*/, + bytes memory /*proof*/, + bytes32 /*root*/ + ) public pure override returns (bool) { + return true; + } +} diff --git a/packages/protocol/contracts/test/L1/TestTaikoL2.sol b/packages/protocol/contracts/test/L1/TestTaikoL2.sol new file mode 100644 index 0000000000..16037e5d6e --- /dev/null +++ b/packages/protocol/contracts/test/L1/TestTaikoL2.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT +// +// ╭━━━━╮╱╱╭╮╱╱╱╱╱╭╮╱╱╱╱╱╭╮ +// ┃╭╮╭╮┃╱╱┃┃╱╱╱╱╱┃┃╱╱╱╱╱┃┃ +// ╰╯┃┃┣┻━┳┫┃╭┳━━╮┃┃╱╱╭━━┫╰━┳━━╮ +// ╱╱┃┃┃╭╮┣┫╰╯┫╭╮┃┃┃╱╭┫╭╮┃╭╮┃━━┫ +// ╱╱┃┃┃╭╮┃┃╭╮┫╰╯┃┃╰━╯┃╭╮┃╰╯┣━━┃ +// ╱╱╰╯╰╯╰┻┻╯╰┻━━╯╰━━━┻╯╰┻━━┻━━╯ +pragma solidity ^0.8.9; + +import "../../L2/TaikoL2.sol"; + +contract TestTaikoL2 is TaikoL2 { + constructor(address _addressManager) TaikoL2(_addressManager) {} + + function getConfig() + public + pure + override + returns (TaikoData.Config memory config) + { + config.chainId = 167; + // up to 2048 pending blocks + config.maxNumBlocks = 4; + config.blockHashHistory = 3; + // 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.commitConfirmations = 1; + config.maxProofsPerForkChoice = 5; + config.blockMaxGasLimit = 30000000; // TODO + config.maxTransactionsPerBlock = 20; // TODO + config.maxBytesPerTxList = 10240; // TODO + config.minTxGasLimit = 21000; // TODO + config.anchorTxGasLimit = 250000; + config.feePremiumLamda = 590; + config.rewardBurnBips = 100; // 100 basis points or 1% + config.proposerDepositPctg = 25; // 25% + + // Moving average factors + config.feeBaseMAF = 1024; + config.blockTimeMAF = 64; + config.proofTimeMAF = 64; + + config.rewardMultiplierPctg = 400; // 400% + config.feeGracePeriodPctg = 125; // 125% + config.feeMaxPeriodPctg = 375; // 375% + config.blockTimeCap = 48 seconds; + config.proofTimeCap = 60 minutes; + config.bootstrapDiscountHalvingPeriod = 180 days; + config.initialUncleDelay = 1 minutes; + config.enableTokenomics = true; + config.enablePublicInputsCheck = false; + } +} diff --git a/packages/protocol/contracts/test/L1/TestTaikoL2EnablePublicInputsCheck.sol b/packages/protocol/contracts/test/L1/TestTaikoL2EnablePublicInputsCheck.sol new file mode 100644 index 0000000000..8b1ebf7e45 --- /dev/null +++ b/packages/protocol/contracts/test/L1/TestTaikoL2EnablePublicInputsCheck.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT +// +// ╭━━━━╮╱╱╭╮╱╱╱╱╱╭╮╱╱╱╱╱╭╮ +// ┃╭╮╭╮┃╱╱┃┃╱╱╱╱╱┃┃╱╱╱╱╱┃┃ +// ╰╯┃┃┣┻━┳┫┃╭┳━━╮┃┃╱╱╭━━┫╰━┳━━╮ +// ╱╱┃┃┃╭╮┣┫╰╯┫╭╮┃┃┃╱╭┫╭╮┃╭╮┃━━┫ +// ╱╱┃┃┃╭╮┃┃╭╮┫╰╯┃┃╰━╯┃╭╮┃╰╯┣━━┃ +// ╱╱╰╯╰╯╰┻┻╯╰┻━━╯╰━━━┻╯╰┻━━┻━━╯ +pragma solidity ^0.8.9; + +import "../../L2/TaikoL2.sol"; + +contract TestTaikoL2EnablePublicInputsCheck is TaikoL2 { + constructor(address _addressManager) TaikoL2(_addressManager) {} + + function getConfig() + public + pure + override + returns (TaikoData.Config memory config) + { + config.chainId = 167; + // up to 2048 pending blocks + config.maxNumBlocks = 4; + config.blockHashHistory = 3; + // 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.commitConfirmations = 1; + config.maxProofsPerForkChoice = 5; + config.blockMaxGasLimit = 30000000; // TODO + config.maxTransactionsPerBlock = 20; // TODO + config.maxBytesPerTxList = 10240; // TODO + config.minTxGasLimit = 21000; // TODO + config.anchorTxGasLimit = 250000; + config.feePremiumLamda = 590; + config.rewardBurnBips = 100; // 100 basis points or 1% + config.proposerDepositPctg = 25; // 25% + + // Moving average factors + config.feeBaseMAF = 1024; + config.blockTimeMAF = 64; + config.proofTimeMAF = 64; + + config.rewardMultiplierPctg = 400; // 400% + config.feeGracePeriodPctg = 125; // 125% + config.feeMaxPeriodPctg = 375; // 375% + config.blockTimeCap = 48 seconds; + config.proofTimeCap = 60 minutes; + config.bootstrapDiscountHalvingPeriod = 180 days; + config.initialUncleDelay = 1 minutes; + config.enableTokenomics = true; + config.enablePublicInputsCheck = true; + } +} diff --git a/packages/protocol/contracts/test/thirdparty/TestTKOToken.sol b/packages/protocol/contracts/test/thirdparty/TestTKOToken.sol new file mode 100644 index 0000000000..c7329c20cf --- /dev/null +++ b/packages/protocol/contracts/test/thirdparty/TestTKOToken.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; +import "../../L1/TkoToken.sol"; + +contract TestTkoToken is TkoToken { + function mintAnyone(address account, uint256 amount) public { + _mint(account, amount); + } +} diff --git a/packages/protocol/package.json b/packages/protocol/package.json index 8cf7dfcd67..34776163c6 100644 --- a/packages/protocol/package.json +++ b/packages/protocol/package.json @@ -15,7 +15,8 @@ "test:coverage": "pnpm coverage", "generate:genesis": "ts-node ./utils/generate_genesis/main.ts", "test:genesis": "./test/genesis/generate_genesis.test.sh", - "test:integration": "./test/test_integration.sh", + "test:integration": "TEST_TYPE=integration ./test/test_integration.sh", + "test:tokenomics": "TEST_TYPE=tokenomics ./test/test_integration.sh", "deploy:hardhat": "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 --confirmations 1", "lint-staged": "lint-staged --allow-empty" }, diff --git a/packages/protocol/tasks/utils.ts b/packages/protocol/tasks/utils.ts index fc50bf80bc..e8a82b35c9 100644 --- a/packages/protocol/tasks/utils.ts +++ b/packages/protocol/tasks/utils.ts @@ -1,7 +1,5 @@ import * as fs from "fs"; import * as log from "./log"; -import { Block, BlockHeader, EthGetProofResponse } from "../test/utils/rpc"; -import RLP from "rlp"; async function deployContract( hre: any, @@ -68,94 +66,10 @@ function getDeployments(_fileName: string) { return JSON.parse(`${json}`); } -async function getMessageStatusSlot(hre: any, signal: any) { - return hre.ethers.utils.solidityKeccak256( - ["string", "bytes"], - ["MESSAGE_STATUS", signal] - ); -} - async function decode(hre: any, type: any, data: any) { return hre.ethers.utils.defaultAbiCoder.decode([type], data).toString(); } -function getSignalSlot(hre: any, sender: any, signal: any) { - return hre.ethers.utils.keccak256( - hre.ethers.utils.solidityPack( - ["string", "address", "bytes32"], - ["SIGNAL", sender, signal] - ) - ); -} - -const MessageStatus = { - NEW: 0, - RETRIABLE: 1, - DONE: 2, - FAILED: 3, -}; - -async function getLatestBlockHeader(hre: any) { - const block: Block = await hre.ethers.provider.send( - "eth_getBlockByNumber", - ["latest", false] - ); - - const logsBloom = block.logsBloom.toString().substring(2); - - const blockHeader: BlockHeader = { - parentHash: block.parentHash, - ommersHash: block.sha3Uncles, - beneficiary: block.miner, - stateRoot: block.stateRoot, - transactionsRoot: block.transactionsRoot, - receiptsRoot: block.receiptsRoot, - logsBloom: logsBloom.match(/.{1,64}/g)!.map((s: string) => "0x" + s), - difficulty: block.difficulty, - height: block.number, - gasLimit: block.gasLimit, - gasUsed: block.gasUsed, - timestamp: block.timestamp, - extraData: block.extraData, - mixHash: block.mixHash, - nonce: block.nonce, - baseFeePerGas: block.baseFeePerGas ? parseInt(block.baseFeePerGas) : 0, - }; - - return { block, blockHeader }; -} - -async function getSignalProof( - hre: any, - contractAddress: string, - key: string, - blockNumber: number, - blockHeader: BlockHeader -) { - const proof: EthGetProofResponse = await hre.ethers.provider.send( - "eth_getProof", - [contractAddress, [key], blockNumber] - ); - - // RLP encode the proof together for LibTrieProof to decode - const encodedProof = hre.ethers.utils.defaultAbiCoder.encode( - ["bytes", "bytes"], - [ - RLP.encode(proof.accountProof), - RLP.encode(proof.storageProof[0].proof), - ] - ); - // encode the SignalProof struct from LibBridgeSignal - const signalProof = hre.ethers.utils.defaultAbiCoder.encode( - [ - "tuple(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, bytes proof)", - ], - [{ header: blockHeader, proof: encodedProof }] - ); - - return signalProof; -} - export { deployContract, getDeployer, @@ -163,10 +77,5 @@ export { getContract, saveDeployments, getDeployments, - getMessageStatusSlot, - getSignalSlot, decode, - MessageStatus, - getLatestBlockHeader, - getSignalProof, }; diff --git a/packages/protocol/test/L1/TaikoL1.integration.test.ts b/packages/protocol/test/L1/TaikoL1.integration.test.ts new file mode 100644 index 0000000000..38dc69c855 --- /dev/null +++ b/packages/protocol/test/L1/TaikoL1.integration.test.ts @@ -0,0 +1,252 @@ +import { expect } from "chai"; +import { BigNumber, ethers as ethersLib } from "ethers"; +import { ethers } from "hardhat"; +import { TaikoL1, TaikoL2 } from "../../typechain"; +import deployAddressManager from "../utils/addressManager"; +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"; + +describe("integration:TaikoL1", function () { + let taikoL1: TaikoL1; + let taikoL2: TaikoL2; + let l2Provider: ethersLib.providers.JsonRpcProvider; + let l2Signer: ethersLib.Signer; + + beforeEach(async function () { + l2Provider = new ethers.providers.JsonRpcProvider( + "http://localhost:28545" + ); + + 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(); + + await l1AddressManager.setAddress( + `${l2ChainId}.taiko`, + taikoL2.address + ); + }); + + describe("isCommitValid()", async function () { + it("should not be valid if it has not been committed", async function () { + const block = await l2Provider.getBlock("latest"); + const commit = generateCommitHash(block); + + const isCommitValid = await taikoL1.isCommitValid( + 1, + 1, + commit.hash + ); + + expect(isCommitValid).to.be.eq(false); + }); + }); + + describe("getProposedBlock()", function () { + it("should revert if block is out of range and not a valid proposed block", async function () { + await expect(taikoL1.getProposedBlock(123)).to.be.revertedWith( + "L1:id" + ); + }); + }); + describe("commitBlock() -> proposeBlock() integration", async function () { + it("should fail if a proposed block's placeholder field values are not default", async function () { + const block = await l2Provider.getBlock("latest"); + const commitSlot = 0; + const { tx, commit } = await commitBlock( + taikoL1, + block, + commitSlot + ); + + const receipt = await tx.wait(1); + + const meta: BlockMetadata = { + id: 1, + l1Height: 0, + l1Hash: ethers.constants.HashZero, + beneficiary: block.miner, + txListHash: commit.txListHash, + mixHash: ethers.constants.HashZero, + extraData: block.extraData, + gasLimit: block.gasLimit, + timestamp: 0, + commitSlot: commitSlot, + commitHeight: receipt.blockNumber as number, + }; + + const inputs = buildProposeBlockInputs(block, meta); + + await expect(taikoL1.proposeBlock(inputs)).to.be.revertedWith( + "L1:placeholder" + ); + }); + + it("should revert with invalid gasLimit", async function () { + const block = await l2Provider.getBlock("latest"); + const config = await taikoL1.getConfig(); + const gasLimit = config.blockMaxGasLimit; + + const { tx, commit } = await commitBlock(taikoL1, block); + + const receipt = await tx.wait(1); + const meta: BlockMetadata = { + id: 0, + l1Height: 0, + l1Hash: ethers.constants.HashZero, + beneficiary: block.miner, + txListHash: commit.txListHash, + mixHash: ethers.constants.HashZero, + extraData: block.extraData, + gasLimit: gasLimit.add(1), + timestamp: 0, + commitSlot: 0, + commitHeight: receipt.blockNumber as number, + }; + + const inputs = buildProposeBlockInputs(block, meta); + + await expect(taikoL1.proposeBlock(inputs)).to.be.revertedWith( + "L1:gasLimit" + ); + }); + + it("should revert with invalid extraData", async function () { + const block = await l2Provider.getBlock("latest"); + const { tx, commit } = await commitBlock(taikoL1, block); + + const meta: BlockMetadata = { + id: 0, + l1Height: 0, + l1Hash: ethers.constants.HashZero, + beneficiary: block.miner, + txListHash: commit.txListHash, + mixHash: ethers.constants.HashZero, + extraData: ethers.utils.hexlify(ethers.utils.randomBytes(33)), // invalid extradata + gasLimit: block.gasLimit, + timestamp: 0, + commitSlot: 0, + commitHeight: tx.blockNumber as number, + }; + + const inputs = buildProposeBlockInputs(block, meta); + + await expect(taikoL1.proposeBlock(inputs)).to.be.revertedWith( + "L1:extraData" + ); + }); + + 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); + + const stateVariables = await taikoL1.getStateVariables(); + const nextBlockId = stateVariables[4]; + const proposedBlock = await taikoL1.getProposedBlock( + nextBlockId.sub(1) + ); + + expect(proposedBlock.metaHash).not.to.be.eq( + ethers.constants.HashZero + ); + expect(proposedBlock.proposer).not.to.be.eq( + ethers.constants.AddressZero + ); + expect(proposedBlock.proposedAt).not.to.be.eq(BigNumber.from(0)); + }); + + 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( + taikoL1, + block, + commit.txListHash, + tx.blockNumber as number, + block.gasLimit, + i + ); + + expect(receipt.status).to.be.eq(1); + + const stateVariables = await taikoL1.getStateVariables(); + const nextBlockId = stateVariables[4]; + const proposedBlock = await taikoL1.getProposedBlock( + nextBlockId.sub(1) + ); + + expect(proposedBlock.metaHash).not.to.be.eq( + ethers.constants.HashZero + ); + expect(proposedBlock.proposer).not.to.be.eq( + ethers.constants.AddressZero + ); + expect(proposedBlock.proposedAt).not.to.be.eq( + BigNumber.from(0) + ); + } + + // 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( + proposeBlock( + taikoL1, + block, + commit.txListHash, + tx.blockNumber as number, + block.gasLimit + ) + ).to.be.revertedWith("L1:tooMany"); + }); + }); +}); diff --git a/packages/protocol/test/L1/TaikoL1.test.ts b/packages/protocol/test/L1/TaikoL1.test.ts index 5a1d23f7fe..96d70461eb 100644 --- a/packages/protocol/test/L1/TaikoL1.test.ts +++ b/packages/protocol/test/L1/TaikoL1.test.ts @@ -1,56 +1,19 @@ import { expect } from "chai"; -import { BigNumber, ContractTransaction, ethers as ethersLib } from "ethers"; import { ethers } from "hardhat"; -import RLP from "rlp"; -import { TaikoL1, TaikoL2 } from "../../typechain"; +import { TaikoL1 } from "../../typechain"; +import deployAddressManager from "../utils/addressManager"; +import { randomBytes32 } from "../utils/bytes"; +import { deployTaikoL1 } from "../utils/taikoL1"; describe("TaikoL1", function () { let taikoL1: TaikoL1; let genesisHash: string; beforeEach(async function () { - const addressManager = await ( - await ethers.getContractFactory("AddressManager") - ).deploy(); - await addressManager.init(); - - const libReceiptDecoder = await ( - await ethers.getContractFactory("LibReceiptDecoder") - ).deploy(); - - const libTxDecoder = await ( - await ethers.getContractFactory("LibTxDecoder") - ).deploy(); - - const libProposing = await ( - await ethers.getContractFactory("LibProposing") - ).deploy(); - - const libProving = await ( - await ethers.getContractFactory("LibProving", { - libraries: { - LibReceiptDecoder: libReceiptDecoder.address, - LibTxDecoder: libTxDecoder.address, - }, - }) - ).deploy(); - - const libVerifying = await ( - await ethers.getContractFactory("LibVerifying") - ).deploy(); - + const l1Signer = (await ethers.getSigners())[0]; + const addressManager = await deployAddressManager(l1Signer); genesisHash = randomBytes32(); - const feeBase = BigNumber.from(10).pow(18); - taikoL1 = await ( - await ethers.getContractFactory("TestTaikoL1", { - libraries: { - LibVerifying: libVerifying.address, - LibProposing: libProposing.address, - LibProving: libProving.address, - }, - }) - ).deploy(); - await taikoL1.init(addressManager.address, genesisHash, feeBase); + taikoL1 = await deployTaikoL1(addressManager, genesisHash, false); }); describe("getLatestSyncedHeader()", async function () { @@ -156,330 +119,3 @@ describe("TaikoL1", function () { }); }); }); - -describe("integration:TaikoL1", function () { - let taikoL1: TaikoL1; - let taikoL2: TaikoL2; - let l2Provider: ethersLib.providers.JsonRpcProvider; - let l2Signer: ethersLib.Signer; - - beforeEach(async function () { - l2Provider = new ethers.providers.JsonRpcProvider( - "http://localhost:28545" - ); - - l2Signer = await l2Provider.getSigner( - ( - await l2Provider.listAccounts() - )[0] - ); - - const addressManager = await ( - await ethers.getContractFactory("AddressManager") - ).deploy(); - await addressManager.init(); - - const libReceiptDecoder = await ( - await ethers.getContractFactory("LibReceiptDecoder") - ).deploy(); - - const libTxDecoder = await ( - await ethers.getContractFactory("LibTxDecoder") - ).deploy(); - - const libProposing = await ( - await ethers.getContractFactory("LibProposing") - ).deploy(); - - const libProving = await ( - await ethers.getContractFactory("LibProving", { - libraries: { - LibReceiptDecoder: libReceiptDecoder.address, - LibTxDecoder: libTxDecoder.address, - }, - }) - ).deploy(); - - const libVerifying = await ( - await ethers.getContractFactory("LibVerifying") - ).deploy(); - - const l2AddressManager = await ( - await ethers.getContractFactory("AddressManager") - ) - .connect(l2Signer) - .deploy(); - await l2AddressManager.init(); - - // Deploying TaikoL2 Contract linked with LibTxDecoder (throws error otherwise) - const l2LibTxDecoder = await ( - await ethers.getContractFactory("LibTxDecoder") - ) - .connect(l2Signer) - .deploy(); - - taikoL2 = await ( - await ethers.getContractFactory("TaikoL2", { - libraries: { - LibTxDecoder: l2LibTxDecoder.address, - }, - }) - ) - .connect(l2Signer) - .deploy(l2AddressManager.address); - - const genesisHash = taikoL2.deployTransaction.blockHash; - - taikoL1 = await ( - await ethers.getContractFactory("TestTaikoL1", { - libraries: { - LibVerifying: libVerifying.address, - LibProposing: libProposing.address, - LibProving: libProving.address, - }, - }) - ).deploy(); - - const feeBase = BigNumber.from(10).pow(18); - - await taikoL1.init( - addressManager.address, - genesisHash as string, - feeBase - ); - }); - - describe("isCommitValid()", async function () { - it("should not be valid", async function () { - const block = await l2Provider.getBlock("latest"); - const txListHash = ethers.utils.keccak256( - RLP.encode(block.transactions) - ); - const hash = ethers.utils.keccak256( - ethers.utils.solidityPack( - ["address", "bytes32"], - [block.miner, txListHash] - ) - ); - - const isCommitValid = await taikoL1.isCommitValid(1, 1, hash); - - expect(isCommitValid).to.be.eq(false); - }); - }); - - describe("getProposedBlock()", function () { - it("reverts when id is not a valid proposed block in range", async function () { - await expect(taikoL1.getProposedBlock(123)).to.be.revertedWith( - "L1:id" - ); - }); - }); - describe("commitBlock() -> proposeBlock() integration", async function () { - it("should revert with invalid meta", async function () { - const block = await l2Provider.getBlock("latest"); - const txListHash = ethers.utils.keccak256( - RLP.encode(block.transactions) - ); - const hash = ethers.utils.keccak256( - ethers.utils.solidityPack( - ["address", "bytes32"], - [block.miner, txListHash] - ) - ); - let tx: ContractTransaction; - expect((tx = await taikoL1.commitBlock(1, hash))).to.emit( - taikoL1, - "BlockCommitted" - ); - - // blockMetadata is inputs[0], txListBytes = inputs[1] - const inputs = []; - const meta = { - id: 1, // invalid because id should be 0 - l1Height: 0, - l1Hash: ethers.constants.HashZero, - beneficiary: block.miner, - txListHash: txListHash, - mixHash: ethers.constants.HashZero, - extraData: block.extraData, - gasLimit: block.gasLimit, - timestamp: 0, - commitSlot: 1, - commitHeight: tx.blockNumber, - }; - - const blockMetadataBytes = encodeBlockMetadata(meta); - - inputs[0] = blockMetadataBytes; - inputs[1] = RLP.encode(block.transactions); - - await expect(taikoL1.proposeBlock(inputs)).to.be.revertedWith( - "L1:placeholder" - ); - }); - - it("should revert with invalid gasLimit", async function () { - const block = await l2Provider.getBlock("latest"); - const txListHash = ethers.utils.keccak256( - RLP.encode(block.transactions) - ); - const hash = ethers.utils.keccak256( - ethers.utils.solidityPack( - ["address", "bytes32"], - [block.miner, txListHash] - ) - ); - let tx: ContractTransaction; - expect((tx = await taikoL1.commitBlock(1, hash))).to.emit( - taikoL1, - "BlockCommitted" - ); - - // blockMetadata is inputs[0], txListBytes = inputs[1] - const config = await taikoL1.getConfig(); - const gasLimit = config[7]; - const inputs = []; - const meta = { - id: 0, - l1Height: 0, - l1Hash: ethers.constants.HashZero, - beneficiary: block.miner, - txListHash: txListHash, - mixHash: ethers.constants.HashZero, - extraData: block.extraData, - gasLimit: gasLimit.add(1), - timestamp: 0, - commitSlot: 1, - commitHeight: tx.blockNumber, - }; - - const blockMetadataBytes = encodeBlockMetadata(meta); - - inputs[0] = blockMetadataBytes; - inputs[1] = RLP.encode(block.transactions); - - await expect(taikoL1.proposeBlock(inputs)).to.be.revertedWith( - "L1:gasLimit" - ); - }); - - it("should revert with invalid extraData", async function () { - const block = await l2Provider.getBlock("latest"); - const txListHash = ethers.utils.keccak256( - RLP.encode(block.transactions) - ); - const hash = ethers.utils.keccak256( - ethers.utils.solidityPack( - ["address", "bytes32"], - [block.miner, txListHash] - ) - ); - let tx: ContractTransaction; - expect((tx = await taikoL1.commitBlock(1, hash))).to.emit( - taikoL1, - "BlockCommitted" - ); - - // blockMetadata is inputs[0], txListBytes = inputs[1] - const inputs = []; - const meta = { - id: 0, - l1Height: 0, - l1Hash: ethers.constants.HashZero, - beneficiary: block.miner, - txListHash: txListHash, - mixHash: ethers.constants.HashZero, - extraData: ethers.utils.hexlify(ethers.utils.randomBytes(33)), // invalid extradata - gasLimit: block.gasLimit, - timestamp: 0, - commitSlot: 1, - commitHeight: tx.blockNumber, - }; - - const blockMetadataBytes = encodeBlockMetadata(meta); - - inputs[0] = blockMetadataBytes; - inputs[1] = RLP.encode(block.transactions); - - await expect(taikoL1.proposeBlock(inputs)).to.be.revertedWith( - "L1:extraData" - ); - }); - - it("should commit and be able to propose", async function () { - const block = await l2Provider.getBlock("latest"); - const txListHash = ethers.utils.keccak256( - RLP.encode(block.transactions) - ); - const hash = ethers.utils.keccak256( - ethers.utils.solidityPack( - ["address", "bytes32"], - [block.miner, txListHash] - ) - ); - let tx: ContractTransaction; - expect((tx = await taikoL1.commitBlock(1, hash))).to.emit( - taikoL1, - "BlockCommitted" - ); - - // blockMetadata is inputs[0], txListBytes = inputs[1] - const inputs = []; - const meta = { - id: 0, - l1Height: 0, - l1Hash: ethers.constants.HashZero, - beneficiary: block.miner, - txListHash: txListHash, - mixHash: ethers.constants.HashZero, - extraData: block.extraData, - gasLimit: block.gasLimit, - timestamp: 0, - commitSlot: 1, - commitHeight: tx.blockNumber, - }; - - const blockMetadataBytes = encodeBlockMetadata(meta); - - inputs[0] = blockMetadataBytes; - inputs[1] = RLP.encode(block.transactions); - - expect(await taikoL1.proposeBlock(inputs)).to.emit( - taikoL1, - "BlockProposed" - ); - - const stateVariables = await taikoL1.getStateVariables(); - const nextBlockId = stateVariables[4]; - const proposedBlock = await taikoL1.getProposedBlock( - nextBlockId.sub(1) - ); - - expect(proposedBlock[0]).not.to.be.eq(ethers.constants.HashZero); - expect(proposedBlock[2]).not.to.be.eq(ethers.constants.AddressZero); - expect(proposedBlock[3]).not.to.be.eq(BigNumber.from(0)); - - const isCommitValid = await taikoL1.isCommitValid( - 1, - tx.blockNumber as number, - hash - ); - - expect(isCommitValid).to.be.eq(true); - }); - }); -}); - -function randomBytes32() { - return ethers.utils.hexlify(ethers.utils.randomBytes(32)); -} - -function encodeBlockMetadata(meta: unknown) { - return ethers.utils.defaultAbiCoder.encode( - [ - "tuple(uint256 id, uint256 l1Height, bytes32 l1Hash, address beneficiary, bytes32 txListHash, bytes32 mixHash, bytes extraData, uint64 gasLimit, uint64 timestamp, uint64 commitHeight, uint64 commitSlot)", - ], - [meta] - ); -} diff --git a/packages/protocol/test/L1/TkoToken.test.ts b/packages/protocol/test/L1/TkoToken.test.ts index 34afa79845..fa25e217ab 100644 --- a/packages/protocol/test/L1/TkoToken.test.ts +++ b/packages/protocol/test/L1/TkoToken.test.ts @@ -1,5 +1,4 @@ import { expect } from "chai"; -import { AddressManager, TkoToken } from "../../typechain"; import { ethers } from "hardhat"; import { ADDRESS_RESOLVER_DENIED, @@ -7,12 +6,15 @@ import { ERC20_TRANSFER_AMOUNT_EXCEEDED, } from "../constants/errors"; import { BigNumber } from "ethers"; +import deployTkoToken from "../utils/tkoToken"; +import { TestTkoToken } from "../../typechain/TestTkoToken"; +import deployAddressManager from "../utils/addressManager"; -describe("TokenVault", function () { +describe("TkoToken", function () { let owner: any; let nonOwner: any; let protoBroker: any; - let token: TkoToken; + let token: TestTkoToken; let amountMinted: BigNumber; before(async function () { @@ -20,23 +22,12 @@ describe("TokenVault", function () { }); beforeEach(async function () { - const addressManager: AddressManager = await ( - await ethers.getContractFactory("AddressManager") - ).deploy(); - await addressManager.init(); - - token = await (await ethers.getContractFactory("TkoToken")) - .connect(owner) - .deploy(); - await token.init(addressManager.address); - - const { chainId } = await ethers.provider.getNetwork(); - - await addressManager.setAddress( - `${chainId}.proto_broker`, + const addressManager = await deployAddressManager(owner); + token = await deployTkoToken( + owner, + addressManager, protoBroker.address ); - amountMinted = ethers.utils.parseEther("100"); await token.connect(protoBroker).mint(owner.address, amountMinted); diff --git a/packages/protocol/test/L2/TaikoL2.test.ts b/packages/protocol/test/L2/TaikoL2.test.ts index ff9f550288..0c4f2c7e3f 100644 --- a/packages/protocol/test/L2/TaikoL2.test.ts +++ b/packages/protocol/test/L2/TaikoL2.test.ts @@ -1,28 +1,17 @@ import { expect } from "chai"; import { ethers } from "hardhat"; import { TaikoL2 } from "../../typechain"; +import deployAddressManager from "../utils/addressManager"; +import { randomBytes32 } from "../utils/bytes"; +import { deployTaikoL2 } from "../utils/taikoL2"; describe("TaikoL2", function () { let taikoL2: TaikoL2; beforeEach(async function () { - const addressManager = await ( - await ethers.getContractFactory("AddressManager") - ).deploy(); - await addressManager.init(); - - // Deploying TaikoL2 Contract linked with LibTxDecoder (throws error otherwise) - const libTxDecoder = await ( - await ethers.getContractFactory("LibTxDecoder") - ).deploy(); - - taikoL2 = await ( - await ethers.getContractFactory("TaikoL2", { - libraries: { - LibTxDecoder: libTxDecoder.address, - }, - }) - ).deploy(addressManager.address); + const signer = (await ethers.getSigners())[0]; + const addressManager = await deployAddressManager(signer); + taikoL2 = await deployTaikoL2(signer, addressManager); }); describe("anchor()", async function () { @@ -47,7 +36,3 @@ describe("TaikoL2", function () { }); }); }); - -function randomBytes32() { - return ethers.utils.hexlify(ethers.utils.randomBytes(32)); -} diff --git a/packages/protocol/test/bridge/Bridge.integration.test.ts b/packages/protocol/test/bridge/Bridge.integration.test.ts new file mode 100644 index 0000000000..983f717bcd --- /dev/null +++ b/packages/protocol/test/bridge/Bridge.integration.test.ts @@ -0,0 +1,503 @@ +import { expect } from "chai"; +import { ethers as ethersLib } from "ethers"; +import hre, { ethers } from "hardhat"; +import { + AddressManager, + Bridge, + TestHeaderSync, + TestLibBridgeData, +} from "../../typechain"; +import deployAddressManager from "../utils/addressManager"; +import { + deployBridge, + processMessage, + sendAndProcessMessage, + sendMessage, +} from "../utils/bridge"; +import { randomBytes32 } from "../utils/bytes"; +import { Message } from "../utils/message"; +import { getDefaultL2Signer, getL2Provider } from "../utils/provider"; +import { Block, getBlockHeader } from "../utils/rpc"; +import { getSignalProof, getSignalSlot } from "../utils/signal"; + +describe("integration:Bridge", function () { + let owner: any; + let l2Provider: ethersLib.providers.JsonRpcProvider; + let l2Signer: ethersLib.Signer; + let srcChainId: number; + let enabledDestChainId: number; + let l2NonOwner: ethersLib.Signer; + let l1Bridge: Bridge; + let l2Bridge: Bridge; + let m: Message; + let headerSync: TestHeaderSync; + + beforeEach(async () => { + [owner] = await ethers.getSigners(); + + const { chainId } = await ethers.provider.getNetwork(); + + srcChainId = chainId; + + // seondary node to deploy L2 on + l2Provider = getL2Provider(); + + l2Signer = await getDefaultL2Signer(); + + l2NonOwner = await l2Provider.getSigner(); + + const l2Network = await l2Provider.getNetwork(); + + enabledDestChainId = l2Network.chainId; + + const addressManager: AddressManager = await deployAddressManager( + owner + ); + + const l2AddressManager: AddressManager = await deployAddressManager( + l2Signer + ); + + ({ bridge: l1Bridge } = await deployBridge( + owner, + addressManager, + enabledDestChainId, + srcChainId + )); + + ({ bridge: l2Bridge } = await deployBridge( + l2Signer, + l2AddressManager, + srcChainId, + enabledDestChainId + )); + + await addressManager.setAddress( + `${enabledDestChainId}.bridge`, + l2Bridge.address + ); + + await l2AddressManager + .connect(l2Signer) + .setAddress(`${srcChainId}.bridge`, l1Bridge.address); + + headerSync = await (await ethers.getContractFactory("TestHeaderSync")) + .connect(l2Signer) + .deploy(); + + await l2AddressManager + .connect(l2Signer) + .setAddress(`${enabledDestChainId}.taiko`, headerSync.address); + + m = { + id: 1, + sender: owner.address, + srcChainId: srcChainId, + destChainId: enabledDestChainId, + owner: owner.address, + to: owner.address, + refundAddress: owner.address, + depositValue: 1000, + callValue: 1000, + processingFee: 1000, + gasLimit: 10000, + data: ethers.constants.HashZero, + memo: "", + }; + }); + + describe("processMessage()", function () { + it("should throw if message.gasLimit == 0 & msg.sender is not message.owner", async function () { + const m: Message = { + id: 1, + sender: await l2NonOwner.getAddress(), + srcChainId: srcChainId, + destChainId: enabledDestChainId, + owner: owner.address, + to: owner.address, + refundAddress: owner.address, + depositValue: 1000, + callValue: 1000, + processingFee: 1000, + gasLimit: 0, + data: ethers.constants.HashZero, + memo: "", + }; + + await expect( + l2Bridge.processMessage(m, ethers.constants.HashZero) + ).to.be.revertedWith("B:forbidden"); + }); + + it("should throw if message.destChainId is not equal to current block.chainId", async function () { + const m: Message = { + id: 1, + sender: owner.address, + srcChainId: srcChainId, + destChainId: enabledDestChainId + 1, + owner: owner.address, + to: owner.address, + refundAddress: owner.address, + depositValue: 1000, + callValue: 1000, + processingFee: 1000, + gasLimit: 10000, + data: ethers.constants.HashZero, + memo: "", + }; + + await expect( + l2Bridge.processMessage(m, ethers.constants.HashZero) + ).to.be.revertedWith("B:destChainId"); + }); + + it("should throw if messageStatus of message is != NEW", async function () { + const { message, signalProof } = await sendAndProcessMessage( + hre.ethers.provider, + headerSync, + m, + l1Bridge, + l2Bridge + ); + + // recalling this process should be prevented as it's status is no longer NEW + await expect( + l2Bridge.processMessage(message, signalProof) + ).to.be.revertedWith("B:status"); + }); + + it("should throw if message signalproof is not valid", async function () { + const libData: TestLibBridgeData = await ( + await ethers.getContractFactory("TestLibBridgeData") + ).deploy(); + + const signal = await libData.hashMessage(m); + + const sender = l1Bridge.address; + + const key = getSignalSlot(sender, signal); + const { block, blockHeader } = await getBlockHeader( + hre.ethers.provider + ); + + await headerSync.setSyncedHeader(ethers.constants.HashZero); + + const signalProof = await getSignalProof( + hre.ethers.provider, + l1Bridge.address, + key, + block.number, + blockHeader + ); + + await expect( + l2Bridge.processMessage(m, signalProof) + ).to.be.revertedWith("LTP:invalid storage proof"); + }); + + it("should throw if message has not been received", async function () { + const { signal, message } = await sendMessage(l1Bridge, m); + + expect(signal).not.to.be.eq(ethers.constants.HashZero); + + const messageStatus = await l1Bridge.getMessageStatus(signal); + + expect(messageStatus).to.be.eq(0); + + const sender = l1Bridge.address; + + const key = getSignalSlot(sender, signal); + + const { block, blockHeader } = await getBlockHeader( + hre.ethers.provider + ); + + await headerSync.setSyncedHeader(ethers.constants.HashZero); + + // get storageValue for the key + const storageValue = await ethers.provider.getStorageAt( + l1Bridge.address, + key, + block.number + ); + // make sure it equals 1 so our proof will pass + expect(storageValue).to.be.eq( + "0x0000000000000000000000000000000000000000000000000000000000000001" + ); + + const signalProof = await getSignalProof( + hre.ethers.provider, + l1Bridge.address, + key, + block.number, + blockHeader + ); + + await expect( + l2Bridge.processMessage(message, signalProof) + ).to.be.revertedWith("B:notReceived"); + }); + + it("processes a message when the signal has been verified from the sending chain", async () => { + const { signal, message } = await sendMessage(l1Bridge, m); + + expect(signal).not.to.be.eq(ethers.constants.HashZero); + + const messageStatus = await l1Bridge.getMessageStatus(signal); + + expect(messageStatus).to.be.eq(0); + let block: Block; + expect( + ({ block } = await processMessage( + l1Bridge, + l2Bridge, + signal, + hre.ethers.provider, + headerSync, + message + )) + ).to.emit(l2Bridge, "MessageStatusChanged"); + + const key = getSignalSlot(l1Bridge.address, signal); + + // get storageValue for the key + const storageValue = await ethers.provider.getStorageAt( + l1Bridge.address, + key, + block.number + ); + // make sure it equals 1 so our proof will pass + expect(storageValue).to.be.eq( + "0x0000000000000000000000000000000000000000000000000000000000000001" + ); + }); + }); + + describe("isMessageSent()", function () { + it("should return false, since no message was sent", async function () { + const libData = await ( + await ethers.getContractFactory("TestLibBridgeData") + ).deploy(); + const signal = await libData.hashMessage(m); + + expect(await l1Bridge.isMessageSent(signal)).to.be.eq(false); + }); + + it("should return true if message was sent properly", async function () { + const { signal } = await sendMessage(l1Bridge, m); + + expect(signal).not.to.be.eq(ethers.constants.HashZero); + + expect(await l1Bridge.isMessageSent(signal)).to.be.eq(true); + }); + }); + + describe("isMessageReceived()", function () { + it("should throw if signal is not a bridge message; proof is invalid since sender != bridge.", async function () { + const signal = ethers.utils.hexlify(ethers.utils.randomBytes(32)); + + const tx = await l1Bridge.connect(owner).sendSignal(signal); + + await tx.wait(); + + const sender = owner.address; + + const key = getSignalSlot(sender, signal); + + const { block, blockHeader } = await getBlockHeader( + hre.ethers.provider + ); + + await headerSync.setSyncedHeader(block.hash); + + // get storageValue for the key + const storageValue = await ethers.provider.getStorageAt( + l1Bridge.address, + key, + block.number + ); + // // make sure it equals 1 so we know sendSignal worked + expect(storageValue).to.be.eq( + "0x0000000000000000000000000000000000000000000000000000000000000001" + ); + + const signalProof = await getSignalProof( + hre.ethers.provider, + l1Bridge.address, + key, + block.number, + blockHeader + ); + + await expect( + l2Bridge.isMessageReceived(signal, srcChainId, signalProof) + ).to.be.reverted; + }); + + it("if message is valid and sent by the bridge it should return true", async function () { + const { signal } = await sendMessage(l1Bridge, m); + + const sender = l1Bridge.address; + + const key = getSignalSlot(sender, signal); + + const { block, blockHeader } = await getBlockHeader( + hre.ethers.provider + ); + + await headerSync.setSyncedHeader(block.hash); + + // get storageValue for the key + const storageValue = await ethers.provider.getStorageAt( + l1Bridge.address, + key, + block.number + ); + // // make sure it equals 1 so we know sendMessage worked + expect(storageValue).to.be.eq( + "0x0000000000000000000000000000000000000000000000000000000000000001" + ); + + const signalProof = await getSignalProof( + hre.ethers.provider, + l1Bridge.address, + key, + block.number, + blockHeader + ); + + expect( + await l2Bridge.isMessageReceived( + signal, + srcChainId, + signalProof + ) + ).to.be.eq(true); + }); + }); + + describe("isSignalReceived()", function () { + it("should throw if sender == address(0)", async function () { + const signal = randomBytes32(); + const sender = ethers.constants.AddressZero; + const signalProof = ethers.constants.HashZero; + + await expect( + l2Bridge.isSignalReceived( + signal, + srcChainId, + sender, + signalProof + ) + ).to.be.revertedWith("B:sender"); + }); + + it("should throw if signal == HashZero", async function () { + const signal = ethers.constants.HashZero; + const sender = owner.address; + const signalProof = ethers.constants.HashZero; + + await expect( + l2Bridge.isSignalReceived( + signal, + srcChainId, + sender, + signalProof + ) + ).to.be.revertedWith("B:signal"); + }); + + it("should throw if calling from same layer", async function () { + const signal = randomBytes32(); + + const tx = await l1Bridge.connect(owner).sendSignal(signal); + + await tx.wait(); + + const sender = owner.address; + + const key = getSignalSlot(sender, signal); + + const { block, blockHeader } = await getBlockHeader( + hre.ethers.provider + ); + + await headerSync.setSyncedHeader(block.hash); + + // get storageValue for the key + const storageValue = await ethers.provider.getStorageAt( + l1Bridge.address, + key, + block.number + ); + // make sure it equals 1 so our proof is valid + expect(storageValue).to.be.eq( + "0x0000000000000000000000000000000000000000000000000000000000000001" + ); + + const signalProof = await getSignalProof( + hre.ethers.provider, + l1Bridge.address, + key, + block.number, + blockHeader + ); + + await expect( + l1Bridge.isSignalReceived( + signal, + srcChainId, + sender, + signalProof + ) + ).to.be.revertedWith("B:srcBridge"); + }); + + it("should return true and pass", async function () { + const signal = ethers.utils.hexlify(ethers.utils.randomBytes(32)); + + const tx = await l1Bridge.connect(owner).sendSignal(signal); + + await tx.wait(); + + const sender = owner.address; + + const key = getSignalSlot(sender, signal); + + const { block, blockHeader } = await getBlockHeader( + hre.ethers.provider + ); + + await headerSync.setSyncedHeader(block.hash); + + // get storageValue for the key + const storageValue = await ethers.provider.getStorageAt( + l1Bridge.address, + key, + block.number + ); + // make sure it equals 1 so our proof will pass + expect(storageValue).to.be.eq( + "0x0000000000000000000000000000000000000000000000000000000000000001" + ); + + const signalProof = await getSignalProof( + hre.ethers.provider, + l1Bridge.address, + key, + block.number, + blockHeader + ); + // proving functionality; l2Bridge can check if l1Bridge receives a signal + // allowing for dapp cross layer communication + expect( + await l2Bridge.isSignalReceived( + signal, + srcChainId, + sender, + signalProof + ) + ).to.be.eq(true); + }); + }); +}); diff --git a/packages/protocol/test/bridge/Bridge.test.ts b/packages/protocol/test/bridge/Bridge.test.ts index 633262a0d7..dba7592640 100644 --- a/packages/protocol/test/bridge/Bridge.test.ts +++ b/packages/protocol/test/bridge/Bridge.test.ts @@ -1,131 +1,47 @@ import { expect } from "chai"; -import { BigNumber, Signer } from "ethers"; -import hre, { ethers } from "hardhat"; -import { - getLatestBlockHeader, - getSignalProof, - getSignalSlot, -} from "../../tasks/utils"; -import { - AddressManager, - Bridge, - EtherVault, - LibTrieProof, - TestBadReceiver, - TestHeaderSync, - TestLibBridgeData, -} from "../../typechain"; +import { BigNumber } from "ethers"; +import { ethers } from "hardhat"; +import { AddressManager, Bridge, EtherVault } from "../../typechain"; +import { deployBridge, sendMessage } from "../utils/bridge"; import { Message } from "../utils/message"; -async function deployBridge( - signer: Signer, - addressManager: AddressManager, - destChain: number, - srcChain: number -): Promise<{ bridge: Bridge; etherVault: EtherVault }> { - const libTrieProof: LibTrieProof = await ( - await ethers.getContractFactory("LibTrieProof") - ) - .connect(signer) - .deploy(); - - const libBridgeProcess = await ( - await ethers.getContractFactory("LibBridgeProcess", { - libraries: { - LibTrieProof: libTrieProof.address, - }, - }) - ) - .connect(signer) - .deploy(); - - const libBridgeRetry = await ( - await ethers.getContractFactory("LibBridgeRetry") - ) - .connect(signer) - .deploy(); - - const BridgeFactory = await ethers.getContractFactory("Bridge", { - libraries: { - LibBridgeProcess: libBridgeProcess.address, - LibBridgeRetry: libBridgeRetry.address, - LibTrieProof: libTrieProof.address, - }, - }); - - const bridge: Bridge = await BridgeFactory.connect(signer).deploy(); - - await bridge.connect(signer).init(addressManager.address); - - const etherVault: EtherVault = await ( - await ethers.getContractFactory("EtherVault") - ) - .connect(signer) - .deploy(); - - await etherVault.connect(signer).init(addressManager.address); - - await etherVault.connect(signer).authorize(bridge.address, true); - - await etherVault.connect(signer).authorize(await signer.getAddress(), true); - - await addressManager.setAddress( - `${srcChain}.ether_vault`, - etherVault.address - ); - - await signer.sendTransaction({ - to: etherVault.address, - value: BigNumber.from(100000000), - gasLimit: 1000000, - }); - - return { bridge, etherVault }; -} describe("Bridge", function () { - async function deployBridgeFixture() { - const [owner, nonOwner] = await ethers.getSigners(); + let owner: any; + let nonOwner: any; + let srcChainId: number; + let enabledDestChainId: number; + let l1Bridge: Bridge; + let l1EtherVault: EtherVault; + + beforeEach(async () => { + [owner, nonOwner] = await ethers.getSigners(); const { chainId } = await ethers.provider.getNetwork(); - const srcChainId = chainId; + srcChainId = chainId; - const enabledDestChainId = srcChainId + 1; + enabledDestChainId = srcChainId + 1; const addressManager: AddressManager = await ( await ethers.getContractFactory("AddressManager") ).deploy(); await addressManager.init(); - const { bridge: l1Bridge, etherVault: l1EtherVault } = - await deployBridge( - owner, - addressManager, - enabledDestChainId, - srcChainId - ); + ({ bridge: l1Bridge, etherVault: l1EtherVault } = await deployBridge( + owner, + addressManager, + enabledDestChainId, + srcChainId + )); await addressManager.setAddress( `${enabledDestChainId}.bridge`, "0x0000000000000000000000000000000000000001" // dummy address so chain is "enabled" ); - - // deploy protocol contract - return { - owner, - nonOwner, - l1Bridge, - addressManager, - enabledDestChainId, - l1EtherVault, - srcChainId, - }; - } + }); describe("sendMessage()", function () { it("throws when owner is the zero address", async () => { - const { owner, nonOwner, l1Bridge } = await deployBridgeFixture(); - const message: Message = { id: 1, sender: owner.address, @@ -148,8 +64,6 @@ describe("Bridge", function () { }); it("throws when dest chain id is same as block.chainid", async () => { - const { owner, nonOwner, l1Bridge } = await deployBridgeFixture(); - const network = await ethers.provider.getNetwork(); const message: Message = { id: 1, @@ -173,8 +87,6 @@ describe("Bridge", function () { }); it("throws when dest chain id is not enabled", async () => { - const { owner, nonOwner, l1Bridge } = await deployBridgeFixture(); - const message: Message = { id: 1, sender: owner.address, @@ -197,9 +109,6 @@ describe("Bridge", function () { }); it("throws when msg.value is not the same as expected amount", async () => { - const { owner, nonOwner, l1Bridge, enabledDestChainId } = - await deployBridgeFixture(); - const message: Message = { id: 1, sender: owner.address, @@ -222,14 +131,6 @@ describe("Bridge", function () { }); it("emits event and is successful when message is valid, ether_vault receives the expectedAmount", async () => { - const { - owner, - nonOwner, - l1EtherVault, - l1Bridge, - enabledDestChainId, - } = await deployBridgeFixture(); - const etherVaultOriginalBalance = await ethers.provider.getBalance( l1EtherVault.address ); @@ -254,11 +155,8 @@ describe("Bridge", function () { message.depositValue + message.callValue + message.processingFee; - await expect( - l1Bridge.sendMessage(message, { - value: expectedAmount, - }) - ).to.emit(l1Bridge, "MessageSent"); + + await sendMessage(l1Bridge, message); const etherVaultUpdatedBalance = await ethers.provider.getBalance( l1EtherVault.address @@ -272,16 +170,12 @@ describe("Bridge", function () { describe("sendSignal()", async function () { it("throws when signal is empty", async function () { - const { owner, l1Bridge } = await deployBridgeFixture(); - await expect( l1Bridge.connect(owner).sendSignal(ethers.constants.HashZero) ).to.be.revertedWith("B:signal"); }); it("sends signal, confirms it was sent", async function () { - const { owner, l1Bridge } = await deployBridgeFixture(); - const hash = "0xf2e08f6b93d8cf4f37a3b38f91a8c37198095dde8697463ca3789e25218a8e9d"; await expect(l1Bridge.connect(owner).sendSignal(hash)) @@ -298,16 +192,11 @@ describe("Bridge", function () { describe("isDestChainEnabled()", function () { it("is disabled for unabled chainIds", async () => { - const { l1Bridge } = await deployBridgeFixture(); - const enabled = await l1Bridge.isDestChainEnabled(68); expect(enabled).to.be.eq(false); }); it("is enabled for enabled chainId", async () => { - const { l1Bridge, enabledDestChainId } = - await deployBridgeFixture(); - const enabled = await l1Bridge.isDestChainEnabled( enabledDestChainId ); @@ -317,8 +206,6 @@ describe("Bridge", function () { describe("context()", function () { it("returns unitialized context", async () => { - const { l1Bridge } = await deployBridgeFixture(); - const ctx = await l1Bridge.context(); expect(ctx[0]).to.be.eq(ethers.constants.HashZero); expect(ctx[1]).to.be.eq(ethers.constants.AddressZero); @@ -328,8 +215,6 @@ describe("Bridge", function () { describe("getMessageStatus()", function () { it("returns new for uninitialized signal", async () => { - const { l1Bridge } = await deployBridgeFixture(); - const messageStatus = await l1Bridge.getMessageStatus( ethers.constants.HashZero ); @@ -338,9 +223,6 @@ describe("Bridge", function () { }); it("returns for initiaized signal", async () => { - const { owner, nonOwner, enabledDestChainId, l1Bridge } = - await deployBridgeFixture(); - const message: Message = { id: 1, sender: owner.address, @@ -357,20 +239,7 @@ describe("Bridge", function () { memo: "", }; - const expectedAmount = - message.depositValue + - message.callValue + - message.processingFee; - - const tx = await l1Bridge.sendMessage(message, { - value: expectedAmount, - }); - - const receipt = await tx.wait(); - - const [messageSentEvent] = receipt.events as any as Event[]; - - const { signal } = (messageSentEvent as any).args; + const { signal } = await sendMessage(l1Bridge, message); expect(signal).not.to.be.eq(ethers.constants.HashZero); @@ -382,9 +251,6 @@ describe("Bridge", function () { describe("processMessage()", async function () { it("throws when message.gasLimit is 0 and msg.sender is not the message.owner", async () => { - const { owner, nonOwner, l1Bridge, enabledDestChainId } = - await deployBridgeFixture(); - const message: Message = { id: 1, sender: owner.address, @@ -401,16 +267,12 @@ describe("Bridge", function () { memo: "", }; - const proof = ethers.constants.HashZero; - await expect( - l1Bridge.processMessage(message, proof) + l1Bridge.processMessage(message, ethers.constants.HashZero) ).to.be.revertedWith("B:forbidden"); }); it("throws message.destChainId is not block.chainId", async () => { - const { owner, nonOwner, l1Bridge } = await deployBridgeFixture(); - const message: Message = { id: 1, sender: nonOwner.address, @@ -427,723 +289,9 @@ describe("Bridge", function () { memo: "", }; - const proof = ethers.constants.HashZero; - await expect( - l1Bridge.processMessage(message, proof) + l1Bridge.processMessage(message, ethers.constants.HashZero) ).to.be.revertedWith("B:destChainId"); }); }); }); - -describe("integration:Bridge", function () { - async function deployBridgeFixture() { - const [owner, nonOwner] = await ethers.getSigners(); - - const { chainId } = await ethers.provider.getNetwork(); - - const srcChainId = chainId; - - // seondary node to deploy L2 on - const l2Provider = new ethers.providers.JsonRpcProvider( - "http://localhost:28545" - ); - - const l2Signer = await l2Provider.getSigner( - ( - await l2Provider.listAccounts() - )[0] - ); - - const l2NonOwner = await l2Provider.getSigner(); - - const l2Network = await l2Provider.getNetwork(); - const enabledDestChainId = l2Network.chainId; - - const addressManager: AddressManager = await ( - await ethers.getContractFactory("AddressManager") - ).deploy(); - await addressManager.init(); - - const l2AddressManager: AddressManager = await ( - await ethers.getContractFactory("AddressManager") - ) - .connect(l2Signer) - .deploy(); - await l2AddressManager.init(); - - const { bridge: l1Bridge, etherVault: l1EtherVault } = - await deployBridge( - owner, - addressManager, - enabledDestChainId, - srcChainId - ); - - const { bridge: l2Bridge, etherVault: l2EtherVault } = - await deployBridge( - l2Signer, - l2AddressManager, - srcChainId, - enabledDestChainId - ); - - await addressManager.setAddress( - `${enabledDestChainId}.bridge`, - l2Bridge.address - ); - - await l2AddressManager - .connect(l2Signer) - .setAddress(`${srcChainId}.bridge`, l1Bridge.address); - - const headerSync: TestHeaderSync = await ( - await ethers.getContractFactory("TestHeaderSync") - ) - .connect(l2Signer) - .deploy(); - - await l2AddressManager - .connect(l2Signer) - .setAddress(`${enabledDestChainId}.taiko`, headerSync.address); - - const m: Message = { - id: 1, - sender: owner.address, - srcChainId: srcChainId, - destChainId: enabledDestChainId, - owner: owner.address, - to: owner.address, - refundAddress: owner.address, - depositValue: 1000, - callValue: 1000, - processingFee: 1000, - gasLimit: 10000, - data: ethers.constants.HashZero, - memo: "", - }; - - return { - owner, - l2Signer, - nonOwner, - l2NonOwner, - l1Bridge, - l2Bridge, - addressManager, - enabledDestChainId, - l1EtherVault, - l2EtherVault, - srcChainId, - headerSync, - m, - }; - } - - describe("processMessage()", function () { - it("should throw if message.gasLimit == 0 & msg.sender is not message.owner", async function () { - const { - owner, - l2NonOwner, - srcChainId, - enabledDestChainId, - l2Bridge, - } = await deployBridgeFixture(); - - const m: Message = { - id: 1, - sender: await l2NonOwner.getAddress(), - srcChainId: srcChainId, - destChainId: enabledDestChainId, - owner: owner.address, - to: owner.address, - refundAddress: owner.address, - depositValue: 1000, - callValue: 1000, - processingFee: 1000, - gasLimit: 0, - data: ethers.constants.HashZero, - memo: "", - }; - - await expect( - l2Bridge.processMessage(m, ethers.constants.HashZero) - ).to.be.revertedWith("B:forbidden"); - }); - - it("should throw if message.destChainId is not equal to current block.chainId", async function () { - const { owner, srcChainId, enabledDestChainId, l2Bridge } = - await deployBridgeFixture(); - - const m: Message = { - id: 1, - sender: owner.address, - srcChainId: srcChainId, - destChainId: enabledDestChainId + 1, - owner: owner.address, - to: owner.address, - refundAddress: owner.address, - depositValue: 1000, - callValue: 1000, - processingFee: 1000, - gasLimit: 10000, - data: ethers.constants.HashZero, - memo: "", - }; - - await expect( - l2Bridge.processMessage(m, ethers.constants.HashZero) - ).to.be.revertedWith("B:destChainId"); - }); - - it("should throw if messageStatus of message is != NEW", async function () { - const { l1Bridge, l2Bridge, headerSync, m } = - await deployBridgeFixture(); - - const expectedAmount = - m.depositValue + m.callValue + m.processingFee; - const tx = await l1Bridge.sendMessage(m, { - value: expectedAmount, - }); - - const receipt = await tx.wait(); - - const [messageSentEvent] = receipt.events as any as Event[]; - - const { signal, message } = (messageSentEvent as any).args; - - const sender = l1Bridge.address; - - const key = getSignalSlot(hre, sender, signal); - - const { block, blockHeader } = await getLatestBlockHeader(hre); - - await headerSync.setSyncedHeader(block.hash); - - const signalProof = await getSignalProof( - hre, - l1Bridge.address, - key, - block.number, - blockHeader - ); - - // upon successful processing, this immediately gets marked as DONE - await l2Bridge.processMessage(message, signalProof); - - // recalling this process should be prevented as it's status is no longer NEW - await expect( - l2Bridge.processMessage(message, signalProof) - ).to.be.revertedWith("B:status"); - }); - - it("should throw if message signalproof is not valid", async function () { - const { l1Bridge, l2Bridge, headerSync, m } = - await deployBridgeFixture(); - - const libData: TestLibBridgeData = await ( - await ethers.getContractFactory("TestLibBridgeData") - ).deploy(); - - const signal = await libData.hashMessage(m); - - const sender = l1Bridge.address; - - const key = getSignalSlot(hre, sender, signal); - const { block, blockHeader } = await getLatestBlockHeader(hre); - - await headerSync.setSyncedHeader(ethers.constants.HashZero); - - const signalProof = await getSignalProof( - hre, - l1Bridge.address, - key, - block.number, - blockHeader - ); - - await expect( - l2Bridge.processMessage(m, signalProof) - ).to.be.revertedWith("LTP:invalid storage proof"); - }); - - it("should throw if message has not been received", async function () { - const { l1Bridge, l2Bridge, headerSync, m } = - await deployBridgeFixture(); - - const expectedAmount = - m.depositValue + m.callValue + m.processingFee; - const tx = await l1Bridge.sendMessage(m, { - value: expectedAmount, - }); - - const receipt = await tx.wait(); - - const [messageSentEvent] = receipt.events as any as Event[]; - - const { signal, message } = (messageSentEvent as any).args; - - expect(signal).not.to.be.eq(ethers.constants.HashZero); - - const messageStatus = await l1Bridge.getMessageStatus(signal); - - expect(messageStatus).to.be.eq(0); - - const sender = l1Bridge.address; - - const key = getSignalSlot(hre, sender, signal); - - const { block, blockHeader } = await getLatestBlockHeader(hre); - - await headerSync.setSyncedHeader(ethers.constants.HashZero); - - // get storageValue for the key - const storageValue = await ethers.provider.getStorageAt( - l1Bridge.address, - key, - block.number - ); - // make sure it equals 1 so our proof will pass - expect(storageValue).to.be.eq( - "0x0000000000000000000000000000000000000000000000000000000000000001" - ); - - const signalProof = await getSignalProof( - hre, - l1Bridge.address, - key, - block.number, - blockHeader - ); - - await expect( - l2Bridge.processMessage(message, signalProof) - ).to.be.revertedWith("B:notReceived"); - }); - - it("processes a message when the signal has been verified from the sending chain", async () => { - const { l1Bridge, l2Bridge, headerSync, m } = - await deployBridgeFixture(); - - const expectedAmount = - m.depositValue + m.callValue + m.processingFee; - const tx = await l1Bridge.sendMessage(m, { - value: expectedAmount, - }); - - const receipt = await tx.wait(); - - const [messageSentEvent] = receipt.events as any as Event[]; - - const { signal, message } = (messageSentEvent as any).args; - - expect(signal).not.to.be.eq(ethers.constants.HashZero); - - const messageStatus = await l1Bridge.getMessageStatus(signal); - - expect(messageStatus).to.be.eq(0); - - const sender = l1Bridge.address; - - const key = getSignalSlot(hre, sender, signal); - - const { block, blockHeader } = await getLatestBlockHeader(hre); - - await headerSync.setSyncedHeader(block.hash); - - // get storageValue for the key - const storageValue = await ethers.provider.getStorageAt( - l1Bridge.address, - key, - block.number - ); - // make sure it equals 1 so our proof will pass - expect(storageValue).to.be.eq( - "0x0000000000000000000000000000000000000000000000000000000000000001" - ); - - const signalProof = await getSignalProof( - hre, - l1Bridge.address, - key, - block.number, - blockHeader - ); - - expect( - await l2Bridge.processMessage(message, signalProof, { - gasLimit: BigNumber.from(2000000), - }) - ).to.emit(l2Bridge, "MessageStatusChanged"); - }); - }); - - describe("isMessageSent()", function () { - it("should return false, since no message was sent", async function () { - const { l1Bridge, m } = await deployBridgeFixture(); - - const libData = await ( - await ethers.getContractFactory("TestLibBridgeData") - ).deploy(); - const signal = await libData.hashMessage(m); - - expect(await l1Bridge.isMessageSent(signal)).to.be.eq(false); - }); - - it("should return true if message was sent properly", async function () { - const { l1Bridge, m } = await deployBridgeFixture(); - - const expectedAmount = - m.depositValue + m.callValue + m.processingFee; - const tx = await l1Bridge.sendMessage(m, { - value: expectedAmount, - }); - - const receipt = await tx.wait(); - - const [messageSentEvent] = receipt.events as any as Event[]; - - const { signal } = (messageSentEvent as any).args; - - expect(signal).not.to.be.eq(ethers.constants.HashZero); - - expect(await l1Bridge.isMessageSent(signal)).to.be.eq(true); - }); - }); - - describe("retryMessage()", function () { - async function retriableMessageSetup() { - const { - owner, - l2Signer, - nonOwner, - l2NonOwner, - l1Bridge, - l2Bridge, - addressManager, - enabledDestChainId, - l1EtherVault, - l2EtherVault, - srcChainId, - headerSync, - } = await deployBridgeFixture(); - - const testBadReceiver: TestBadReceiver = await ( - await ethers.getContractFactory("TestBadReceiver") - ) - .connect(l2Signer) - .deploy(); - - await testBadReceiver.deployed(); - - const m: Message = { - id: 1, - sender: owner.address, - srcChainId: srcChainId, - destChainId: enabledDestChainId, - owner: owner.address, - to: testBadReceiver.address, - refundAddress: owner.address, - depositValue: 1000, - callValue: 1000, - processingFee: 1000, - gasLimit: 1, - data: ethers.constants.HashZero, - memo: "", - }; - - const expectedAmount = - m.depositValue + m.callValue + m.processingFee; - const tx = await l1Bridge.connect(owner).sendMessage(m, { - value: expectedAmount, - }); - - const receipt = await tx.wait(); - - const [messageSentEvent] = receipt.events as any as Event[]; - - const { signal, message } = (messageSentEvent as any).args; - - expect(signal).not.to.be.eq(ethers.constants.HashZero); - - const messageStatus = await l1Bridge.getMessageStatus(signal); - - expect(messageStatus).to.be.eq(0); - - const sender = l1Bridge.address; - - const key = getSignalSlot(hre, sender, signal); - - const { block, blockHeader } = await getLatestBlockHeader(hre); - - await headerSync.setSyncedHeader(block.hash); - - const signalProof = await getSignalProof( - hre, - l1Bridge.address, - key, - block.number, - blockHeader - ); - - await l2Bridge - .connect(l2NonOwner) - .processMessage(message, signalProof, { - gasLimit: BigNumber.from(2000000), - }); - - const status = await l2Bridge.getMessageStatus(signal); - expect(status).to.be.eq(1); // message is retriable now - // because the LibBridgeInvoke call failed, because - // message.to is a bad receiver and throws upon receipt - - return { - message, - l2Signer, - l2NonOwner, - l1Bridge, - l2Bridge, - addressManager, - headerSync, - owner, - nonOwner, - srcChainId, - enabledDestChainId, - l1EtherVault, - l2EtherVault, - signal, - }; - } - it("setup message to fail first processMessage", async function () { - const { l2Bridge, signal } = await retriableMessageSetup(); - l2Bridge; - signal; - }); - }); - - describe("isMessageReceived()", function () { - it("should throw if signal is not a bridge message; proof is invalid since sender != bridge.", async function () { - const { owner, l1Bridge, l2Bridge, headerSync, srcChainId } = - await deployBridgeFixture(); - - const signal = ethers.utils.hexlify(ethers.utils.randomBytes(32)); - - const tx = await l1Bridge.connect(owner).sendSignal(signal); - - await tx.wait(); - - const sender = owner.address; - - const key = getSignalSlot(hre, sender, signal); - - const { block, blockHeader } = await getLatestBlockHeader(hre); - - await headerSync.setSyncedHeader(block.hash); - - // get storageValue for the key - const storageValue = await ethers.provider.getStorageAt( - l1Bridge.address, - key, - block.number - ); - // // make sure it equals 1 so we know sendSignal worked - expect(storageValue).to.be.eq( - "0x0000000000000000000000000000000000000000000000000000000000000001" - ); - - const signalProof = await getSignalProof( - hre, - l1Bridge.address, - key, - block.number, - blockHeader - ); - - await expect( - l2Bridge.isMessageReceived(signal, srcChainId, signalProof) - ).to.be.reverted; - }); - - it("should return true", async function () { - const { l1Bridge, srcChainId, l2Bridge, headerSync, m } = - await deployBridgeFixture(); - - const expectedAmount = - m.depositValue + m.callValue + m.processingFee; - const tx = await l1Bridge.sendMessage(m, { - value: expectedAmount, - }); - - const receipt = await tx.wait(); - - const [messageSentEvent] = receipt.events as any as Event[]; - - const { signal } = (messageSentEvent as any).args; - - const sender = l1Bridge.address; - - const key = getSignalSlot(hre, sender, signal); - - const { block, blockHeader } = await getLatestBlockHeader(hre); - - await headerSync.setSyncedHeader(block.hash); - - // get storageValue for the key - const storageValue = await ethers.provider.getStorageAt( - l1Bridge.address, - key, - block.number - ); - // // make sure it equals 1 so we know sendMessage worked - expect(storageValue).to.be.eq( - "0x0000000000000000000000000000000000000000000000000000000000000001" - ); - - const signalProof = await getSignalProof( - hre, - l1Bridge.address, - key, - block.number, - blockHeader - ); - - expect( - await l2Bridge.isMessageReceived( - signal, - srcChainId, - signalProof - ) - ).to.be.eq(true); - }); - }); - - describe("isSignalReceived()", function () { - it("should throw if sender == address(0)", async function () { - const { l2Bridge, srcChainId } = await deployBridgeFixture(); - - const signal = ethers.utils.randomBytes(32); - const sender = ethers.constants.AddressZero; - const signalProof = ethers.constants.HashZero; - - await expect( - l2Bridge.isSignalReceived( - signal, - srcChainId, - sender, - signalProof - ) - ).to.be.revertedWith("B:sender"); - }); - - it("should throw if signal == HashZero", async function () { - const { owner, l2Bridge, srcChainId } = await deployBridgeFixture(); - - const signal = ethers.constants.HashZero; - const sender = owner.address; - const signalProof = ethers.constants.HashZero; - - await expect( - l2Bridge.isSignalReceived( - signal, - srcChainId, - sender, - signalProof - ) - ).to.be.revertedWith("B:signal"); - }); - - it("should throw if calling from same layer", async function () { - const { owner, l1Bridge, headerSync, srcChainId } = - await deployBridgeFixture(); - const signal = ethers.utils.hexlify(ethers.utils.randomBytes(32)); - - const tx = await l1Bridge.connect(owner).sendSignal(signal); - - await tx.wait(); - - const sender = owner.address; - - const key = getSignalSlot(hre, sender, signal); - - const { block, blockHeader } = await getLatestBlockHeader(hre); - - await headerSync.setSyncedHeader(block.hash); - - // get storageValue for the key - const storageValue = await ethers.provider.getStorageAt( - l1Bridge.address, - key, - block.number - ); - // make sure it equals 1 so our proof is valid - expect(storageValue).to.be.eq( - "0x0000000000000000000000000000000000000000000000000000000000000001" - ); - - const signalProof = await getSignalProof( - hre, - l1Bridge.address, - key, - block.number, - blockHeader - ); - - await expect( - l1Bridge.isSignalReceived( - signal, - srcChainId, - sender, - signalProof - ) - ).to.be.revertedWith("B:srcBridge"); - }); - - it("should return true and pass", async function () { - const { owner, l1Bridge, l2Bridge, headerSync, srcChainId } = - await deployBridgeFixture(); - - const signal = ethers.utils.hexlify(ethers.utils.randomBytes(32)); - - const tx = await l1Bridge.connect(owner).sendSignal(signal); - - await tx.wait(); - - const sender = owner.address; - - const key = getSignalSlot(hre, sender, signal); - - const { block, blockHeader } = await getLatestBlockHeader(hre); - - await headerSync.setSyncedHeader(block.hash); - - // get storageValue for the key - const storageValue = await ethers.provider.getStorageAt( - l1Bridge.address, - key, - block.number - ); - // make sure it equals 1 so our proof will pass - expect(storageValue).to.be.eq( - "0x0000000000000000000000000000000000000000000000000000000000000001" - ); - - const signalProof = await getSignalProof( - hre, - l1Bridge.address, - key, - block.number, - blockHeader - ); - // proving functionality; l2Bridge can check if l1Bridge receives a signal - // allowing for dapp cross layer communication - expect( - await l2Bridge.isSignalReceived( - signal, - srcChainId, - sender, - signalProof - ) - ).to.be.eq(true); - }); - }); -}); diff --git a/packages/protocol/test/bridge/BridgedERC20.test.ts b/packages/protocol/test/bridge/BridgedERC20.test.ts index 3d24213e06..3ce2a42ed1 100644 --- a/packages/protocol/test/bridge/BridgedERC20.test.ts +++ b/packages/protocol/test/bridge/BridgedERC20.test.ts @@ -7,6 +7,7 @@ import { ERC20_BURN_AMOUNT_EXCEEDED, ERC20_TRANSFER_AMOUNT_EXCEEDED, } from "../constants/errors"; +import deployAddressManager from "../utils/addressManager"; const WETH_GOERLI = "0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6"; const CHAIN_ID_GOERLI = 5; @@ -28,19 +29,12 @@ describe("BridgedERC20", function () { }); beforeEach(async function () { - unInitAddressManager = await ( - await ethers.getContractFactory("AddressManager") - ).deploy(); - await unInitAddressManager.init(); - + unInitAddressManager = await deployAddressManager(owner); unInitERC20 = await (await ethers.getContractFactory("BridgedERC20")) .connect(owner) .deploy(); - addressManager = await ( - await ethers.getContractFactory("AddressManager") - ).deploy(); - await addressManager.init(); + addressManager = await deployAddressManager(owner); const network = await ethers.provider.getNetwork(); diff --git a/packages/protocol/test/bridge/libs/LibBridgeData.test.ts b/packages/protocol/test/bridge/libs/LibBridgeData.test.ts index 2d04c6e4d4..c78bbd7634 100644 --- a/packages/protocol/test/bridge/libs/LibBridgeData.test.ts +++ b/packages/protocol/test/bridge/libs/LibBridgeData.test.ts @@ -2,8 +2,7 @@ import { expect } from "chai"; import { ethers } from "hardhat"; import { TestLibBridgeData } from "../../../typechain"; import { K_BRIDGE_MESSAGE } from "../../constants/messages"; -import { MessageStatus } from "../../../tasks/utils"; -import { Message } from "../../utils/message"; +import { Message, MessageStatus } from "../../utils/message"; describe("LibBridgeData", function () { let owner: any; diff --git a/packages/protocol/test/bridge/libs/LibBridgeProcess.test.ts b/packages/protocol/test/bridge/libs/LibBridgeProcess.test.ts index c542d640ae..4655bdb395 100644 --- a/packages/protocol/test/bridge/libs/LibBridgeProcess.test.ts +++ b/packages/protocol/test/bridge/libs/LibBridgeProcess.test.ts @@ -1,8 +1,11 @@ import * as helpers from "@nomicfoundation/hardhat-network-helpers"; import { expect } from "chai"; import hre, { ethers } from "hardhat"; -import { getMessageStatusSlot, MessageStatus } from "../../../tasks/utils"; -import { Message } from "../../utils/message"; +import { + getMessageStatusSlot, + Message, + MessageStatus, +} from "../../utils/message"; import { AddressManager, EtherVault, diff --git a/packages/protocol/test/bridge/libs/LibBridgeRetry.test.ts b/packages/protocol/test/bridge/libs/LibBridgeRetry.test.ts index bd99c8110e..43a474c227 100644 --- a/packages/protocol/test/bridge/libs/LibBridgeRetry.test.ts +++ b/packages/protocol/test/bridge/libs/LibBridgeRetry.test.ts @@ -1,12 +1,12 @@ import * as helpers from "@nomicfoundation/hardhat-network-helpers"; import { expect } from "chai"; import hre, { ethers } from "hardhat"; -import { Message } from "../../utils/message"; import { getMessageStatusSlot, - decode, + Message, MessageStatus, -} from "../../../tasks/utils"; +} from "../../utils/message"; +import { decode } from "../../../tasks/utils"; import { AddressManager, EtherVault, @@ -15,6 +15,7 @@ import { TestLibBridgeRetry, TestReceiver, } from "../../../typechain"; +import deployAddressManager from "../../utils/addressManager"; describe("LibBridgeRetry", function () { let owner: any; @@ -32,15 +33,13 @@ describe("LibBridgeRetry", function () { }); beforeEach(async function () { - const addressManager: AddressManager = await ( - await ethers.getContractFactory("AddressManager") - ).deploy(); - await addressManager.init(); + const addressManager: AddressManager = await deployAddressManager( + owner + ); - const badAddressManager: AddressManager = await ( - await ethers.getContractFactory("AddressManager") - ).deploy(); - await badAddressManager.init(); + const badAddressManager: AddressManager = await deployAddressManager( + owner + ); etherVault = await (await ethers.getContractFactory("EtherVault")) .connect(etherVaultOwner) diff --git a/packages/protocol/test/bridge/EtherVault.test.ts b/packages/protocol/test/etherVault/EtherVault.test.ts similarity index 96% rename from packages/protocol/test/bridge/EtherVault.test.ts rename to packages/protocol/test/etherVault/EtherVault.test.ts index 0d4b403d21..9d64ec1886 100644 --- a/packages/protocol/test/bridge/EtherVault.test.ts +++ b/packages/protocol/test/etherVault/EtherVault.test.ts @@ -2,6 +2,7 @@ import { expect } from "chai"; import { AddressManager, EtherVault } from "../../typechain"; import { ethers } from "hardhat"; import { BigNumber } from "ethers"; +import deployAddressManager from "../utils/addressManager"; describe("EtherVault", function () { let owner: any; @@ -16,10 +17,9 @@ describe("EtherVault", function () { }); beforeEach(async function () { - const addressManager: AddressManager = await ( - await ethers.getContractFactory("AddressManager") - ).deploy(); - await addressManager.init(); + const addressManager: AddressManager = await deployAddressManager( + owner + ); etherVault = await (await ethers.getContractFactory("EtherVault")) .connect(owner) diff --git a/packages/protocol/test/libs/LibTrieProof.test.ts b/packages/protocol/test/libs/LibTrieProof.test.ts index 4d8829900c..434b6e3aa6 100644 --- a/packages/protocol/test/libs/LibTrieProof.test.ts +++ b/packages/protocol/test/libs/LibTrieProof.test.ts @@ -1,9 +1,10 @@ import { expect } from "chai"; -import hre, { ethers } from "hardhat"; +import { ethers } from "hardhat"; import RLP from "rlp"; +import { sendMessage } from "../utils/bridge"; import { Message } from "../utils/message"; import { EthGetProofResponse } from "../utils/rpc"; -import { getSignalSlot } from "../../tasks/utils"; +import { getSignalSlot } from "../utils/signal"; describe("integration:LibTrieProof", function () { async function deployLibTrieProofFixture() { @@ -90,19 +91,9 @@ describe("integration:LibTrieProof", function () { memo: "", }; - const expectedAmount = - message.depositValue + - message.callValue + - message.processingFee; - const tx = await bridge.sendMessage(message, { - value: expectedAmount, - }); + const { tx, signal } = await sendMessage(bridge, message); - const receipt = await tx.wait(); - - const [messageSentEvent] = receipt.events as any as Event[]; - - const { signal } = (messageSentEvent as any).args; + await tx.wait(); expect(signal).not.to.be.eq(ethers.constants.HashZero); @@ -110,9 +101,7 @@ describe("integration:LibTrieProof", function () { expect(messageStatus).to.be.eq(0); - const sender = bridge.address; - - const key = getSignalSlot(hre, sender, signal); + const key = getSignalSlot(bridge.address, signal); // use this instead of ethers.provider.getBlock() beccause it doesnt have stateRoot // in the response diff --git a/packages/protocol/test/test_integration.sh b/packages/protocol/test/test_integration.sh index 485cc8b4d2..5b0d7c7ad7 100755 --- a/packages/protocol/test/test_integration.sh +++ b/packages/protocol/test/test_integration.sh @@ -80,4 +80,4 @@ trap cleanup EXIT INT KILL ERR # Run the tests PRIVATE_KEY=$TEST_ACCOUNT_PRIV_KEY \ - npx hardhat test --network l1_test --grep "^integration" \ No newline at end of file + npx hardhat test --network l1_test --grep "^$TEST_TYPE" \ No newline at end of file diff --git a/packages/protocol/test/thirdparty/AddressManager.test.ts b/packages/protocol/test/thirdparty/AddressManager.test.ts index c4cf33276c..b89759e3d2 100644 --- a/packages/protocol/test/thirdparty/AddressManager.test.ts +++ b/packages/protocol/test/thirdparty/AddressManager.test.ts @@ -1,6 +1,7 @@ import { expect } from "chai"; import { AddressManager } from "../../typechain"; import { ethers } from "hardhat"; +import deployAddressManager from "../utils/addressManager"; describe("AddressManager", function () { let owner: any; @@ -12,10 +13,7 @@ describe("AddressManager", function () { }); beforeEach(async function () { - addressManager = await ( - await ethers.getContractFactory("AddressManager") - ).deploy(); - await addressManager.init(); + addressManager = await deployAddressManager(owner); }); describe("setAddress()", async () => { diff --git a/packages/protocol/test/thirdparty/LibBlockHeaderDecoder.test.ts b/packages/protocol/test/thirdparty/LibBlockHeaderDecoder.test.ts index e3e2115601..3c2edc9431 100644 --- a/packages/protocol/test/thirdparty/LibBlockHeaderDecoder.test.ts +++ b/packages/protocol/test/thirdparty/LibBlockHeaderDecoder.test.ts @@ -1,14 +1,15 @@ // eslint-disable-next-line no-unused-vars import { expect } from "chai"; import { keccak256 } from "ethers/lib/utils"; +import { LibBlockHeaderDecoder, TestLibBlockHeader } from "../../typechain"; const hre = require("hardhat"); const ethers = hre.ethers; const EBN = ethers.BigNumber; describe("LibBlockHeaderDecoder", async function () { // eslint-disable-next-line no-unused-vars - let blockHeaderDecoder: any; - let hashBlockHeader: any; + let blockHeaderDecoder: LibBlockHeaderDecoder; + let hashBlockHeader: TestLibBlockHeader; before(async function () { // Deploying Lib to Link diff --git a/packages/protocol/test/bridge/TokenVault.test.ts b/packages/protocol/test/tokenVault/TokenVault.test.ts similarity index 100% rename from packages/protocol/test/bridge/TokenVault.test.ts rename to packages/protocol/test/tokenVault/TokenVault.test.ts diff --git a/packages/protocol/test/tokenomics/Tokenomics.test.ts b/packages/protocol/test/tokenomics/Tokenomics.test.ts new file mode 100644 index 0000000000..60d066b809 --- /dev/null +++ b/packages/protocol/test/tokenomics/Tokenomics.test.ts @@ -0,0 +1,272 @@ +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); + }); + + describe("bootstrapHalvingPeriod", function () { + 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; + } + }); + }); + + 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/utils.ts b/packages/protocol/test/tokenomics/utils.ts new file mode 100644 index 0000000000..b2d56776b4 --- /dev/null +++ b/packages/protocol/test/tokenomics/utils.ts @@ -0,0 +1,70 @@ +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 new file mode 100644 index 0000000000..47e577c116 --- /dev/null +++ b/packages/protocol/test/utils/addressManager.ts @@ -0,0 +1,15 @@ +import { ethers } from "ethers"; +import { ethers as hardhatEthers } from "hardhat"; +import { AddressManager } from "../../typechain"; + +const deployAddressManager = async (signer: ethers.Signer) => { + const addressManager: AddressManager = await ( + await hardhatEthers.getContractFactory("AddressManager") + ) + .connect(signer) + .deploy(); + await addressManager.init(); + return addressManager; +}; + +export default deployAddressManager; diff --git a/packages/protocol/test/utils/block_metadata.ts b/packages/protocol/test/utils/block_metadata.ts new file mode 100644 index 0000000000..74532a6ebd --- /dev/null +++ b/packages/protocol/test/utils/block_metadata.ts @@ -0,0 +1,17 @@ +import { BigNumberish } from "ethers"; + +type BlockMetadata = { + id: number; + l1Height: number; + l1Hash: string; + beneficiary: string; + txListHash: string; + mixHash: string; + extraData: string; + gasLimit: BigNumberish; + timestamp: number; + commitSlot: number; + commitHeight: number; +}; + +export { BlockMetadata }; diff --git a/packages/protocol/test/utils/bridge.ts b/packages/protocol/test/utils/bridge.ts new file mode 100644 index 0000000000..0e699b20bd --- /dev/null +++ b/packages/protocol/test/utils/bridge.ts @@ -0,0 +1,164 @@ +import { BigNumber, ethers, Signer } from "ethers"; +import { ethers as hardhatEthers } from "hardhat"; +import { + AddressManager, + Bridge, + EtherVault, + LibTrieProof, + TestHeaderSync, +} from "../../typechain"; +import { Message } from "./message"; +import { Block, BlockHeader, getBlockHeader } from "./rpc"; +import { getSignalProof, getSignalSlot } from "./signal"; + +async function deployBridge( + signer: Signer, + addressManager: AddressManager, + destChain: number, + srcChain: number +): Promise<{ bridge: Bridge; etherVault: EtherVault }> { + const libTrieProof: LibTrieProof = await ( + await hardhatEthers.getContractFactory("LibTrieProof") + ) + .connect(signer) + .deploy(); + + const libBridgeProcess = await ( + await hardhatEthers.getContractFactory("LibBridgeProcess", { + libraries: { + LibTrieProof: libTrieProof.address, + }, + }) + ) + .connect(signer) + .deploy(); + + const libBridgeRetry = await ( + await hardhatEthers.getContractFactory("LibBridgeRetry") + ) + .connect(signer) + .deploy(); + + const BridgeFactory = await hardhatEthers.getContractFactory("Bridge", { + libraries: { + LibBridgeProcess: libBridgeProcess.address, + LibBridgeRetry: libBridgeRetry.address, + LibTrieProof: libTrieProof.address, + }, + }); + + const bridge: Bridge = await BridgeFactory.connect(signer).deploy(); + + await bridge.connect(signer).init(addressManager.address); + + const etherVault: EtherVault = await ( + await hardhatEthers.getContractFactory("EtherVault") + ) + .connect(signer) + .deploy(); + + await etherVault.connect(signer).init(addressManager.address); + + await etherVault.connect(signer).authorize(bridge.address, true); + + await etherVault.connect(signer).authorize(await signer.getAddress(), true); + + await addressManager.setAddress( + `${srcChain}.ether_vault`, + etherVault.address + ); + + await signer.sendTransaction({ + to: etherVault.address, + value: BigNumber.from(100000000), + gasLimit: 1000000, + }); + + await addressManager.setAddress(`${destChain}.bridge`, bridge.address); + + return { bridge, etherVault }; +} + +async function sendMessage( + bridge: Bridge, + m: Message +): Promise<{ + bridge: Bridge; + signal: any; + messageSentEvent: any; + message: Message; + tx: ethers.ContractTransaction; +}> { + const expectedAmount = m.depositValue + m.callValue + m.processingFee; + + const tx = await bridge.sendMessage(m, { + value: expectedAmount, + }); + + const receipt = await tx.wait(); + + const [messageSentEvent] = receipt.events as any as Event[]; + + const { signal, message } = (messageSentEvent as any).args; + + return { bridge, messageSentEvent, signal, message, tx }; +} + +async function processMessage( + l1Bridge: Bridge, + l2Bridge: Bridge, + signal: string, + provider: ethers.providers.JsonRpcProvider, + headerSync: TestHeaderSync, + message: Message +): Promise<{ + tx: ethers.ContractTransaction; + signalProof: string; + block: Block; + blockHeader: BlockHeader; +}> { + const sender = l1Bridge.address; + + const key = getSignalSlot(sender, signal); + + const { block, blockHeader } = await getBlockHeader(provider); + + await headerSync.setSyncedHeader(block.hash); + + const signalProof = await getSignalProof( + provider, + l1Bridge.address, + key, + block.number, + blockHeader + ); + + const tx = await l2Bridge.processMessage(message, signalProof); + return { tx, signalProof, block, blockHeader }; +} + +async function sendAndProcessMessage( + provider: ethers.providers.JsonRpcProvider, + headerSync: TestHeaderSync, + m: Message, + l1Bridge: Bridge, + l2Bridge: Bridge +): Promise<{ + tx: ethers.ContractTransaction; + message: Message; + signal: string; + signalProof: string; +}> { + const { signal, message } = await sendMessage(l1Bridge, m); + const { tx, signalProof } = await processMessage( + l1Bridge, + l2Bridge, + signal, + provider, + headerSync, + message + ); + return { tx, signal, message, signalProof }; +} + +export { deployBridge, sendMessage, processMessage, sendAndProcessMessage }; diff --git a/packages/protocol/test/utils/bytes.ts b/packages/protocol/test/utils/bytes.ts new file mode 100644 index 0000000000..50331a1d3a --- /dev/null +++ b/packages/protocol/test/utils/bytes.ts @@ -0,0 +1,7 @@ +import { ethers } from "hardhat"; + +function randomBytes32() { + return ethers.utils.hexlify(ethers.utils.randomBytes(32)); +} + +export { randomBytes32 }; diff --git a/packages/protocol/test/utils/commit.ts b/packages/protocol/test/utils/commit.ts new file mode 100644 index 0000000000..f22f366c0c --- /dev/null +++ b/packages/protocol/test/utils/commit.ts @@ -0,0 +1,32 @@ +import { ethers } from "ethers"; +import RLP from "rlp"; +import { TaikoL1 } from "../../typechain"; + +const generateCommitHash = ( + block: ethers.providers.Block +): { hash: string; txListHash: string } => { + const txListHash = ethers.utils.keccak256(RLP.encode(block.transactions)); + const hash = ethers.utils.keccak256( + ethers.utils.solidityPack( + ["address", "bytes32"], + [block.miner, txListHash] + ) + ); + + return { hash: hash, txListHash: txListHash }; +}; + +const commitBlock = async ( + taikoL1: TaikoL1, + block: ethers.providers.Block, + commitSlot: number = 0 +): Promise<{ + tx: ethers.ContractTransaction; + commit: { hash: string; txListHash: string }; +}> => { + const commit = generateCommitHash(block); + const tx = await taikoL1.commitBlock(commitSlot, commit.hash); + return { tx, commit }; +}; + +export { generateCommitHash, commitBlock }; diff --git a/packages/protocol/test/utils/encoding.ts b/packages/protocol/test/utils/encoding.ts new file mode 100644 index 0000000000..7984bb22e7 --- /dev/null +++ b/packages/protocol/test/utils/encoding.ts @@ -0,0 +1,23 @@ +import { ethers } from "hardhat"; +import { BlockMetadata } from "./block_metadata"; +import Evidence from "./evidence"; + +function encodeBlockMetadata(meta: BlockMetadata) { + return ethers.utils.defaultAbiCoder.encode( + [ + "tuple(uint256 id, uint256 l1Height, bytes32 l1Hash, address beneficiary, bytes32 txListHash, bytes32 mixHash, bytes extraData, uint64 gasLimit, uint64 timestamp, uint64 commitHeight, uint64 commitSlot)", + ], + [meta] + ); +} + +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)", + ], + [evidence] + ); +} + +export { encodeBlockMetadata, encodeEvidence }; diff --git a/packages/protocol/test/utils/evidence.ts b/packages/protocol/test/utils/evidence.ts new file mode 100644 index 0000000000..58e39c1641 --- /dev/null +++ b/packages/protocol/test/utils/evidence.ts @@ -0,0 +1,11 @@ +import { BlockMetadata } from "./block_metadata"; +import { BlockHeader } from "./rpc"; + +type Evidence = { + meta: BlockMetadata; + header: BlockHeader; + prover: string; + proofs: string[]; +}; + +export default Evidence; diff --git a/packages/protocol/test/utils/message.ts b/packages/protocol/test/utils/message.ts index 717641d1b2..bf74fb9906 100644 --- a/packages/protocol/test/utils/message.ts +++ b/packages/protocol/test/utils/message.ts @@ -14,4 +14,18 @@ type Message = { memo: string; }; -export { Message }; +const MessageStatus = { + NEW: 0, + RETRIABLE: 1, + DONE: 2, + FAILED: 3, +}; + +async function getMessageStatusSlot(hre: any, signal: any) { + return hre.ethers.utils.solidityKeccak256( + ["string", "bytes"], + ["MESSAGE_STATUS", signal] + ); +} + +export { Message, MessageStatus, getMessageStatusSlot }; diff --git a/packages/protocol/test/utils/propose.ts b/packages/protocol/test/utils/propose.ts new file mode 100644 index 0000000000..7d961c0ae5 --- /dev/null +++ b/packages/protocol/test/utils/propose.ts @@ -0,0 +1,48 @@ +import { BigNumber, ethers } from "ethers"; +import RLP from "rlp"; +import { TaikoL1 } from "../../typechain"; +import { BlockMetadata } from "./block_metadata"; +import { encodeBlockMetadata } from "./encoding"; + +const buildProposeBlockInputs = ( + block: ethers.providers.Block, + meta: BlockMetadata +) => { + const inputs = []; + const blockMetadataBytes = encodeBlockMetadata(meta); + inputs[0] = blockMetadataBytes; + inputs[1] = RLP.encode(block.transactions); + return inputs; +}; + +const proposeBlock = async ( + taikoL1: TaikoL1, + block: ethers.providers.Block, + txListHash: string, + commitHeight: number, + gasLimit: BigNumber, + commitSlot: number = 0 +) => { + const meta: BlockMetadata = { + id: 0, + l1Height: 0, + l1Hash: ethers.constants.HashZero, + beneficiary: block.miner, + txListHash: txListHash, + mixHash: ethers.constants.HashZero, + extraData: block.extraData, + gasLimit: gasLimit, + timestamp: 0, + commitSlot: commitSlot, + commitHeight: commitHeight, + }; + + 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; +}; + +export { buildProposeBlockInputs, proposeBlock }; diff --git a/packages/protocol/test/utils/proposer.ts b/packages/protocol/test/utils/proposer.ts new file mode 100644 index 0000000000..1bb2c98f10 --- /dev/null +++ b/packages/protocol/test/utils/proposer.ts @@ -0,0 +1,62 @@ +import { ethers } from "ethers"; +import { TaikoL1 } from "../../typechain"; +import { commitBlock } from "./commit"; +import { proposeBlock } from "./propose"; +import sleep from "./sleep"; + +class Proposer { + private readonly taikoL1: TaikoL1; + private readonly l2Provider: ethers.providers.JsonRpcProvider; + private readonly commitConfirms: number; + private readonly maxNumBlocks: number; + private nextCommitSlot: number; + + private proposingMutex: boolean = false; + + constructor( + taikoL1: TaikoL1, + l2Provider: ethers.providers.JsonRpcProvider, + commitConfirms: number, + maxNumBlocks: number, + initialCommitSlot: number + ) { + this.taikoL1 = taikoL1; + this.l2Provider = l2Provider; + this.commitConfirms = commitConfirms; + this.maxNumBlocks = maxNumBlocks; + this.nextCommitSlot = initialCommitSlot; + } + + async commitThenProposeBlock(block?: ethers.providers.Block) { + while (this.proposingMutex) { + await sleep(100); + } + 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; + } +} + +export default Proposer; diff --git a/packages/protocol/test/utils/prove.ts b/packages/protocol/test/utils/prove.ts new file mode 100644 index 0000000000..3ee00ec7e8 --- /dev/null +++ b/packages/protocol/test/utils/prove.ts @@ -0,0 +1,92 @@ +import { ethers } from "ethers"; +import RLP from "rlp"; +import { TaikoL1, TaikoL2 } from "../../typechain"; +import { BlockMetadata } from "./block_metadata"; +import { encodeEvidence } from "./encoding"; +import Evidence from "./evidence"; +import { BlockHeader, getBlockHeader } from "./rpc"; + +const buildProveBlockInputs = ( + meta: BlockMetadata, + header: BlockHeader, + prover: string, + anchorTx: Uint8Array | string, + anchorReceipt: Uint8Array | string, + zkProofsPerBlock: number +) => { + const inputs = []; + const evidence: Evidence = { + meta: meta, + header: header, + prover: prover, + proofs: [], // TODO + }; + // 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 + for (let i = 0; i < zkProofsPerBlock + 2; i++) { + evidence.proofs.push("0xff"); + } + + 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 +) => { + 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, + 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; +}; + +export { buildProveBlockInputs, proveBlock }; diff --git a/packages/protocol/test/utils/prover.ts b/packages/protocol/test/utils/prover.ts new file mode 100644 index 0000000000..4ca2f2c713 --- /dev/null +++ b/packages/protocol/test/utils/prover.ts @@ -0,0 +1,53 @@ +import { ethers } from "ethers"; +import { TaikoL1, TaikoL2 } from "../../typechain"; +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; + + constructor( + taikoL1: TaikoL1, + taikoL2: TaikoL2, + l1Provider: ethers.providers.JsonRpcProvider, + l2Provider: ethers.providers.JsonRpcProvider, + l2Signer: ethers.Signer + ) { + this.taikoL1 = taikoL1; + this.taikoL2 = taikoL2; + this.l1Provider = l1Provider; + this.l2Provider = l2Provider; + this.l2Signer = l2Signer; + } + + async prove( + proverAddress: string, + blockId: number, + blockNumber: number, + meta: BlockMetadata + ) { + while (this.provingMutex) { + await sleep(100); + } + this.provingMutex = true; + + await proveBlock( + this.taikoL1, + this.taikoL2, + this.l2Signer, + this.l2Provider, + proverAddress, + blockId, + blockNumber, + meta + ); + } +} + +export default Prover; diff --git a/packages/protocol/test/utils/provider.ts b/packages/protocol/test/utils/provider.ts new file mode 100644 index 0000000000..66bcdbdd6f --- /dev/null +++ b/packages/protocol/test/utils/provider.ts @@ -0,0 +1,13 @@ +import { ethers } from "ethers"; +// providers for integration tests + +const getL1Provider = () => + new ethers.providers.JsonRpcProvider("http://localhost:18545"); + +const getL2Provider = () => + new ethers.providers.JsonRpcProvider("http://localhost:28545"); + +const getDefaultL2Signer = async () => + await getL2Provider().getSigner((await getL2Provider().listAccounts())[0]); + +export { getL1Provider, getL2Provider, getDefaultL2Signer }; diff --git a/packages/protocol/test/utils/rpc.ts b/packages/protocol/test/utils/rpc.ts index 250d4ea87a..a72ee51492 100644 --- a/packages/protocol/test/utils/rpc.ts +++ b/packages/protocol/test/utils/rpc.ts @@ -1,3 +1,5 @@ +import { BigNumber, ethers } from "ethers"; + type StorageEntry = { key: string; value: string; @@ -56,4 +58,47 @@ type BlockHeader = { baseFeePerGas: number; }; -export { Block, BlockHeader, StorageEntry, EthGetProofResponse }; +async function getBlockHeader( + provider: ethers.providers.JsonRpcProvider, + blockNumber?: number +) { + const b = await provider.getBlock( + blockNumber ? BigNumber.from(blockNumber).toHexString() : "latest" + ); + + const block: Block = await provider.send("eth_getBlockByHash", [ + b.hash, + false, + ]); + + const logsBloom = block.logsBloom.toString().substring(2); + + const blockHeader: BlockHeader = { + parentHash: block.parentHash, + ommersHash: block.sha3Uncles, + beneficiary: block.miner, + stateRoot: block.stateRoot, + transactionsRoot: block.transactionsRoot, + receiptsRoot: block.receiptsRoot, + logsBloom: logsBloom.match(/.{1,64}/g)!.map((s: string) => "0x" + s), + difficulty: block.difficulty, + height: block.number, + gasLimit: block.gasLimit, + gasUsed: block.gasUsed, + timestamp: block.timestamp, + extraData: block.extraData, + mixHash: block.mixHash, + nonce: block.nonce, + baseFeePerGas: block.baseFeePerGas ? parseInt(block.baseFeePerGas) : 0, + }; + + return { block, blockHeader }; +} + +export { + Block, + BlockHeader, + StorageEntry, + EthGetProofResponse, + getBlockHeader, +}; diff --git a/packages/protocol/test/utils/seed.ts b/packages/protocol/test/utils/seed.ts new file mode 100644 index 0000000000..83665b3ba1 --- /dev/null +++ b/packages/protocol/test/utils/seed.ts @@ -0,0 +1,23 @@ +import { BigNumber, ethers } from "ethers"; + +const createAndSeedWallets = async ( + len: number, + signer: any, + amount: BigNumber = ethers.utils.parseEther("1") +): Promise => { + const wallets: ethers.Wallet[] = []; + for (let i = 0; i < len; i++) { + const wallet = ethers.Wallet.createRandom().connect(signer.provider); + const tx = await signer.sendTransaction({ + to: await wallet.getAddress(), + value: amount, + }); + + await tx.wait(1); + wallets.push(wallet); + } + + return wallets; +}; + +export default createAndSeedWallets; diff --git a/packages/protocol/test/utils/signal.ts b/packages/protocol/test/utils/signal.ts new file mode 100644 index 0000000000..c36a3052a1 --- /dev/null +++ b/packages/protocol/test/utils/signal.ts @@ -0,0 +1,46 @@ +import { ethers } from "ethers"; +import RLP from "rlp"; +import { BlockHeader, EthGetProofResponse } from "./rpc"; + +function getSignalSlot(sender: string, signal: any) { + return ethers.utils.keccak256( + ethers.utils.solidityPack( + ["string", "address", "bytes32"], + ["SIGNAL", sender, signal] + ) + ); +} + +async function getSignalProof( + provider: ethers.providers.JsonRpcProvider, + contractAddress: string, + key: string, + blockNumber: number, + blockHeader: BlockHeader +) { + const proof: EthGetProofResponse = await provider.send("eth_getProof", [ + contractAddress, + [key], + blockNumber, + ]); + + // RLP encode the proof together for LibTrieProof to decode + const encodedProof = ethers.utils.defaultAbiCoder.encode( + ["bytes", "bytes"], + [ + RLP.encode(proof.accountProof), + RLP.encode(proof.storageProof[0].proof), + ] + ); + // encode the SignalProof struct from LibBridgeSignal + const signalProof = ethers.utils.defaultAbiCoder.encode( + [ + "tuple(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, bytes proof)", + ], + [{ header: blockHeader, proof: encodedProof }] + ); + + return signalProof; +} + +export { getSignalSlot, getSignalProof }; diff --git a/packages/protocol/test/utils/sleep.ts b/packages/protocol/test/utils/sleep.ts new file mode 100644 index 0000000000..268c312630 --- /dev/null +++ b/packages/protocol/test/utils/sleep.ts @@ -0,0 +1,7 @@ +function sleep(ms: number) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} + +export default sleep; diff --git a/packages/protocol/test/utils/taikoL1.ts b/packages/protocol/test/utils/taikoL1.ts new file mode 100644 index 0000000000..f6191c61a5 --- /dev/null +++ b/packages/protocol/test/utils/taikoL1.ts @@ -0,0 +1,60 @@ +import { BigNumber } from "ethers"; +import { ethers } from "hardhat"; +import { AddressManager, TaikoL1 } from "../../typechain"; + +const defaultFeeBase = BigNumber.from(10).pow(18); + +async function deployTaikoL1( + addressManager: AddressManager, + genesisHash: string, + enableTokenomics: boolean, + feeBase?: BigNumber +): Promise { + const libReceiptDecoder = await ( + await ethers.getContractFactory("LibReceiptDecoder") + ).deploy(); + + const libTxDecoder = await ( + await ethers.getContractFactory("LibTxDecoder") + ).deploy(); + + const libProposing = await ( + await ethers.getContractFactory("LibProposing") + ).deploy(); + + const libProving = await ( + await ethers.getContractFactory("LibProving", { + libraries: { + LibReceiptDecoder: libReceiptDecoder.address, + LibTxDecoder: libTxDecoder.address, + }, + }) + ).deploy(); + + const libVerifying = await ( + await ethers.getContractFactory("LibVerifying") + ).deploy(); + + const taikoL1 = await ( + await ethers.getContractFactory( + enableTokenomics ? "TestTaikoL1EnableTokenomics" : "TestTaikoL1", + { + libraries: { + LibVerifying: libVerifying.address, + LibProposing: libProposing.address, + LibProving: libProving.address, + }, + } + ) + ).deploy(); + + await taikoL1.init( + addressManager.address, + genesisHash, + feeBase ?? defaultFeeBase + ); + + return taikoL1 as TaikoL1; +} + +export { deployTaikoL1, defaultFeeBase }; diff --git a/packages/protocol/test/utils/taikoL2.ts b/packages/protocol/test/utils/taikoL2.ts new file mode 100644 index 0000000000..63fd0a833a --- /dev/null +++ b/packages/protocol/test/utils/taikoL2.ts @@ -0,0 +1,35 @@ +import { ethers } from "ethers"; +import { ethers as hardhatEthers } from "hardhat"; +import { AddressManager, TaikoL2 } from "../../typechain"; + +async function deployTaikoL2( + signer: ethers.Signer, + addressManager: AddressManager, + enablePublicInputsCheck: boolean = true +): Promise { + // Deploying TaikoL2 Contract linked with LibTxDecoder (throws error otherwise) + const l2LibTxDecoder = await ( + await hardhatEthers.getContractFactory("LibTxDecoder") + ) + .connect(signer) + .deploy(); + + const taikoL2 = await ( + await hardhatEthers.getContractFactory( + enablePublicInputsCheck + ? "TestTaikoL2EnablePublicInputsCheck" + : "TestTaikoL2", + { + libraries: { + LibTxDecoder: l2LibTxDecoder.address, + }, + } + ) + ) + .connect(signer) + .deploy(addressManager.address); + + return taikoL2 as TaikoL2; +} + +export { deployTaikoL2 }; diff --git a/packages/protocol/test/utils/tkoToken.ts b/packages/protocol/test/utils/tkoToken.ts new file mode 100644 index 0000000000..b59c467bc4 --- /dev/null +++ b/packages/protocol/test/utils/tkoToken.ts @@ -0,0 +1,25 @@ +import { ethers } from "ethers"; +import { ethers as hardhatEthers } from "hardhat"; +import { AddressManager } from "../../typechain"; + +const deployTkoToken = async ( + signer: ethers.Signer, + addressManager: AddressManager, + protoBroker: string +) => { + const token = await (await hardhatEthers.getContractFactory("TestTkoToken")) + .connect(signer) + .deploy(); + await token.init(addressManager.address); + + const network = await signer.provider?.getNetwork(); + + await addressManager.setAddress( + `${network?.chainId}.proto_broker`, + protoBroker + ); + + return token; +}; + +export default deployTkoToken; diff --git a/packages/protocol/test/utils/tokenomics.ts b/packages/protocol/test/utils/tokenomics.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/relayer/TaikoL1.json b/packages/relayer/TaikoL1.json index 9af8ad4746..d27319e81f 100644 --- a/packages/relayer/TaikoL1.json +++ b/packages/relayer/TaikoL1.json @@ -423,7 +423,7 @@ }, { "internalType": "uint64", - "name": "boostrapDiscountHalvingPeriod", + "name": "bootstrapDiscountHalvingPeriod", "type": "uint64" }, { diff --git a/packages/status-page/src/constants/abi/TaikoL1.ts b/packages/status-page/src/constants/abi/TaikoL1.ts index 421cb7fb58..b265ffdb68 100644 --- a/packages/status-page/src/constants/abi/TaikoL1.ts +++ b/packages/status-page/src/constants/abi/TaikoL1.ts @@ -423,7 +423,7 @@ export default [ }, { internalType: "uint64", - name: "boostrapDiscountHalvingPeriod", + name: "bootstrapDiscountHalvingPeriod", type: "uint64", }, { diff --git a/packages/website/docs/smart-contracts/L1/TaikoData.md b/packages/website/docs/smart-contracts/L1/TaikoData.md index 2e9969f0f4..6144b3f3de 100644 --- a/packages/website/docs/smart-contracts/L1/TaikoData.md +++ b/packages/website/docs/smart-contracts/L1/TaikoData.md @@ -27,7 +27,7 @@ struct Config { uint64 feeMaxPeriodPctg; uint64 blockTimeCap; uint64 proofTimeCap; - uint64 boostrapDiscountHalvingPeriod; + uint64 bootstrapDiscountHalvingPeriod; uint64 initialUncleDelay; bool enableTokenomics; }