diff --git a/packages/rollup-contracts/contracts/CanonicalTransactionChain.sol b/packages/rollup-contracts/contracts/CanonicalTransactionChain.sol index 3dc842adcc55b..23648f53b098f 100644 --- a/packages/rollup-contracts/contracts/CanonicalTransactionChain.sol +++ b/packages/rollup-contracts/contracts/CanonicalTransactionChain.sol @@ -5,12 +5,14 @@ pragma experimental ABIEncoderV2; import {DataTypes as dt} from "./DataTypes.sol"; import {RollupMerkleUtils} from "./RollupMerkleUtils.sol"; import {L1ToL2TransactionQueue} from "./L1ToL2TransactionQueue.sol"; +import {SafetyTransactionQueue} from "./SafetyTransactionQueue.sol"; contract CanonicalTransactionChain { address public sequencer; uint public forceInclusionPeriod; RollupMerkleUtils public merkleUtils; L1ToL2TransactionQueue public l1ToL2Queue; + SafetyTransactionQueue public safetyQueue; uint public cumulativeNumElements; bytes32[] public batches; uint public lastOVMTimestamp; @@ -24,6 +26,7 @@ contract CanonicalTransactionChain { merkleUtils = RollupMerkleUtils(_rollupMerkleUtilsAddress); sequencer = _sequencer; l1ToL2Queue = new L1ToL2TransactionQueue(_rollupMerkleUtilsAddress, _l1ToL2TransactionPasserAddress, address(this)); + safetyQueue = new SafetyTransactionQueue(_rollupMerkleUtilsAddress, address(this)); forceInclusionPeriod =_forceInclusionPeriod; lastOVMTimestamp = 0; } @@ -49,34 +52,60 @@ contract CanonicalTransactionChain { } function appendL1ToL2Batch() public { - dt.TimestampedHash memory timestampedHash = l1ToL2Queue.peek(); + dt.TimestampedHash memory l1ToL2Header = l1ToL2Queue.peek(); + require( + safetyQueue.isEmpty() || l1ToL2Header.timestamp <= safetyQueue.peekTimestamp(), + "Must process older SafetyQueue batches first to enforce timestamp monotonicity" + ); + _appendQueueBatch(l1ToL2Header, true); + l1ToL2Queue.dequeue(); + } + + function appendSafetyBatch() public { + dt.TimestampedHash memory safetyHeader = safetyQueue.peek(); + require( + l1ToL2Queue.isEmpty() || safetyHeader.timestamp <= l1ToL2Queue.peekTimestamp(), + "Must process older L1ToL2Queue batches first to enforce timestamp monotonicity" + ); + _appendQueueBatch(safetyHeader, false); + safetyQueue.dequeue(); + } + + function _appendQueueBatch( + dt.TimestampedHash memory timestampedHash, + bool isL1ToL2Tx + ) internal { uint timestamp = timestampedHash.timestamp; - if (timestamp + forceInclusionPeriod > now) { - require(authenticateAppend(msg.sender), "Message sender does not have permission to append this batch"); - } + require( + timestamp + forceInclusionPeriod <= now || authenticateAppend(msg.sender), + "Message sender does not have permission to append this batch" + ); lastOVMTimestamp = timestamp; bytes32 elementsMerkleRoot = timestampedHash.txHash; uint numElementsInBatch = 1; bytes32 batchHeaderHash = keccak256(abi.encodePacked( timestamp, - true, // isL1ToL2Tx + isL1ToL2Tx, elementsMerkleRoot, numElementsInBatch, cumulativeNumElements // cumulativePrevElements )); batches.push(batchHeaderHash); cumulativeNumElements += numElementsInBatch; - l1ToL2Queue.dequeue(); } - function appendTransactionBatch(bytes[] memory _txBatch, uint _timestamp) public { + function appendSequencerBatch(bytes[] memory _txBatch, uint _timestamp) public { require(authenticateAppend(msg.sender), "Message sender does not have permission to append a batch"); require(_txBatch.length > 0, "Cannot submit an empty batch"); require(_timestamp + forceInclusionPeriod > now, "Cannot submit a batch with a timestamp older than the sequencer inclusion period"); require(_timestamp <= now, "Cannot submit a batch with a timestamp in the future"); - if(!l1ToL2Queue.isEmpty()) { - require(_timestamp <= l1ToL2Queue.peekTimestamp(), "Must process older queued batches first to enforce timestamp monotonicity"); - } + require( + l1ToL2Queue.isEmpty() || _timestamp <= l1ToL2Queue.peekTimestamp(), + "Must process older L1ToL2Queue batches first to enforce timestamp monotonicity" + ); + require( + safetyQueue.isEmpty() || _timestamp <= safetyQueue.peekTimestamp(), + "Must process older SafetyQueue batches first to enforce timestamp monotonicity"); require(_timestamp >= lastOVMTimestamp, "Timestamps must monotonically increase"); lastOVMTimestamp = _timestamp; bytes32 batchHeaderHash = keccak256(abi.encodePacked( @@ -94,7 +123,7 @@ contract CanonicalTransactionChain { function verifyElement( bytes memory _element, // the element of the list being proven uint _position, // the position in the list of the element being proven - dt.ElementInclusionProof memory _inclusionProof // inclusion proof in the rollup batch + dt.TxElementInclusionProof memory _inclusionProof // inclusion proof in the rollup batch ) public view returns (bool) { // For convenience, store the batchHeader dt.TxChainBatchHeader memory batchHeader = _inclusionProof.batchHeader; diff --git a/packages/rollup-contracts/contracts/DataTypes.sol b/packages/rollup-contracts/contracts/DataTypes.sol index 6dab7c9d0c56c..353e91858d19c 100644 --- a/packages/rollup-contracts/contracts/DataTypes.sol +++ b/packages/rollup-contracts/contracts/DataTypes.sol @@ -34,11 +34,24 @@ contract DataTypes { address l1MessageSender; } - struct ElementInclusionProof { - uint batchIndex; // index in batches array (first batch has batchNumber of 0) + struct TxElementInclusionProof { + uint batchIndex; TxChainBatchHeader batchHeader; - uint indexInBatch; // used to verify inclusion of the element in elementsMerkleRoot - bytes32[] siblings; // used to verify inclusion of the element in elementsMerkleRoot + uint indexInBatch; + bytes32[] siblings; + } + + struct StateElementInclusionProof { + uint batchIndex; + StateChainBatchHeader batchHeader; + uint indexInBatch; + bytes32[] siblings; + } + + struct StateChainBatchHeader { + bytes32 elementsMerkleRoot; + uint numElementsInBatch; + uint cumulativePrevElements; } struct TxChainBatchHeader { diff --git a/packages/rollup-contracts/contracts/SafetyTransactionQueue.sol b/packages/rollup-contracts/contracts/SafetyTransactionQueue.sol new file mode 100644 index 0000000000000..3e684a95f0c4d --- /dev/null +++ b/packages/rollup-contracts/contracts/SafetyTransactionQueue.sol @@ -0,0 +1,20 @@ +pragma solidity ^0.5.0; +pragma experimental ABIEncoderV2; + +/* Internal Imports */ +import {RollupQueue} from "./RollupQueue.sol"; + +contract SafetyTransactionQueue is RollupQueue { + address public canonicalTransactionChain; + + constructor( + address _rollupMerkleUtilsAddress, + address _canonicalTransactionChain + ) RollupQueue(_rollupMerkleUtilsAddress) public { + canonicalTransactionChain = _canonicalTransactionChain; + } + + function authenticateDequeue(address _sender) public view returns (bool) { + return _sender == canonicalTransactionChain; + } +} diff --git a/packages/rollup-contracts/contracts/StateCommitmentChain.sol b/packages/rollup-contracts/contracts/StateCommitmentChain.sol new file mode 100644 index 0000000000000..d05e4172e910f --- /dev/null +++ b/packages/rollup-contracts/contracts/StateCommitmentChain.sol @@ -0,0 +1,84 @@ +pragma solidity ^0.5.0; +pragma experimental ABIEncoderV2; + +/* Internal Imports */ +import {DataTypes as dt} from "./DataTypes.sol"; +import {RollupMerkleUtils} from "./RollupMerkleUtils.sol"; +import {CanonicalTransactionChain} from "./CanonicalTransactionChain.sol"; + +contract StateCommitmentChain { + CanonicalTransactionChain canonicalTransactionChain; + RollupMerkleUtils public merkleUtils; + address public fraudVerifier; + uint public cumulativeNumElements; + bytes32[] public batches; + + constructor( + address _rollupMerkleUtilsAddress, + address _canonicalTransactionChain, + address _fraudVerifier + ) public { + merkleUtils = RollupMerkleUtils(_rollupMerkleUtilsAddress); + canonicalTransactionChain = CanonicalTransactionChain(_canonicalTransactionChain); + fraudVerifier = _fraudVerifier; + } + + function getBatchesLength() public view returns (uint) { + return batches.length; + } + + function hashBatchHeader( + dt.StateChainBatchHeader memory _batchHeader + ) public pure returns (bytes32) { + return keccak256(abi.encodePacked( + _batchHeader.elementsMerkleRoot, + _batchHeader.numElementsInBatch, + _batchHeader.cumulativePrevElements + )); + } + + function appendStateBatch(bytes[] memory _stateBatch) public { + require(cumulativeNumElements + _stateBatch.length <= canonicalTransactionChain.cumulativeNumElements(), + "Cannot append more state commitments than total number of transactions in CanonicalTransactionChain"); + require(_stateBatch.length > 0, "Cannot submit an empty state commitment batch"); + bytes32 batchHeaderHash = keccak256(abi.encodePacked( + merkleUtils.getMerkleRoot(_stateBatch), // elementsMerkleRoot + _stateBatch.length, // numElementsInBatch + cumulativeNumElements // cumulativeNumElements + )); + batches.push(batchHeaderHash); + cumulativeNumElements += _stateBatch.length; + } + + // verifies an element is in the current list at the given position + function verifyElement( + bytes memory _element, // the element of the list being proven + uint _position, // the position in the list of the element being proven + dt.StateElementInclusionProof memory _inclusionProof + ) public view returns (bool) { + dt.StateChainBatchHeader memory batchHeader = _inclusionProof.batchHeader; + if(_position != _inclusionProof.indexInBatch + + batchHeader.cumulativePrevElements) + return false; + if (!merkleUtils.verify( + batchHeader.elementsMerkleRoot, + _element, + _inclusionProof.indexInBatch, + _inclusionProof.siblings + )) return false; + //compare computed batch header with the batch header in the list. + return hashBatchHeader(batchHeader) == batches[_inclusionProof.batchIndex]; + } + + function deleteAfterInclusive( + uint _batchIndex, + dt.StateChainBatchHeader memory _batchHeader + ) public { + require(msg.sender == fraudVerifier, "Only FraudVerifier has permission to delete state batches"); + require(_batchIndex < batches.length, "Cannot delete batches outside of valid range"); + bytes32 calculatedBatchHeaderHash = hashBatchHeader(_batchHeader); + require(calculatedBatchHeaderHash == batches[_batchIndex], "Calculated batch header is different than expected batch header"); + batches.length = _batchIndex; + cumulativeNumElements = _batchHeader.cumulativePrevElements; + } +} diff --git a/packages/rollup-contracts/test/helpers/index.ts b/packages/rollup-contracts/test/helpers/index.ts index d4e3db3ed6664..81614d8539ae8 100644 --- a/packages/rollup-contracts/test/helpers/index.ts +++ b/packages/rollup-contracts/test/helpers/index.ts @@ -1,11 +1,3 @@ -/* Imports */ -import { - keccak256, - abi, - hexStrToBuf, - bufToHexString, -} from '@eth-optimism/core-utils' - /********************************** * Byte String Generation Helpers * *********************************/ @@ -18,74 +10,14 @@ export function makeRepeatedBytes(value: string, length: number): string { return '0x' + sliced } -// Make padded bytes. Bytes are right padded. -export function makePaddedBytes(value: string, length: number): string { - if (value.length > length * 2) { - throw new Error('Value too large to fit in ' + length + ' byte string') - } - const targetLength = length * 2 - while (value.length < (targetLength || 2)) { - value = value + '0' +export function makeRandomBlockOfSize(blockSize: number): string[] { + const block = [] + for (let i = 0; i < blockSize; i++) { + block.push(makeRepeatedBytes('' + Math.floor(Math.random() * 500 + 1), 32)) } - return '0x' + value + return block } -// Make a padded uint. Uints are left padded. -export function makePaddedUint(value: string, length: number): string { - if (value.length > length * 2) { - throw new Error('Value too large to fit in ' + length + ' byte string') - } - const targetLength = length * 2 - while (value.length < (targetLength || 2)) { - value = '0' + value - } - return '0x' + value +export function makeRandomBatchOfSize(batchSize: number): string[] { + return makeRandomBlockOfSize(batchSize) } - -/******************************* - * Transition Encoding Helpers * - ******************************/ -// export type Transition = string - -// // Generates some number of dummy transitions -// export function generateNTransitions( -// numTransitions: number -// ): RollupTransition[] { -// const transitions = [] -// for (let i = 0; i < numTransitions; i++) { -// const transfer: TransferTransition = { -// stateRoot: getStateRoot('ab'), -// senderSlotIndex: 2, -// recipientSlotIndex: 2, -// tokenType: 0, -// amount: 1, -// signature: getSignature('01'), -// } -// transitions.push(transfer) -// } -// return transitions -// } - -/**************** - * Misc Helpers * - ***************/ - -export const ZERO_BYTES32 = makeRepeatedBytes('0', 32) -export const ZERO_ADDRESS = makeRepeatedBytes('0', 20) -export const ZERO_UINT32 = makeRepeatedBytes('0', 4) -export const ZERO_SIGNATURE = makeRepeatedBytes('0', 65) - -/* Extra Helpers */ -export const STORAGE_TREE_HEIGHT = 5 -export const AMOUNT_BYTES = 5 -export const getSlot = (storageSlot: string) => - makePaddedUint(storageSlot, STORAGE_TREE_HEIGHT) -export const getAmount = (amount: string) => - makePaddedUint(amount, AMOUNT_BYTES) -export const getAddress = (address: string) => makeRepeatedBytes(address, 20) -export const getSignature = (sig: string) => makeRepeatedBytes(sig, 65) -export const getStateRoot = (bytes: string) => makeRepeatedBytes(bytes, 32) -export const getBytes32 = (bytes: string) => makeRepeatedBytes(bytes, 32) - -export const UNISWAP_ADDRESS = getAddress('00') -export const UNISWAP_STORAGE_SLOT = 0 diff --git a/packages/rollup-contracts/test/merklization/RollupMerkleUtils.spec.ts b/packages/rollup-contracts/test/merklization/RollupMerkleUtils.spec.ts index 8656fab3ecb50..6eb952f7b16ac 100644 --- a/packages/rollup-contracts/test/merklization/RollupMerkleUtils.spec.ts +++ b/packages/rollup-contracts/test/merklization/RollupMerkleUtils.spec.ts @@ -1,7 +1,7 @@ import '../setup' /* Internal Imports */ -import { makeRepeatedBytes } from '../helpers' +import { makeRepeatedBytes, makeRandomBlockOfSize } from '../helpers' /* External Imports */ import { newInMemoryDB, SparseMerkleTreeImpl } from '@eth-optimism/core-db' @@ -36,14 +36,6 @@ async function getNewSMT(treeHeight: number): Promise { return SparseMerkleTreeImpl.create(newInMemoryDB(), undefined, treeHeight) } -function makeRandomBlockOfSize(blockSize: number): string[] { - const block = [] - for (let i = 0; i < blockSize; i++) { - block.push(makeRepeatedBytes('' + Math.floor(Math.random() * 500 + 1), 32)) - } - return block -} - /* Begin tests */ describe('RollupMerkleUtils', () => { const provider = createMockProvider() diff --git a/packages/rollup-contracts/test/rollup-list/CanonicalTransactionChain.spec.ts b/packages/rollup-contracts/test/rollup-list/CanonicalTransactionChain.spec.ts index e511d6a88341b..aa0d5d155040b 100644 --- a/packages/rollup-contracts/test/rollup-list/CanonicalTransactionChain.spec.ts +++ b/packages/rollup-contracts/test/rollup-list/CanonicalTransactionChain.spec.ts @@ -6,7 +6,8 @@ import { createMockProvider, deployContract, getWallets } from 'ethereum-waffle' import { Contract } from 'ethers' /* Internal Imports */ -import { DefaultRollupBatch, RollupQueueBatch } from './RLhelper' +import { TxChainBatch, TxQueueBatch } from './RLhelper' +import { makeRandomBatchOfSize } from '../helpers' /* Logging */ const log = getLogger('canonical-tx-chain', true) @@ -14,42 +15,40 @@ const log = getLogger('canonical-tx-chain', true) /* Contract Imports */ import * as CanonicalTransactionChain from '../../build/CanonicalTransactionChain.json' import * as L1ToL2TransactionQueue from '../../build/L1ToL2TransactionQueue.json' +import * as SafetyTransactionQueue from '../../build/SafetyTransactionQueue.json' import * as RollupMerkleUtils from '../../build/RollupMerkleUtils.json' /* Begin tests */ describe('CanonicalTransactionChain', () => { const provider = createMockProvider() - const [ - wallet, - sequencer, - canonicalTransactionChain, - l1ToL2TransactionPasser, - ] = getWallets(provider) + const [wallet, sequencer, l1ToL2TransactionPasser, randomWallet] = getWallets( + provider + ) let canonicalTxChain let rollupMerkleUtils let l1ToL2Queue - const localL1ToL2Queue = [] - const LIVENESS_ASSUMPTION = 600 //600 seconds = 10 minutes + let safetyQueue + const FORCE_INCLUSION_PERIOD = 600 //600 seconds = 10 minutes const DEFAULT_BATCH = ['0x1234', '0x5678'] const DEFAULT_TX = '0x1234' - const appendBatch = async (batch: string[]): Promise => { + const appendSequencerBatch = async (batch: string[]): Promise => { const timestamp = Math.floor(Date.now() / 1000) // Submit the rollup batch on-chain await canonicalTxChain .connect(sequencer) - .appendTransactionBatch(batch, timestamp) + .appendSequencerBatch(batch, timestamp) return timestamp } - const appendAndGenerateBatch = async ( + const appendAndGenerateSequencerBatch = async ( batch: string[], batchIndex: number = 0, cumulativePrevElements: number = 0 - ): Promise => { - const timestamp = await appendBatch(batch) + ): Promise => { + const timestamp = await appendSequencerBatch(batch) // Generate a local version of the rollup batch - const localBatch = new DefaultRollupBatch( + const localBatch = new TxChainBatch( timestamp, false, batchIndex, @@ -60,17 +59,32 @@ describe('CanonicalTransactionChain', () => { return localBatch } - const enqueueAndGenerateBatch = async ( + const enqueueAndGenerateL1ToL2Batch = async ( _tx: string - ): Promise => { + ): Promise => { // Submit the rollup batch on-chain const enqueueTx = await l1ToL2Queue .connect(l1ToL2TransactionPasser) .enqueueTx(_tx) - const txReceipt = await provider.getTransactionReceipt(enqueueTx.hash) + const localBatch = await generateQueueBatch(_tx, enqueueTx.hash) + return localBatch + } + const enqueueAndGenerateSafetyBatch = async ( + _tx: string + ): Promise => { + const enqueueTx = await safetyQueue.connect(randomWallet).enqueueTx(_tx) + const localBatch = await generateQueueBatch(_tx, enqueueTx.hash) + return localBatch + } + + const generateQueueBatch = async ( + _tx: string, + _txHash: string + ): Promise => { + const txReceipt = await provider.getTransactionReceipt(_txHash) const timestamp = (await provider.getBlock(txReceipt.blockNumber)).timestamp // Generate a local version of the rollup batch - const localBatch = new RollupQueueBatch(_tx, timestamp) + const localBatch = new TxQueueBatch(_tx, timestamp) await localBatch.generateTree() return localBatch } @@ -91,23 +105,31 @@ describe('CanonicalTransactionChain', () => { rollupMerkleUtils.address, sequencer.address, l1ToL2TransactionPasser.address, - LIVENESS_ASSUMPTION, + FORCE_INCLUSION_PERIOD, ], { gasLimit: 6700000, } ) + const l1ToL2QueueAddress = await canonicalTxChain.l1ToL2Queue() l1ToL2Queue = new Contract( l1ToL2QueueAddress, L1ToL2TransactionQueue.abi, provider ) + + const safetyQueueAddress = await canonicalTxChain.safetyQueue() + safetyQueue = new Contract( + safetyQueueAddress, + SafetyTransactionQueue.abi, + provider + ) }) - describe('appendTransactionBatch()', async () => { + describe('appendSequencerBatch()', async () => { it('should not throw when appending a batch from the sequencer', async () => { - await appendBatch(DEFAULT_BATCH) + await appendSequencerBatch(DEFAULT_BATCH) }) it('should throw if submitting an empty batch', async () => { @@ -115,99 +137,95 @@ describe('CanonicalTransactionChain', () => { await TestUtils.assertRevertsAsync( 'Cannot submit an empty batch', async () => { - await appendBatch(emptyBatch) + await appendSequencerBatch(emptyBatch) } ) }) it('should revert if submitting a batch older than the inclusion period', async () => { const timestamp = Math.floor(Date.now() / 1000) - const oldTimestamp = timestamp - (LIVENESS_ASSUMPTION + 1) + const oldTimestamp = timestamp - (FORCE_INCLUSION_PERIOD + 1) await TestUtils.assertRevertsAsync( 'Cannot submit a batch with a timestamp older than the sequencer inclusion period', async () => { await canonicalTxChain .connect(sequencer) - .appendTransactionBatch(DEFAULT_BATCH, oldTimestamp) + .appendSequencerBatch(DEFAULT_BATCH, oldTimestamp) } ) }) it('should not revert if submitting a 5 minute old batch', async () => { const timestamp = Math.floor(Date.now() / 1000) - const oldTimestamp = timestamp - LIVENESS_ASSUMPTION / 2 + const oldTimestamp = timestamp - FORCE_INCLUSION_PERIOD / 2 await canonicalTxChain .connect(sequencer) - .appendTransactionBatch(DEFAULT_BATCH, oldTimestamp) + .appendSequencerBatch(DEFAULT_BATCH, oldTimestamp) }) it('should revert if submitting a batch with a future timestamp', async () => { const timestamp = Math.floor(Date.now() / 1000) const futureTimestamp = timestamp + 100 - await TestUtils.assertRevertsAsync( 'Cannot submit a batch with a timestamp in the future', async () => { await canonicalTxChain .connect(sequencer) - .appendTransactionBatch(DEFAULT_BATCH, futureTimestamp) + .appendSequencerBatch(DEFAULT_BATCH, futureTimestamp) } ) }) - it('should revert if submitting a new batch with a timestamp less than latest batch timestamp', async () => { - const timestamp = await appendBatch(DEFAULT_BATCH) + it('should revert if submitting a new batch with a timestamp older than last batch timestamp', async () => { + const timestamp = await appendSequencerBatch(DEFAULT_BATCH) const oldTimestamp = timestamp - 1 - await TestUtils.assertRevertsAsync( 'Timestamps must monotonically increase', async () => { await canonicalTxChain .connect(sequencer) - .appendTransactionBatch(DEFAULT_BATCH, oldTimestamp) + .appendSequencerBatch(DEFAULT_BATCH, oldTimestamp) } ) }) it('should add to batches array', async () => { - await appendBatch(DEFAULT_BATCH) + await appendSequencerBatch(DEFAULT_BATCH) const batchesLength = await canonicalTxChain.getBatchesLength() batchesLength.toNumber().should.equal(1) }) it('should update cumulativeNumElements correctly', async () => { - await appendBatch(DEFAULT_BATCH) + await appendSequencerBatch(DEFAULT_BATCH) const cumulativeNumElements = await canonicalTxChain.cumulativeNumElements.call() cumulativeNumElements.toNumber().should.equal(DEFAULT_BATCH.length) }) - it('should not allow appendTransactionBatch from non-sequencer', async () => { + it('should not allow appendSequencerBatch from non-sequencer', async () => { const timestamp = Math.floor(Date.now() / 1000) - await TestUtils.assertRevertsAsync( 'Message sender does not have permission to append a batch', async () => { - await canonicalTxChain.appendTransactionBatch( - DEFAULT_BATCH, - timestamp - ) + await canonicalTxChain.appendSequencerBatch(DEFAULT_BATCH, timestamp) } ) }) it('should calculate batchHeaderHash correctly', async () => { - const localBatch = await appendAndGenerateBatch(DEFAULT_BATCH) + const localBatch = await appendAndGenerateSequencerBatch(DEFAULT_BATCH) const expectedBatchHeaderHash = await localBatch.hashBatchHeader() const calculatedBatchHeaderHash = await canonicalTxChain.batches(0) calculatedBatchHeaderHash.should.equal(expectedBatchHeaderHash) }) it('should add multiple batches correctly', async () => { - const numBatchs = 10 - for (let batchIndex = 0; batchIndex < numBatchs; batchIndex++) { - const cumulativePrevElements = DEFAULT_BATCH.length * batchIndex - const localBatch = await appendAndGenerateBatch( - DEFAULT_BATCH, + const numBatches = 5 + let expectedNumElements = 0 + for (let batchIndex = 0; batchIndex < numBatches; batchIndex++) { + const batch = makeRandomBatchOfSize(batchIndex + 1) + const cumulativePrevElements = expectedNumElements + const localBatch = await appendAndGenerateSequencerBatch( + batch, batchIndex, cumulativePrevElements ) @@ -216,41 +234,188 @@ describe('CanonicalTransactionChain', () => { batchIndex ) calculatedBatchHeaderHash.should.equal(expectedBatchHeaderHash) + expectedNumElements += batch.length } const cumulativeNumElements = await canonicalTxChain.cumulativeNumElements.call() - cumulativeNumElements - .toNumber() - .should.equal(numBatchs * DEFAULT_BATCH.length) + cumulativeNumElements.toNumber().should.equal(expectedNumElements) const batchesLength = await canonicalTxChain.getBatchesLength() - batchesLength.toNumber().should.equal(numBatchs) + batchesLength.toNumber().should.equal(numBatches) }) - describe('when the l1ToL2Queue is not empty', async () => { + + describe('when there is a batch in the L1toL2Queue', async () => { let localBatch beforeEach(async () => { - localBatch = await enqueueAndGenerateBatch(DEFAULT_TX) + localBatch = await enqueueAndGenerateL1ToL2Batch(DEFAULT_TX) }) it('should succesfully append a batch with an older timestamp', async () => { const oldTimestamp = localBatch.timestamp - 1 await canonicalTxChain .connect(sequencer) - .appendTransactionBatch(DEFAULT_BATCH, oldTimestamp) + .appendSequencerBatch(DEFAULT_BATCH, oldTimestamp) }) it('should succesfully append a batch with an equal timestamp', async () => { await canonicalTxChain .connect(sequencer) - .appendTransactionBatch(DEFAULT_BATCH, localBatch.timestamp) + .appendSequencerBatch(DEFAULT_BATCH, localBatch.timestamp) }) - it('should revert when appending a block with a newer timestamp', async () => { - const newTimestamp = localBatch.timestamp + 1 + it('should revert when there is an older batch in the L1ToL2Queue', async () => { + const snapshotID = await provider.send('evm_snapshot', []) + await provider.send('evm_increaseTime', [FORCE_INCLUSION_PERIOD]) + const newTimestamp = localBatch.timestamp + 60 await TestUtils.assertRevertsAsync( - 'Cannot submit a batch with a timestamp in the future', + 'Must process older L1ToL2Queue batches first to enforce timestamp monotonicity', async () => { await canonicalTxChain .connect(sequencer) - .appendTransactionBatch(DEFAULT_BATCH, newTimestamp) + .appendSequencerBatch(DEFAULT_BATCH, newTimestamp) + } + ) + await provider.send('evm_revert', [snapshotID]) + }) + }) + + describe('when there is a batch in the SafetyQueue', async () => { + let localBatch + beforeEach(async () => { + localBatch = await enqueueAndGenerateSafetyBatch(DEFAULT_TX) + }) + + it('should succesfully append a batch with an older timestamp', async () => { + const oldTimestamp = localBatch.timestamp - 1 + await canonicalTxChain + .connect(sequencer) + .appendSequencerBatch(DEFAULT_BATCH, oldTimestamp) + }) + + it('should succesfully append a batch with an equal timestamp', async () => { + await canonicalTxChain + .connect(sequencer) + .appendSequencerBatch(DEFAULT_BATCH, localBatch.timestamp) + }) + + it('should revert when there is an older batch in the SafetyQueue', async () => { + const snapshotID = await provider.send('evm_snapshot', []) + await provider.send('evm_increaseTime', [FORCE_INCLUSION_PERIOD]) + const newTimestamp = localBatch.timestamp + 60 + await TestUtils.assertRevertsAsync( + 'Must process older SafetyQueue batches first to enforce timestamp monotonicity', + async () => { + await canonicalTxChain + .connect(sequencer) + .appendSequencerBatch(DEFAULT_BATCH, newTimestamp) + } + ) + await provider.send('evm_revert', [snapshotID]) + }) + }) + describe('when there is an old batch in the safetyQueue and a recent batch in the l1ToL2Queue', async () => { + let safetyTimestamp + let l1ToL2Timestamp + let snapshotID + beforeEach(async () => { + const localSafetyBatch = await enqueueAndGenerateSafetyBatch(DEFAULT_TX) + safetyTimestamp = localSafetyBatch.timestamp + snapshotID = await provider.send('evm_snapshot', []) + await provider.send('evm_increaseTime', [FORCE_INCLUSION_PERIOD / 2]) + const localL1ToL2Batch = await enqueueAndGenerateL1ToL2Batch(DEFAULT_TX) + l1ToL2Timestamp = localL1ToL2Batch.timestamp + }) + afterEach(async () => { + await provider.send('evm_revert', [snapshotID]) + }) + + it('should succesfully append a batch with an older timestamp than the oldest batch', async () => { + const oldTimestamp = safetyTimestamp - 1 + await canonicalTxChain + .connect(sequencer) + .appendSequencerBatch(DEFAULT_BATCH, oldTimestamp) + }) + + it('should succesfully append a batch with a timestamp equal to the oldest batch', async () => { + await canonicalTxChain + .connect(sequencer) + .appendSequencerBatch(DEFAULT_BATCH, safetyTimestamp) + }) + + it('should revert when appending a batch with a timestamp in between the two batches', async () => { + const middleTimestamp = safetyTimestamp + 1 + await TestUtils.assertRevertsAsync( + 'Must process older SafetyQueue batches first to enforce timestamp monotonicity', + async () => { + await canonicalTxChain + .connect(sequencer) + .appendSequencerBatch(DEFAULT_BATCH, middleTimestamp) + } + ) + }) + + it('should revert when appending a batch with a timestamp newer than both batches', async () => { + await provider.send('evm_increaseTime', [FORCE_INCLUSION_PERIOD / 10]) // increase time by 60 seconds + const oldTimestamp = l1ToL2Timestamp + 1 + await TestUtils.assertRevertsAsync( + 'Must process older L1ToL2Queue batches first to enforce timestamp monotonicity', + async () => { + await canonicalTxChain + .connect(sequencer) + .appendSequencerBatch(DEFAULT_BATCH, oldTimestamp) + } + ) + }) + }) + + describe('when there is an old batch in the l1ToL2Queue and a recent batch in the safetyQueue', async () => { + let l1ToL2Timestamp + let safetyTimestamp + let snapshotID + beforeEach(async () => { + const localL1ToL2Batch = await enqueueAndGenerateL1ToL2Batch(DEFAULT_TX) + l1ToL2Timestamp = localL1ToL2Batch.timestamp + snapshotID = await provider.send('evm_snapshot', []) + await provider.send('evm_increaseTime', [FORCE_INCLUSION_PERIOD / 2]) + const localSafetyBatch = await enqueueAndGenerateSafetyBatch(DEFAULT_TX) + safetyTimestamp = localSafetyBatch.timestamp + }) + afterEach(async () => { + await provider.send('evm_revert', [snapshotID]) + }) + + it('should succesfully append a batch with an older timestamp than both batches', async () => { + const oldTimestamp = l1ToL2Timestamp - 1 + await canonicalTxChain + .connect(sequencer) + .appendSequencerBatch(DEFAULT_BATCH, oldTimestamp) + }) + + it('should succesfully append a batch with a timestamp equal to the older batch', async () => { + await canonicalTxChain + .connect(sequencer) + .appendSequencerBatch(DEFAULT_BATCH, l1ToL2Timestamp) + }) + + it('should revert when appending a batch with a timestamp in between the two batches', async () => { + const middleTimestamp = l1ToL2Timestamp + 1 + await TestUtils.assertRevertsAsync( + 'Must process older L1ToL2Queue batches first to enforce timestamp monotonicity', + async () => { + await canonicalTxChain + .connect(sequencer) + .appendSequencerBatch(DEFAULT_BATCH, middleTimestamp) + } + ) + }) + + it('should revert when appending a batch with a timestamp newer than both batches', async () => { + await provider.send('evm_increaseTime', [FORCE_INCLUSION_PERIOD / 10]) // increase time by 60 seconds + const newTimestamp = safetyTimestamp + 1 + await TestUtils.assertRevertsAsync( + 'Must process older L1ToL2Queue batches first to enforce timestamp monotonicity', + async () => { + await canonicalTxChain + .connect(sequencer) + .appendSequencerBatch(DEFAULT_BATCH, newTimestamp) } ) }) @@ -260,8 +425,7 @@ describe('CanonicalTransactionChain', () => { describe('appendL1ToL2Batch()', async () => { describe('when there is a batch in the L1toL2Queue', async () => { beforeEach(async () => { - const localBatch = await enqueueAndGenerateBatch(DEFAULT_TX) - localL1ToL2Queue.push(localBatch) + await enqueueAndGenerateL1ToL2Batch(DEFAULT_TX) }) it('should successfully dequeue a L1ToL2Batch', async () => { @@ -277,7 +441,7 @@ describe('CanonicalTransactionChain', () => { it('should successfully append a L1ToL2Batch', async () => { const { timestamp, txHash } = await l1ToL2Queue.batchHeaders(0) - const localBatch = new DefaultRollupBatch( + const localBatch = new TxChainBatch( timestamp, true, // isL1ToL2Tx 0, //batchIndex @@ -300,18 +464,36 @@ describe('CanonicalTransactionChain', () => { ) }) - describe('after inclusion period has elapsed', async () => { - let snapshotID - beforeEach(async () => { - snapshotID = await provider.send('evm_snapshot', []) - await provider.send('evm_increaseTime', [LIVENESS_ASSUMPTION]) - }) - afterEach(async () => { - await provider.send('evm_revert', [snapshotID]) - }) - it('should allow non-sequencer to appendL1ToL2Batch', async () => { - await canonicalTxChain.appendL1ToL2Batch() - }) + it('should allow non-sequencer to appendL1ToL2Batch after inclusion period has elapsed', async () => { + const snapshotID = await provider.send('evm_snapshot', []) + await provider.send('evm_increaseTime', [FORCE_INCLUSION_PERIOD]) + await canonicalTxChain.appendL1ToL2Batch() + await provider.send('evm_revert', [snapshotID]) + }) + }) + + describe('when there is a batch in both the SafetyQueue and L1toL2Queue', async () => { + it('should revert when the SafetyQueue batch is older', async () => { + const snapshotID = await provider.send('evm_snapshot', []) + await enqueueAndGenerateSafetyBatch(DEFAULT_TX) + await provider.send('evm_increaseTime', [10]) + await enqueueAndGenerateL1ToL2Batch(DEFAULT_TX) + await TestUtils.assertRevertsAsync( + 'Must process older SafetyQueue batches first to enforce timestamp monotonicity', + async () => { + await canonicalTxChain.appendL1ToL2Batch() + } + ) + await provider.send('evm_revert', [snapshotID]) + }) + + it('should succeed when the L1ToL2Queue batch is older', async () => { + const snapshotID = await provider.send('evm_snapshot', []) + await enqueueAndGenerateL1ToL2Batch(DEFAULT_TX) + await provider.send('evm_increaseTime', [10]) + await enqueueAndGenerateSafetyBatch(DEFAULT_TX) + await canonicalTxChain.connect(sequencer).appendL1ToL2Batch() + await provider.send('evm_revert', [snapshotID]) }) }) @@ -319,7 +501,90 @@ describe('CanonicalTransactionChain', () => { await TestUtils.assertRevertsAsync( 'Queue is empty, no element to peek at', async () => { - await canonicalTxChain.connect(sequencer).appendL1ToL2Batch() + await canonicalTxChain.appendL1ToL2Batch() + } + ) + }) + }) + + describe('appendSafetyBatch()', async () => { + describe('when there is a batch in the SafetyQueue', async () => { + beforeEach(async () => { + await enqueueAndGenerateSafetyBatch(DEFAULT_TX) + }) + + it('should successfully dequeue a SafetyBatch', async () => { + await canonicalTxChain.connect(sequencer).appendSafetyBatch() + const front = await safetyQueue.front() + front.should.equal(1) + const { timestamp, txHash } = await safetyQueue.batchHeaders(0) + timestamp.should.equal(0) + txHash.should.equal( + '0x0000000000000000000000000000000000000000000000000000000000000000' + ) + }) + + it('should successfully append a SafetyBatch', async () => { + const { timestamp, txHash } = await safetyQueue.batchHeaders(0) + const localBatch = new TxChainBatch( + timestamp, + false, // isL1ToL2Tx + 0, //batchIndex + 0, // cumulativePrevElements + [DEFAULT_TX] // elements + ) + await localBatch.generateTree() + const localBatchHeaderHash = await localBatch.hashBatchHeader() + await canonicalTxChain.connect(sequencer).appendSafetyBatch() + const batchHeaderHash = await canonicalTxChain.batches(0) + batchHeaderHash.should.equal(localBatchHeaderHash) + }) + + it('should not allow non-sequencer to appendSafetyBatch if less than force inclusion period', async () => { + await TestUtils.assertRevertsAsync( + 'Message sender does not have permission to append this batch', + async () => { + await canonicalTxChain.appendSafetyBatch() + } + ) + }) + + it('should allow non-sequencer to appendSafetyBatch after force inclusion period has elapsed', async () => { + const snapshotID = await provider.send('evm_snapshot', []) + await provider.send('evm_increaseTime', [FORCE_INCLUSION_PERIOD]) + await canonicalTxChain.appendSafetyBatch() + await provider.send('evm_revert', [snapshotID]) + }) + }) + + it('should revert when trying to appendSafetyBatch when there is an older batch in the L1ToL2Queue ', async () => { + const snapshotID = await provider.send('evm_snapshot', []) + await enqueueAndGenerateL1ToL2Batch(DEFAULT_TX) + await provider.send('evm_increaseTime', [10]) + await enqueueAndGenerateSafetyBatch(DEFAULT_TX) + await TestUtils.assertRevertsAsync( + 'Must process older L1ToL2Queue batches first to enforce timestamp monotonicity', + async () => { + await canonicalTxChain.appendSafetyBatch() + } + ) + await provider.send('evm_revert', [snapshotID]) + }) + + it('should succeed when there are only newer batches in the L1ToL2Queue ', async () => { + const snapshotID = await provider.send('evm_snapshot', []) + await enqueueAndGenerateSafetyBatch(DEFAULT_TX) + await provider.send('evm_increaseTime', [10]) + await enqueueAndGenerateL1ToL2Batch(DEFAULT_TX) + await canonicalTxChain.connect(sequencer).appendSafetyBatch() + await provider.send('evm_revert', [snapshotID]) + }) + + it('should revert when SafetyTxQueue is empty', async () => { + await TestUtils.assertRevertsAsync( + 'Queue is empty, no element to peek at', + async () => { + await canonicalTxChain.appendSafetyBatch() } ) }) @@ -327,27 +592,21 @@ describe('CanonicalTransactionChain', () => { describe('verifyElement() ', async () => { it('should return true for valid elements for different batches and elements', async () => { - const numBatches = 3 - const batch = [ - '0x1234', - '0x4567', - '0x890a', - '0x4567', - '0x890a', - '0xabcd', - '0x1234', - ] + const numBatches = 4 + let cumulativePrevElements = 0 for (let batchIndex = 0; batchIndex < numBatches; batchIndex++) { - const cumulativePrevElements = batch.length * batchIndex - const localBatch = await appendAndGenerateBatch( + const batchSize = batchIndex * batchIndex + 1 // 1, 2, 5, 10 + const batch = makeRandomBatchOfSize(batchSize) + const localBatch = await appendAndGenerateSequencerBatch( batch, batchIndex, cumulativePrevElements ) + cumulativePrevElements += batchSize for ( let elementIndex = 0; elementIndex < batch.length; - elementIndex += 3 + elementIndex++ ) { const element = batch[elementIndex] const position = localBatch.getPosition(elementIndex) @@ -365,9 +624,9 @@ describe('CanonicalTransactionChain', () => { }) it('should return true for valid element from a l1ToL2Batch', async () => { - const l1ToL2Batch = await enqueueAndGenerateBatch(DEFAULT_TX) + const l1ToL2Batch = await enqueueAndGenerateL1ToL2Batch(DEFAULT_TX) await canonicalTxChain.connect(sequencer).appendL1ToL2Batch() - const localBatch = new DefaultRollupBatch( + const localBatch = new TxChainBatch( l1ToL2Batch.timestamp, //timestamp true, //isL1ToL2Tx 0, //batchIndex @@ -388,9 +647,33 @@ describe('CanonicalTransactionChain', () => { isIncluded.should.equal(true) }) + it('should return true for valid element from a SafetyBatch', async () => { + const safetyBatch = await enqueueAndGenerateSafetyBatch(DEFAULT_TX) + await canonicalTxChain.connect(sequencer).appendSafetyBatch() + const localBatch = new TxChainBatch( + safetyBatch.timestamp, //timestamp + false, //isL1ToL2Tx + 0, //batchIndex + 0, //cumulativePrevElements + [DEFAULT_TX] //batch + ) + await localBatch.generateTree() + const elementIndex = 0 + const position = localBatch.getPosition(elementIndex) + const elementInclusionProof = await localBatch.getElementInclusionProof( + elementIndex + ) + const isIncluded = await canonicalTxChain.verifyElement( + DEFAULT_TX, // element + position, + elementInclusionProof + ) + isIncluded.should.equal(true) + }) + it('should return false for wrong position with wrong indexInBatch', async () => { const batch = ['0x1234', '0x4567', '0x890a', '0x4567', '0x890a', '0xabcd'] - const localBatch = await appendAndGenerateBatch(batch) + const localBatch = await appendAndGenerateSequencerBatch(batch) const elementIndex = 1 const element = batch[elementIndex] const position = localBatch.getPosition(elementIndex) @@ -409,7 +692,7 @@ describe('CanonicalTransactionChain', () => { it('should return false for wrong position and matching indexInBatch', async () => { const batch = ['0x1234', '0x4567', '0x890a', '0x4567', '0x890a', '0xabcd'] - const localBatch = await appendAndGenerateBatch(batch) + const localBatch = await appendAndGenerateSequencerBatch(batch) const elementIndex = 1 const element = batch[elementIndex] const position = localBatch.getPosition(elementIndex) diff --git a/packages/rollup-contracts/test/rollup-list/L1ToL2TransactionQueue.spec.ts b/packages/rollup-contracts/test/rollup-list/L1ToL2TransactionQueue.spec.ts index 6a64438d4b34e..afeff5c3c2a3b 100644 --- a/packages/rollup-contracts/test/rollup-list/L1ToL2TransactionQueue.spec.ts +++ b/packages/rollup-contracts/test/rollup-list/L1ToL2TransactionQueue.spec.ts @@ -78,7 +78,6 @@ describe('L1ToL2TransactionQueue', () => { }) it('should not allow dequeue from other address', async () => { await l1ToL2TxQueue.connect(l1ToL2TransactionPasser).enqueueTx(defaultTx) - await TestUtils.assertRevertsAsync( 'Message sender does not have permission to dequeue', async () => { diff --git a/packages/rollup-contracts/test/rollup-list/RLhelper.ts b/packages/rollup-contracts/test/rollup-list/RLhelper.ts index 68a81476ffc56..0c619d5a72e04 100644 --- a/packages/rollup-contracts/test/rollup-list/RLhelper.ts +++ b/packages/rollup-contracts/test/rollup-list/RLhelper.ts @@ -18,35 +18,37 @@ interface TxChainBatchHeader { cumulativePrevElements: number } -interface ElementInclusionProof { +interface TxElementInclusionProof { batchIndex: number batchHeader: TxChainBatchHeader indexInBatch: number siblings: string[] } -/* - * Helper class which provides all information requried for a particular - * Rollup batch. This includes all of the transactions in readable form - * as well as the merkle tree which it generates. - */ -export class DefaultRollupBatch { - public timestamp: number - public isL1ToL2Tx: boolean +interface StateBatchHeader { + elementsMerkleRoot: string + numElementsInBatch: number + cumulativePrevElements: number +} + +interface StateElementInclusionProof { + batchIndex: number + batchHeader: StateBatchHeader + indexInBatch: number + siblings: string[] +} + +export class ChainBatch { public batchIndex: number //index in public cumulativePrevElements: number //in batchHeader public elements: string[] //Rollup batch public elementsMerkleTree: SparseMerkleTreeImpl constructor( - timestamp: number, // Ethereum batch this batch was submitted in - isL1ToL2Tx: boolean, batchIndex: number, // index in batchs array (first batch has batchIndex of 0) cumulativePrevElements: number, elements: string[] ) { - this.isL1ToL2Tx = isL1ToL2Tx - this.timestamp = timestamp this.batchIndex = batchIndex this.cumulativePrevElements = cumulativePrevElements this.elements = elements @@ -55,7 +57,6 @@ export class DefaultRollupBatch { * Generate the elements merkle tree from this.elements */ public async generateTree(): Promise { - // Create a tree! const treeHeight = Math.ceil(Math.log2(this.elements.length)) + 1 // The height should actually not be plus 1 this.elementsMerkleTree = await SparseMerkleTreeImpl.create( newInMemoryDB(), @@ -93,6 +94,48 @@ export class DefaultRollupBatch { return siblings } + /* + * elementIndex is the index in this batch of the element + * that we want to create an inclusion proof for. + */ + public async getElementInclusionProof( + elementIndex: number + ): Promise { + const bufferRoot = await this.elementsMerkleTree.getRootHash() + return { + batchIndex: this.batchIndex, + batchHeader: { + elementsMerkleRoot: bufToHexString(bufferRoot), + numElementsInBatch: this.elements.length, + cumulativePrevElements: this.cumulativePrevElements, + }, + indexInBatch: elementIndex, + siblings: await this.getSiblings(elementIndex), + } + } +} + +/* + * Helper class which provides all information requried for a particular + * Rollup batch. This includes all of the transactions in readable form + * as well as the merkle tree which it generates. + */ +export class TxChainBatch extends ChainBatch { + public timestamp: number + public isL1ToL2Tx: boolean + + constructor( + timestamp: number, // Ethereum batch this batch was submitted in + isL1ToL2Tx: boolean, + batchIndex: number, // index in batchs array (first batch has batchIndex of 0) + cumulativePrevElements: number, + elements: string[] + ) { + super(batchIndex, cumulativePrevElements, elements) + this.isL1ToL2Tx = isL1ToL2Tx + this.timestamp = timestamp + } + public async hashBatchHeader(): Promise { const bufferRoot = await this.elementsMerkleTree.getRootHash() return utils.solidityKeccak256( @@ -111,10 +154,9 @@ export class DefaultRollupBatch { * elementIndex is the index in this batch of the element * that we want to create an inclusion proof for. */ - public async getElementInclusionProof( elementIndex: number - ): Promise { + ): Promise { const bufferRoot = await this.elementsMerkleTree.getRootHash() return { batchIndex: this.batchIndex, @@ -130,12 +172,35 @@ export class DefaultRollupBatch { } } } + +export class StateChainBatch extends ChainBatch { + constructor( + batchIndex: number, // index in batchs array (first batch has batchIndex of 0) + cumulativePrevElements: number, + elements: string[] + ) { + super(batchIndex, cumulativePrevElements, elements) + } + + public async hashBatchHeader(): Promise { + const bufferRoot = await this.elementsMerkleTree.getRootHash() + return utils.solidityKeccak256( + ['bytes32', 'uint', 'uint'], + [ + bufToHexString(bufferRoot), + this.elements.length, + this.cumulativePrevElements, + ] + ) + } +} + /* * Helper class which provides all information requried for a particular * Rollup Queue Batch. This includes all of the transactions in readable form * as well as the merkle tree which it generates. */ -export class RollupQueueBatch { +export class TxQueueBatch { public elements: string[] public elementsMerkleTree: SparseMerkleTreeImpl public timestamp: number diff --git a/packages/rollup-contracts/test/rollup-list/RollupQueue.spec.ts b/packages/rollup-contracts/test/rollup-list/RollupQueue.spec.ts index 202a5ce9840c8..13f33aedad7ef 100644 --- a/packages/rollup-contracts/test/rollup-list/RollupQueue.spec.ts +++ b/packages/rollup-contracts/test/rollup-list/RollupQueue.spec.ts @@ -5,7 +5,7 @@ import { getLogger, TestUtils } from '@eth-optimism/core-utils' import { createMockProvider, deployContract, getWallets } from 'ethereum-waffle' /* Internal Imports */ -import { RollupQueueBatch } from './RLhelper' +import { TxQueueBatch } from './RLhelper' /* Logging */ const log = getLogger('rollup-queue', true) @@ -38,23 +38,18 @@ describe('RollupQueue', () => { ) }) - const enqueueAndGenerateBatch = async ( - tx: string - ): Promise => { + const enqueueAndGenerateBatch = async (tx: string): Promise => { // Submit the rollup batch on-chain const enqueueTx = await rollupQueue.enqueueTx(tx) const txReceipt = await provider.getTransactionReceipt(enqueueTx.hash) const timestamp = (await provider.getBlock(txReceipt.blockNumber)).timestamp // Generate a local version of the rollup batch - const localBatch = new RollupQueueBatch(tx, timestamp) + const localBatch = new TxQueueBatch(tx, timestamp) await localBatch.generateTree() return localBatch } describe('enqueueTx() ', async () => { - it('should not throw as long as it gets a bytes array (even if its invalid)', async () => { - await rollupQueue.enqueueTx(DEFAULT_TX) - }) it('should add to batchHeaders array', async () => { await rollupQueue.enqueueTx(DEFAULT_TX) const batchesLength = await rollupQueue.getBatchHeadersLength() diff --git a/packages/rollup-contracts/test/rollup-list/SafetyTransactionQueue.spec.ts b/packages/rollup-contracts/test/rollup-list/SafetyTransactionQueue.spec.ts new file mode 100644 index 0000000000000..62a4d38a0a895 --- /dev/null +++ b/packages/rollup-contracts/test/rollup-list/SafetyTransactionQueue.spec.ts @@ -0,0 +1,71 @@ +import '../setup' + +/* External Imports */ +import { getLogger, TestUtils } from '@eth-optimism/core-utils' +import { createMockProvider, deployContract, getWallets } from 'ethereum-waffle' + +/* Logging */ +const log = getLogger('safety-tx-queue', true) + +/* Contract Imports */ +import * as SafetyTransactionQueue from '../../build/SafetyTransactionQueue.json' +import * as RollupMerkleUtils from '../../build/RollupMerkleUtils.json' + +describe('SafetyTransactionQueue', () => { + const provider = createMockProvider() + const [wallet, canonicalTransactionChain, randomWallet] = getWallets(provider) + const defaultTx = '0x1234' + let safetyTxQueue + let rollupMerkleUtils + + /* Link libraries before tests */ + before(async () => { + rollupMerkleUtils = await deployContract(wallet, RollupMerkleUtils, [], { + gasLimit: 6700000, + }) + }) + + beforeEach(async () => { + safetyTxQueue = await deployContract( + wallet, + SafetyTransactionQueue, + [rollupMerkleUtils.address, canonicalTransactionChain.address], + { + gasLimit: 6700000, + } + ) + }) + + describe('enqueueBatch() ', async () => { + it('should allow enqueue from any address', async () => { + await safetyTxQueue.connect(randomWallet).enqueueTx(defaultTx) + const batchesLength = await safetyTxQueue.getBatchHeadersLength() + batchesLength.should.equal(1) + }) + }) + + describe('dequeue() ', async () => { + it('should allow dequeue from canonicalTransactionChain', async () => { + await safetyTxQueue.enqueueTx(defaultTx) + await safetyTxQueue.connect(canonicalTransactionChain).dequeue() + const batchesLength = await safetyTxQueue.getBatchHeadersLength() + batchesLength.should.equal(1) + const { txHash, timestamp } = await safetyTxQueue.batchHeaders(0) + txHash.should.equal( + '0x0000000000000000000000000000000000000000000000000000000000000000' + ) + timestamp.should.equal(0) + const front = await safetyTxQueue.front() + front.should.equal(1) + }) + it('should not allow dequeue from other address', async () => { + await safetyTxQueue.enqueueTx(defaultTx) + await TestUtils.assertRevertsAsync( + 'Message sender does not have permission to dequeue', + async () => { + await safetyTxQueue.dequeue() + } + ) + }) + }) +}) diff --git a/packages/rollup-contracts/test/rollup-list/StateCommitmentChain.spec.ts b/packages/rollup-contracts/test/rollup-list/StateCommitmentChain.spec.ts new file mode 100644 index 0000000000000..8996ec45edc19 --- /dev/null +++ b/packages/rollup-contracts/test/rollup-list/StateCommitmentChain.spec.ts @@ -0,0 +1,378 @@ +import '../setup' + +/* External Imports */ +import { getLogger, TestUtils } from '@eth-optimism/core-utils' +import { createMockProvider, deployContract, getWallets } from 'ethereum-waffle' +import { Contract } from 'ethers' + +/* Internal Imports */ +import { StateChainBatch } from './RLhelper' +import { makeRandomBatchOfSize } from '../helpers' + +/* Logging */ +const log = getLogger('state-commitment-chain', true) + +/* Contract Imports */ +import * as StateCommitmentChain from '../../build/StateCommitmentChain.json' +import * as CanonicalTransactionChain from '../../build/CanonicalTransactionChain.json' +import * as RollupMerkleUtils from '../../build/RollupMerkleUtils.json' + +/* Begin tests */ +describe('StateCommitmentChain', () => { + const provider = createMockProvider() + const [ + wallet, + sequencer, + l1ToL2TransactionPasser, + fraudVerifier, + randomWallet, + ] = getWallets(provider) + let stateChain + let canonicalTxChain + let rollupMerkleUtils + const DEFAULT_STATE_BATCH = ['0x1234', '0x5678'] + const DEFAULT_TX_BATCH = [ + '0x1234', + '0x5678', + '0x1234', + '0x5678', + '0x1234', + '0x5678', + '0x1234', + '0x5678', + '0x1234', + '0x5678', + ] + const DEFAULT_STATE_ROOT = '0x1234' + const FORCE_INCLUSION_PERIOD = 600 + + const appendAndGenerateStateBatch = async ( + batch: string[], + batchIndex: number = 0, + cumulativePrevElements: number = 0 + ): Promise => { + await stateChain.appendStateBatch(batch) + // Generate a local version of the rollup batch + const localBatch = new StateChainBatch( + batchIndex, + cumulativePrevElements, + batch + ) + await localBatch.generateTree() + return localBatch + } + + const appendTxBatch = async (batch: string[]): Promise => { + const timestamp = Math.floor(Date.now() / 1000) + // Submit the rollup batch on-chain + await canonicalTxChain + .connect(sequencer) + .appendSequencerBatch(batch, timestamp) + } + + before(async () => { + rollupMerkleUtils = await deployContract(wallet, RollupMerkleUtils, [], { + gasLimit: 6700000, + }) + + canonicalTxChain = await deployContract( + wallet, + CanonicalTransactionChain, + [ + rollupMerkleUtils.address, + sequencer.address, + l1ToL2TransactionPasser.address, + FORCE_INCLUSION_PERIOD, + ], + { + gasLimit: 6700000, + } + ) + // length 10 batch + await appendTxBatch(DEFAULT_TX_BATCH) + }) + + /* Deploy a new RollupChain before each test */ + beforeEach(async () => { + stateChain = await deployContract( + wallet, + StateCommitmentChain, + [ + rollupMerkleUtils.address, + canonicalTxChain.address, + fraudVerifier.address, + ], + { + gasLimit: 6700000, + } + ) + }) + + describe('appendStateBatch()', async () => { + it('should allow appending of state batches from any wallet', async () => { + await stateChain + .connect(randomWallet) + .appendStateBatch(DEFAULT_STATE_BATCH) + }) + + it('should throw if submitting an empty batch', async () => { + const emptyBatch = [] + await TestUtils.assertRevertsAsync( + 'Cannot submit an empty state commitment batch', + async () => { + await stateChain.appendStateBatch(emptyBatch) + } + ) + }) + + it('should add to batches array', async () => { + await stateChain.appendStateBatch(DEFAULT_STATE_BATCH) + const batchesLength = await stateChain.getBatchesLength() + batchesLength.toNumber().should.equal(1) + }) + + it('should update cumulativeNumElements correctly', async () => { + await stateChain.appendStateBatch(DEFAULT_STATE_BATCH) + const cumulativeNumElements = await stateChain.cumulativeNumElements.call() + cumulativeNumElements.toNumber().should.equal(DEFAULT_STATE_BATCH.length) + }) + + it('should calculate batchHeaderHash correctly', async () => { + const localBatch = await appendAndGenerateStateBatch(DEFAULT_STATE_BATCH) + const expectedBatchHeaderHash = await localBatch.hashBatchHeader() + const calculatedBatchHeaderHash = await stateChain.batches(0) + calculatedBatchHeaderHash.should.equal(expectedBatchHeaderHash) + }) + + it('should add multiple batches correctly', async () => { + const numBatches = 3 + let expectedNumElements = 0 + for (let batchIndex = 0; batchIndex < numBatches; batchIndex++) { + const batch = makeRandomBatchOfSize(batchIndex + 1) + const cumulativePrevElements = expectedNumElements + const localBatch = await appendAndGenerateStateBatch( + batch, + batchIndex, + cumulativePrevElements + ) + const expectedBatchHeaderHash = await localBatch.hashBatchHeader() + const calculatedBatchHeaderHash = await stateChain.batches(batchIndex) + calculatedBatchHeaderHash.should.equal(expectedBatchHeaderHash) + expectedNumElements += batch.length + } + const cumulativeNumElements = await stateChain.cumulativeNumElements.call() + cumulativeNumElements.toNumber().should.equal(expectedNumElements) + const batchesLength = await stateChain.getBatchesLength() + batchesLength.toNumber().should.equal(numBatches) + }) + + it('should throw if submitting more state commitments than number of txs in canonical tx chain', async () => { + const numBatches = 5 + for (let i = 0; i < numBatches; i++) { + await stateChain.appendStateBatch(DEFAULT_STATE_BATCH) + } + await TestUtils.assertRevertsAsync( + 'Cannot append more state commitments than total number of transactions in CanonicalTransactionChain', + async () => { + await stateChain.appendStateBatch(DEFAULT_STATE_BATCH) + } + ) + }) + }) + + describe('verifyElement() ', async () => { + it('should return true for valid elements for different batches and elements', async () => { + // add enough transaction batches so # txs > # state roots + await appendTxBatch(DEFAULT_TX_BATCH) + + const numBatches = 4 + let cumulativePrevElements = 0 + for (let batchIndex = 0; batchIndex < numBatches; batchIndex++) { + const batchSize = batchIndex * batchIndex + 1 // 1, 2, 5, 10 + const batch = makeRandomBatchOfSize(batchSize) + const localBatch = await appendAndGenerateStateBatch( + batch, + batchIndex, + cumulativePrevElements + ) + cumulativePrevElements += batchSize + for ( + let elementIndex = 0; + elementIndex < batch.length; + elementIndex++ + ) { + const element = batch[elementIndex] + const position = localBatch.getPosition(elementIndex) + const elementInclusionProof = await localBatch.getElementInclusionProof( + elementIndex + ) + const isIncluded = await stateChain.verifyElement( + element, + position, + elementInclusionProof + ) + isIncluded.should.equal(true) + } + } + }) + + it('should return false for wrong position with wrong indexInBatch', async () => { + const batch = ['0x1234', '0x4567', '0x890a', '0x4567', '0x890a', '0xabcd'] + const localBatch = await appendAndGenerateStateBatch(batch) + const elementIndex = 1 + const element = batch[elementIndex] + const position = localBatch.getPosition(elementIndex) + const elementInclusionProof = await localBatch.getElementInclusionProof( + elementIndex + ) + //Give wrong position so inclusion proof is wrong + const wrongPosition = position + 1 + const isIncluded = await stateChain.verifyElement( + element, + wrongPosition, + elementInclusionProof + ) + isIncluded.should.equal(false) + }) + + it('should return false for wrong position and matching indexInBatch', async () => { + const batch = ['0x1234', '0x4567', '0x890a', '0x4567', '0x890a', '0xabcd'] + const localBatch = await appendAndGenerateStateBatch(batch) + const elementIndex = 1 + const element = batch[elementIndex] + const position = localBatch.getPosition(elementIndex) + const elementInclusionProof = await localBatch.getElementInclusionProof( + elementIndex + ) + //Give wrong position so inclusion proof is wrong + const wrongPosition = position + 1 + //Change index to also be false (so position = index + cumulative) + elementInclusionProof.indexInBatch++ + const isIncluded = await stateChain.verifyElement( + element, + wrongPosition, + elementInclusionProof + ) + isIncluded.should.equal(false) + }) + }) + + describe('deleteAfterInclusive() ', async () => { + it('should not allow deletion from address other than fraud verifier', async () => { + const cumulativePrevElements = 0 + const batchIndex = 0 + const localBatch = await appendAndGenerateStateBatch(DEFAULT_STATE_BATCH) + const batchHeader = { + elementsMerkleRoot: await localBatch.elementsMerkleTree.getRootHash(), + numElementsInBatch: DEFAULT_STATE_BATCH.length, + cumulativePrevElements, + } + await TestUtils.assertRevertsAsync( + 'Only FraudVerifier has permission to delete state batches', + async () => { + await stateChain.connect(randomWallet).deleteAfterInclusive( + batchIndex, // delete the single appended batch + batchHeader + ) + } + ) + }) + describe('when a single batch is deleted', async () => { + beforeEach(async () => { + const cumulativePrevElements = 0 + const batchIndex = 0 + const localBatch = await appendAndGenerateStateBatch( + DEFAULT_STATE_BATCH + ) + const batchHeader = { + elementsMerkleRoot: await localBatch.elementsMerkleTree.getRootHash(), + numElementsInBatch: DEFAULT_STATE_BATCH.length, + cumulativePrevElements, + } + await stateChain.connect(fraudVerifier).deleteAfterInclusive( + batchIndex, // delete the single appended batch + batchHeader + ) + }) + + it('should successfully update the batches array', async () => { + const batchesLength = await stateChain.getBatchesLength() + batchesLength.should.equal(0) + }) + + it('should successfully append a batch after deletion', async () => { + const localBatch = await appendAndGenerateStateBatch( + DEFAULT_STATE_BATCH + ) + const expectedBatchHeaderHash = await localBatch.hashBatchHeader() + const calculatedBatchHeaderHash = await stateChain.batches(0) + calculatedBatchHeaderHash.should.equal(expectedBatchHeaderHash) + }) + }) + + it('should delete many batches', async () => { + const deleteBatchIndex = 0 + const localBatches = [] + for (let batchIndex = 0; batchIndex < 5; batchIndex++) { + const cumulativePrevElements = batchIndex * DEFAULT_STATE_BATCH.length + const localBatch = await appendAndGenerateStateBatch( + DEFAULT_STATE_BATCH, + batchIndex, + cumulativePrevElements + ) + localBatches.push(localBatch) + } + const deleteBatch = localBatches[deleteBatchIndex] + const batchHeader = { + elementsMerkleRoot: deleteBatch.elementsMerkleTree.getRootHash(), + numElementsInBatch: DEFAULT_STATE_BATCH.length, + cumulativePrevElements: deleteBatch.cumulativePrevElements, + } + await stateChain.connect(fraudVerifier).deleteAfterInclusive( + deleteBatchIndex, // delete all batches (including and after batch 0) + batchHeader + ) + const batchesLength = await stateChain.getBatchesLength() + batchesLength.should.equal(0) + }) + + it('should revert if batchHeader is incorrect', async () => { + const cumulativePrevElements = 0 + const batchIndex = 0 + const localBatch = await appendAndGenerateStateBatch(DEFAULT_STATE_BATCH) + const batchHeader = { + elementsMerkleRoot: await localBatch.elementsMerkleTree.getRootHash(), + numElementsInBatch: DEFAULT_STATE_BATCH.length + 1, // increment to make header incorrect + cumulativePrevElements, + } + await TestUtils.assertRevertsAsync( + 'Calculated batch header is different than expected batch header', + async () => { + await stateChain.connect(fraudVerifier).deleteAfterInclusive( + batchIndex, // delete the single appended batch + batchHeader + ) + } + ) + }) + + it('should revert if trying to delete a batch outside of valid range', async () => { + const cumulativePrevElements = 0 + const batchIndex = 1 // outside of range + const localBatch = await appendAndGenerateStateBatch(DEFAULT_STATE_BATCH) + const batchHeader = { + elementsMerkleRoot: await localBatch.elementsMerkleTree.getRootHash(), + numElementsInBatch: DEFAULT_STATE_BATCH.length + 1, // increment to make header incorrect + cumulativePrevElements, + } + await TestUtils.assertRevertsAsync( + 'Cannot delete batches outside of valid range', + async () => { + await stateChain + .connect(fraudVerifier) + .deleteAfterInclusive(batchIndex, batchHeader) + } + ) + }) + }) +})