UNPKG

@hyperlane-xyz/core

Version:

Core solidity contracts for Hyperlane

599 lines (544 loc) 18.4 kB
/* * Copyright 2019-2020, Offchain Labs, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* eslint-env node, mocha */ import { ethers } from 'hardhat' import { BigNumber } from '@ethersproject/bignumber' import { JsonRpcProvider, TransactionReceipt } from '@ethersproject/providers' import { expect } from 'chai' import { Bridge, Bridge__factory, GasRefunder__factory, Inbox, Inbox__factory, MessageTester, MessageTester__factory, RollupMock__factory, SequencerInbox__factory, TransparentUpgradeableProxy__factory, } from '../../build/types' import { applyAlias } from './utils' import { Event } from '@ethersproject/contracts' import { Interface } from '@ethersproject/abi' import { BridgeInterface, MessageDeliveredEvent, } from '../../build/types/src/bridge/Bridge' import { Signer, Wallet, constants, utils } from 'ethers' import { keccak256, parseEther, solidityKeccak256, solidityPack, } from 'ethers/lib/utils' import { Toolkit4844 } from './toolkit4844' import { SequencerInbox } from '../../build/types/src/bridge/SequencerInbox' import { InboxMessageDeliveredEvent } from '../../build/types/src/bridge/AbsInbox' import { SequencerBatchDeliveredEvent } from '../../build/types/src/bridge/ISequencerInbox' describe('SequencerInbox', async () => { const findMatchingLogs = <TInterface extends Interface, TEvent extends Event>( receipt: TransactionReceipt, iFace: TInterface, eventTopicGen: (i: TInterface) => string ): TEvent['args'][] => { const logs = receipt.logs.filter( log => log.topics[0] === eventTopicGen(iFace) ) return logs.map(l => iFace.parseLog(l).args as TEvent['args']) } const getMessageDeliveredEvents = (receipt: TransactionReceipt) => { const bridgeInterface = Bridge__factory.createInterface() return findMatchingLogs<BridgeInterface, MessageDeliveredEvent>( receipt, bridgeInterface, i => i.getEventTopic(i.getEvent('MessageDelivered')) ) } const sendDelayedTx = async ( sender: Signer, inbox: Inbox, bridge: Bridge, messageTester: MessageTester, l2Gas: number, l2GasPrice: number, nonce: number, destAddr: string, amount: BigNumber, data: string ) => { const countBefore = ( await bridge.functions.delayedMessageCount() )[0].toNumber() const sendUnsignedTx = await inbox .connect(sender) .sendUnsignedTransaction(l2Gas, l2GasPrice, nonce, destAddr, amount, data) const sendUnsignedTxReceipt = await sendUnsignedTx.wait() const countAfter = ( await bridge.functions.delayedMessageCount() )[0].toNumber() expect(countAfter, 'Unexpected inbox count').to.eq(countBefore + 1) const senderAddr = applyAlias(await sender.getAddress()) const messageDeliveredEvent = getMessageDeliveredEvents( sendUnsignedTxReceipt )[0] const l1BlockNumber = sendUnsignedTxReceipt.blockNumber const blockL1 = await sender.provider!.getBlock(l1BlockNumber) const baseFeeL1 = blockL1.baseFeePerGas!.toNumber() const l1BlockTimestamp = blockL1.timestamp const delayedAcc = await bridge.delayedInboxAccs(countBefore) // need to hex pad the address const messageDataHash = ethers.utils.solidityKeccak256( ['uint8', 'uint256', 'uint256', 'uint256', 'uint256', 'uint256', 'bytes'], [ 0, l2Gas, l2GasPrice, nonce, ethers.utils.hexZeroPad(destAddr, 32), amount, data, ] ) expect( messageDeliveredEvent.messageDataHash, 'Incorrect messageDataHash' ).to.eq(messageDataHash) const messageHash = ( await messageTester.functions.messageHash( 3, senderAddr, l1BlockNumber, l1BlockTimestamp, countBefore, baseFeeL1, messageDataHash ) )[0] const prevAccumulator = messageDeliveredEvent.beforeInboxAcc expect(prevAccumulator, 'Incorrect prev accumulator').to.eq( countBefore === 0 ? ethers.utils.hexZeroPad('0x', 32) : await bridge.delayedInboxAccs(countBefore - 1) ) const nextAcc = ( await messageTester.functions.accumulateInboxMessage( prevAccumulator, messageHash ) )[0] expect(delayedAcc, 'Incorrect delayed acc').to.eq(nextAcc) return { baseFeeL1: baseFeeL1, deliveredMessageEvent: messageDeliveredEvent, l1BlockNumber, l1BlockTimestamp, delayedAcc, l2Gas, l2GasPrice, nonce, destAddr, amount, data, senderAddr, inboxAccountLength: countAfter, } } const fundAccounts = async ( wallet: Wallet, length: number, amount: BigNumber ): Promise<Wallet[]> => { let key = wallet.privateKey const wallets: Wallet[] = [] for (let index = 0; index < length; index++) { key = keccak256(key) const nextWallet = new Wallet(key).connect(wallet.provider) if ((await nextWallet.getBalance()).lt(amount)) { await ( await wallet.sendTransaction({ to: nextWallet.address, value: amount, }) ).wait() } wallets.push(nextWallet) } return wallets } const setupSequencerInbox = async ( fundingWallet: Wallet, maxDelayBlocks = 10, maxDelayTime = 0 ) => { const accounts = await fundAccounts(fundingWallet, 5, utils.parseEther('1')) const admin = accounts[0] const adminAddr = await admin.getAddress() const user = accounts[1] const deployer = accounts[2] const rollupOwner = accounts[3] const batchPoster = accounts[4] // update the addresses below and uncomment to avoid redeploying // return connectAddreses(user, deployer, batchPoster, { // user: '0x870204e93ca485a6676E264EB0d7df4cD0246203', // bridge: '0x95491D63100cc7a21155247329007ca294fC752B', // inbox: '0x00eb941BD8B89E0396A983c870fa74DA4aC5ecFB', // sequencerInbox: '0x87fEe873425A65Bb2A11dFf6E15B4Ce25e7AFccD', // messageTester: '0x68BCf73c6b36ae3f20b2fD06c2d4651538Ae02a6', // batchPoster: '0x328375c90F01Dcb114888DA36e3832F69Ad0BB57', // gasRefunder: '0x33B1355B2F3BE116eB1c8226CF3B0a433259459C' // }) const rollupMockFac = new RollupMock__factory(deployer) const rollupMock = await rollupMockFac.deploy( await rollupOwner.getAddress() ) const reader4844 = await Toolkit4844.deployReader4844(fundingWallet) const sequencerInboxFac = new SequencerInbox__factory(deployer) const seqInboxTemplate = await sequencerInboxFac.deploy( 117964, reader4844.address, false ) const inboxFac = new Inbox__factory(deployer) const inboxTemplate = await inboxFac.deploy(117964) const bridgeFac = new Bridge__factory(deployer) const bridgeTemplate = await bridgeFac.deploy() await rollupMock.deployed() await inboxTemplate.deployed() await bridgeTemplate.deployed() await seqInboxTemplate.deployed() const transparentUpgradeableProxyFac = new TransparentUpgradeableProxy__factory(deployer) const bridgeProxy = await transparentUpgradeableProxyFac.deploy( bridgeTemplate.address, adminAddr, '0x' ) const sequencerInboxProxy = await transparentUpgradeableProxyFac.deploy( seqInboxTemplate.address, adminAddr, '0x' ) const inboxProxy = await transparentUpgradeableProxyFac.deploy( inboxTemplate.address, adminAddr, '0x' ) await bridgeProxy.deployed() await inboxProxy.deployed() await sequencerInboxProxy.deployed() const bridge = await bridgeFac.attach(bridgeProxy.address).connect(user) const bridgeAdmin = await bridgeFac .attach(bridgeProxy.address) .connect(rollupOwner) const sequencerInbox = await sequencerInboxFac .attach(sequencerInboxProxy.address) .connect(user) await (await bridgeAdmin.initialize(rollupMock.address)).wait() await ( await sequencerInbox.initialize(bridgeProxy.address, { delayBlocks: maxDelayBlocks, delaySeconds: maxDelayTime, futureBlocks: 10, futureSeconds: 3000, }) ).wait() const inbox = await inboxFac.attach(inboxProxy.address).connect(user) await ( await sequencerInbox .connect(rollupOwner) .setIsBatchPoster(await batchPoster.getAddress(), true) ).wait() await ( await inbox.initialize(bridgeProxy.address, sequencerInbox.address) ).wait() await (await bridgeAdmin.setDelayedInbox(inbox.address, true)).wait() await (await bridgeAdmin.setSequencerInbox(sequencerInbox.address)).wait() const messageTester = await new MessageTester__factory(deployer).deploy() await messageTester.deployed() const gasRefunderFac = new GasRefunder__factory(deployer) const gasRefunder = await gasRefunderFac.deploy() await gasRefunder.deployed() // fund the gas refunder await ( await deployer.sendTransaction({ to: gasRefunder.address, value: parseEther('0.2'), }) ).wait() await (await gasRefunder.allowContracts([sequencerInbox.address])).wait() await (await gasRefunder.allowRefundees([batchPoster.address])).wait() await (await gasRefunder.setExtraGasMargin(35000)).wait() const res = { user, bridge: bridge, inbox: inbox, sequencerInbox: sequencerInbox, messageTester, batchPoster, gasRefunder, } // comment this in to print the addresses that can then be re-used to avoid redeployment // let consoleRes: { [index: string]: string } = {} // Object.entries(res).forEach(r => (consoleRes[r[0]] = r[1].address)) // console.log(consoleRes) return res } it('can send normal batch', async () => { const privKey = 'cb5790da63720727af975f42c79f69918580209889225fa7128c92402a6d3a65' const prov = new JsonRpcProvider('http://127.0.0.1:8545') const wallet = new Wallet(privKey).connect(prov) const { user, inbox, bridge, messageTester, sequencerInbox, batchPoster, gasRefunder, } = await setupSequencerInbox(wallet) await sendDelayedTx( user, inbox, bridge, messageTester, 1000000, 21000000000, 0, await user.getAddress(), BigNumber.from(10), '0x1010' ) const subMessageCount = await bridge.sequencerReportedSubMessageCount() const balBefore = await batchPoster.getBalance() await ( await sequencerInbox .connect(batchPoster) .functions[ 'addSequencerL2BatchFromOrigin(uint256,bytes,uint256,address,uint256,uint256)' ]( await bridge.sequencerMessageCount(), '0x0042', await bridge.delayedMessageCount(), gasRefunder.address, subMessageCount, subMessageCount.add(1) ) ).wait() expect((await batchPoster.getBalance()).gt(balBefore), 'Refund not enough') }) it('can send blob batch', async () => { const privKey = 'cb5790da63720727af975f42c79f69918580209889225fa7128c92402a6d3a65' const prov = new JsonRpcProvider('http://127.0.0.1:8545') const wallet = new Wallet(privKey).connect(prov) const { user, inbox, bridge, messageTester, sequencerInbox, batchPoster, gasRefunder, } = await setupSequencerInbox(wallet) await sendDelayedTx( user, inbox, bridge, messageTester, 1000000, 21000000000, 0, await user.getAddress(), BigNumber.from(10), '0x1010' ) const subMessageCount = await bridge.sequencerReportedSubMessageCount() const afterDelayedMessagesRead = await bridge.delayedMessageCount() const sequenceNumber = await bridge.sequencerMessageCount() const balBefore = await batchPoster.getBalance() const txHash = await Toolkit4844.sendBlobTx( batchPoster.privateKey.substring(2), sequencerInbox.address, ['0x0142', '0x0143'], sequencerInbox.interface.encodeFunctionData( 'addSequencerL2BatchFromBlobs', [ sequenceNumber, afterDelayedMessagesRead, gasRefunder.address, subMessageCount, subMessageCount.add(1), ] ) ) expect((await batchPoster.getBalance()).gt(balBefore), 'Refund not enough') const batchSendTx = await Toolkit4844.getTx(txHash) const blobHashes = (batchSendTx as any)['blobVersionedHashes'] as string[] const batchSendReceipt = await Toolkit4844.getTxReceipt(txHash) const { timestamp: blockTimestamp, number: blockNumber, baseFeePerGas, } = await wallet.provider.getBlock(batchSendReceipt.blockNumber) const timeBounds = await getTimeBounds( blockNumber, blockTimestamp, sequencerInbox ) const dataHash = formDataBlobHash( timeBounds, afterDelayedMessagesRead.toNumber(), blobHashes ) const batchDeliveredEvent = batchSendReceipt.logs .filter( (b: any) => b.address.toLowerCase() === sequencerInbox.address.toLowerCase() && b.topics[0] === sequencerInbox.interface.getEventTopic('SequencerBatchDelivered') ) .map( (l: any) => sequencerInbox.interface.parseLog(l).args )[0] as SequencerBatchDeliveredEvent['args'] if (!batchDeliveredEvent) throw new Error('missing batch event') const seqMessageCountAfter = ( await bridge.sequencerMessageCount() ).toNumber() const delayedMessageCountAfter = ( await bridge.delayedMessageCount() ).toNumber() // -2 since we add a message to the from the sequencer inbox const beforeAcc = seqMessageCountAfter > 1 ? await bridge.sequencerInboxAccs(seqMessageCountAfter - 2) : constants.HashZero expect(batchDeliveredEvent.beforeAcc, 'before acc').to.eq(beforeAcc) // -2 since we add the batch spending report const delayedAcc = delayedMessageCountAfter > 0 ? await bridge.delayedInboxAccs(delayedMessageCountAfter - 2) : constants.HashZero expect(batchDeliveredEvent.delayedAcc, 'delayed acc').to.eq(delayedAcc) const afterAcc = solidityKeccak256( ['bytes32', 'bytes32', 'bytes32'], [beforeAcc, dataHash, delayedAcc] ) expect(batchDeliveredEvent.afterAcc, 'after acc').to.eq(afterAcc) // check the spending report was submitted const inboxMsgDeliveredEvent = batchSendReceipt.logs .filter( (b: any) => b.address.toLowerCase() === sequencerInbox.address.toLowerCase() && b.topics[0] === sequencerInbox.interface.getEventTopic('InboxMessageDelivered') ) .map( (l: any) => sequencerInbox.interface.parseLog(l).args )[0] as InboxMessageDeliveredEvent['args'] const spendingTimestamp = '0x' + inboxMsgDeliveredEvent.data.substring(2, 66) const spendingBatchPoster = '0x' + inboxMsgDeliveredEvent.data.substring(66, 106) const spendingDataHash = '0x' + inboxMsgDeliveredEvent.data.substring(106, 170) const spendingSeqMessageIndex = '0x' + inboxMsgDeliveredEvent.data.substring(170, 234) const spendingBlockBaseFee = '0x' + inboxMsgDeliveredEvent.data.substring(234, 298) const spendingExtraGas = '0x' + inboxMsgDeliveredEvent.data.substring(298, 314) expect( BigNumber.from(spendingTimestamp).eq(blockTimestamp), 'spending timestamp' ).to.eq(true) expect(spendingBatchPoster.toLowerCase(), 'spending batch poster').to.eq( (await batchPoster.getAddress()).toLowerCase() ) expect(spendingDataHash, 'spending data hash').to.eq(dataHash) expect( BigNumber.from(spendingSeqMessageIndex).eq(sequenceNumber), 'spending seq message index' ).to.eq(true) if (baseFeePerGas == null) { throw new Error('Missing base fee') } expect( BigNumber.from(spendingBlockBaseFee).eq(baseFeePerGas), `spending basefee: ${BigNumber.from(spendingBlockBaseFee).toString()}` ).to.eq(true) expect( BigNumber.from(spendingExtraGas).gt(0), // blob spending is normalized into extra gas `spending extra gas: ${BigNumber.from(spendingExtraGas).toString()}` ).to.eq(true) }) const getTimeBounds = async ( blockNumber: number, blockTimestamp: number, sequencerInbox: SequencerInbox ): Promise<{ maxBlock: number minBlocks: number minTimestamp: number maxTimestamp: number }> => { const [delayBlocks, futureBlocks, delaySeconds, futureSeconds] = await sequencerInbox.maxTimeVariation() return { minBlocks: blockNumber > delayBlocks.toNumber() ? blockNumber - delayBlocks.toNumber() : 0, maxBlock: blockNumber + futureBlocks.toNumber(), minTimestamp: blockTimestamp > delaySeconds.toNumber() ? blockTimestamp - delaySeconds.toNumber() : 0, maxTimestamp: blockTimestamp + futureSeconds.toNumber(), } } const formDataBlobHash = ( timeBounds: { maxBlock: number minBlocks: number minTimestamp: number maxTimestamp: number }, afterDelayedMessagesRead: number, blobHashes: string[] ) => { const header = solidityPack( ['uint64', 'uint64', 'uint64', 'uint64', 'uint64'], [ timeBounds.minTimestamp, timeBounds.maxTimestamp, timeBounds.minBlocks, timeBounds.maxBlock, afterDelayedMessagesRead, ] ) return keccak256( solidityPack( ['bytes', 'bytes', 'bytes'], [ header, Toolkit4844.DATA_BLOB_HEADER_FLAG, solidityPack(['bytes32[]'], [blobHashes]), ] ) ) } })