From cc0ae1c0c7b266e870681b79b41054ec2eebec79 Mon Sep 17 00:00:00 2001 From: Will Meister Date: Tue, 7 Jul 2020 09:30:35 -0500 Subject: [PATCH 01/14] Adding event emission and tests to rollup chain contracts --- .../chain/CanonicalTransactionChain.sol | 16 +- .../chain/StateCommitmentChain.sol | 7 +- .../optimistic-ethereum/queue/RollupQueue.sol | 10 +- packages/contracts/package.json | 2 + .../chain/CanonicalTransactionChain.spec.ts | 167 +++++++++++++++++- .../chain/StateCommitmentChain.spec.ts | 24 ++- .../test/contracts/queue/RollupQueue.spec.ts | 19 +- .../contracts/test/test-helpers/rl-helpers.ts | 13 +- packages/core-utils/src/app/misc.ts | 13 +- 9 files changed, 257 insertions(+), 14 deletions(-) diff --git a/packages/contracts/contracts/optimistic-ethereum/chain/CanonicalTransactionChain.sol b/packages/contracts/contracts/optimistic-ethereum/chain/CanonicalTransactionChain.sol index fdc980f870ef9..8dad7b10401c8 100644 --- a/packages/contracts/contracts/optimistic-ethereum/chain/CanonicalTransactionChain.sol +++ b/packages/contracts/contracts/optimistic-ethereum/chain/CanonicalTransactionChain.sol @@ -11,7 +11,6 @@ contract CanonicalTransactionChain { /* * Contract Variables */ - address public sequencer; uint public forceInclusionPeriod; RollupMerkleUtils public merkleUtils; @@ -21,6 +20,12 @@ contract CanonicalTransactionChain { bytes32[] public batches; uint public lastOVMTimestamp; + /* + * Events + */ + + event QueueBatchAppended( bytes32 _batchHeaderHash, bytes32 _txHash); + event SequencerBatchAppended(bytes32 _batchHeaderHash); /* * Constructor @@ -44,7 +49,6 @@ contract CanonicalTransactionChain { ); } - /* * Public Functions */ @@ -79,8 +83,8 @@ contract CanonicalTransactionChain { "Must process older SafetyQueue batches first to enforce timestamp monotonicity" ); - _appendQueueBatch(l1ToL2Header, true); l1ToL2Queue.dequeue(); + _appendQueueBatch(l1ToL2Header, true); } function appendSafetyBatch() public { @@ -91,8 +95,8 @@ contract CanonicalTransactionChain { "Must process older L1ToL2Queue batches first to enforce timestamp monotonicity" ); - _appendQueueBatch(safetyHeader, false); safetyQueue.dequeue(); + _appendQueueBatch(safetyHeader, false); } function _appendQueueBatch( @@ -120,6 +124,8 @@ contract CanonicalTransactionChain { batches.push(batchHeaderHash); cumulativeNumElements += numElementsInBatch; + + emit QueueBatchAppended(batchHeaderHash, timestampedHash.txHash); } function appendSequencerBatch( @@ -173,6 +179,8 @@ contract CanonicalTransactionChain { batches.push(batchHeaderHash); cumulativeNumElements += _txBatch.length; + + emit SequencerBatchAppended(batchHeaderHash); } // verifies an element is in the current list at the given position diff --git a/packages/contracts/contracts/optimistic-ethereum/chain/StateCommitmentChain.sol b/packages/contracts/contracts/optimistic-ethereum/chain/StateCommitmentChain.sol index 773691a2abf6b..ce40091325247 100644 --- a/packages/contracts/contracts/optimistic-ethereum/chain/StateCommitmentChain.sol +++ b/packages/contracts/contracts/optimistic-ethereum/chain/StateCommitmentChain.sol @@ -17,6 +17,11 @@ contract StateCommitmentChain { uint public cumulativeNumElements; bytes32[] public batches; + /* + * Events + */ + + event StateBatchAppended(bytes32 _batchHeaderHash); /* * Constructor @@ -32,7 +37,6 @@ contract StateCommitmentChain { fraudVerifier = _fraudVerifier; } - /* * Public Functions */ @@ -72,6 +76,7 @@ contract StateCommitmentChain { batches.push(batchHeaderHash); cumulativeNumElements += _stateBatch.length; + emit StateBatchAppended(batchHeaderHash); } // verifies an element is in the current list at the given position diff --git a/packages/contracts/contracts/optimistic-ethereum/queue/RollupQueue.sol b/packages/contracts/contracts/optimistic-ethereum/queue/RollupQueue.sol index 7a5d71e2f9b3a..8babcaac8635a 100644 --- a/packages/contracts/contracts/optimistic-ethereum/queue/RollupQueue.sol +++ b/packages/contracts/contracts/optimistic-ethereum/queue/RollupQueue.sol @@ -12,6 +12,10 @@ contract RollupQueue { DataTypes.TimestampedHash[] public batchHeaders; uint256 public front; + /* + * Events + */ + event TxEnqueued(bytes32 _txHash); /* * Public Functions @@ -54,10 +58,14 @@ contract RollupQueue { "Message sender does not have permission to enqueue" ); + bytes32 txHash = keccak256(_tx); + batchHeaders.push(DataTypes.TimestampedHash({ timestamp: now, - txHash: keccak256(_tx) + txHash: txHash })); + + emit TxEnqueued(txHash); } function dequeue() public { diff --git a/packages/contracts/package.json b/packages/contracts/package.json index e018502e20108..c519957ffa24b 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -18,11 +18,13 @@ ] }, "scripts": { + "all": "yarn clean && yarn build && yarn test && yarn fix && yarn lint", "test": "yarn run test:contracts", "test:contracts": "buidler test --show-stack-traces", "build": "yarn run build:contracts && yarn run build:typescript", "build:contracts": "buidler compile", "build:typescript": "tsc -p .", + "clean": "rm -rf ./artifacts ./build ./cache", "lint": "yarn run lint:typescript", "lint:typescript": "tslint --format stylish --project .", "fix": "yarn run fix:typescript", diff --git a/packages/contracts/test/contracts/chain/CanonicalTransactionChain.spec.ts b/packages/contracts/test/contracts/chain/CanonicalTransactionChain.spec.ts index edb8d5bfe5102..68a2c9abdc625 100644 --- a/packages/contracts/test/contracts/chain/CanonicalTransactionChain.spec.ts +++ b/packages/contracts/test/contracts/chain/CanonicalTransactionChain.spec.ts @@ -2,8 +2,14 @@ import '../../setup' /* External Imports */ import { ethers } from '@nomiclabs/buidler' -import { getLogger, TestUtils } from '@eth-optimism/core-utils' -import { Contract, Signer, ContractFactory } from 'ethers' +import { + getLogger, + isHexString, + remove0x, + sleep, + TestUtils, +} from '@eth-optimism/core-utils' +import { Signer, ContractFactory } from 'ethers' /* Internal Imports */ import { TxChainBatch, TxQueueBatch } from '../../test-helpers/rl-helpers' @@ -52,11 +58,26 @@ describe('CanonicalTransactionChain', () => { cumulativePrevElements: number = 0 ): Promise => { const timestamp = await appendSequencerBatch(batch) - // Generate a local version of the rollup batch - const localBatch = new TxChainBatch( + return createTxChainBatch( + batch, timestamp, false, batchIndex, + cumulativePrevElements + ) + } + + const createTxChainBatch = async ( + batch: string[], + timestamp: number, + isL1ToL2Tx: boolean, + batchIndex: number = 0, + cumulativePrevElements: number = 0 + ): Promise => { + const localBatch = new TxChainBatch( + timestamp, + isL1ToL2Tx, + batchIndex, cumulativePrevElements, batch ) @@ -708,4 +729,142 @@ describe('CanonicalTransactionChain', () => { isIncluded.should.equal(false) }) }) + + describe('Event Emitting', () => { + it('should emit SequencerBatchAppended event when appending sequencer batch', async () => { + let receivedBatchHeaderHash: string + canonicalTxChain.on( + canonicalTxChain.filters['SequencerBatchAppended'](), + (...data) => { + receivedBatchHeaderHash = data[0] + } + ) + const localBatch: TxChainBatch = await appendAndGenerateSequencerBatch( + DEFAULT_BATCH + ) + + await sleep(5_000) + + const received = !!receivedBatchHeaderHash + received.should.equal(true, `Did not receive expected event!`) + + receivedBatchHeaderHash.should.equal( + await localBatch.hashBatchHeader(), + 'Header hash mismatch!' + ) + }) + + it('should emit TxEnqueued event when enqueuing safety batch', async () => { + let receivedTxhash: string + safetyQueue.on(safetyQueue.filters['TxEnqueued'](), (...data) => { + receivedTxhash = data[0] + }) + + const localBatch: TxQueueBatch = await enqueueAndGenerateSafetyBatch( + DEFAULT_TX + ) + + await sleep(5_000) + + const received = !!receivedTxhash + received.should.equal(true, `Did not receive expected event!`) + + const root = await localBatch.getMerkleRoot() + receivedTxhash.should.equal(root, `Incorrect batch root!`) + }) + + it('should emit TxEnqueued event when enqueuing L1 To L2 batch', async () => { + let receivedTxhash: string + l1ToL2Queue.on(l1ToL2Queue.filters['TxEnqueued'](), (...data) => { + receivedTxhash = data[0] + }) + + const localBatch: TxQueueBatch = await enqueueAndGenerateL1ToL2Batch( + DEFAULT_TX + ) + + await sleep(5_000) + + const received = !!receivedTxhash + received.should.equal(true, `Did not receive expected event!`) + + const root = await localBatch.getMerkleRoot() + receivedTxhash.should.equal(root, `Incorrect batch root!`) + }) + + it('should emit QueueBatchAppended event when appending L1 to L2 batch', async () => { + let receivedBatchHeaderHash: string + let receivedTxhash: string + canonicalTxChain.on( + canonicalTxChain.filters['QueueBatchAppended'](), + (...data) => { + receivedBatchHeaderHash = data[0] + receivedTxhash = data[1] + } + ) + + const localBatch: TxQueueBatch = await enqueueAndGenerateL1ToL2Batch( + DEFAULT_TX + ) + await canonicalTxChain.connect(sequencer).appendL1ToL2Batch() + const front = await l1ToL2Queue.front() + front.should.equal(1) + const { timestamp, txHash } = await l1ToL2Queue.batchHeaders(0) + timestamp.should.equal(0) + txHash.should.equal( + '0x0000000000000000000000000000000000000000000000000000000000000000' + ) + + await sleep(5_000) + + const received = !!receivedBatchHeaderHash + received.should.equal(true, `Did not receive expected event!`) + + receivedBatchHeaderHash.should.equal( + await localBatch.hashBatchHeader(true), + `Incorrect batch header hash!` + ) + receivedTxhash.should.equal( + await localBatch.getMerkleRoot(), + `Incorrect tx hash!` + ) + }) + + it('should emit QueueBatchAppended event when appending Safety Queue batch', async () => { + let receivedBatchHeaderHash: string + let receivedTxhash: string + canonicalTxChain.on( + canonicalTxChain.filters['QueueBatchAppended'](), + (...data) => { + receivedBatchHeaderHash = data[0] + receivedTxhash = data[1] + } + ) + + const localBatch = await enqueueAndGenerateSafetyBatch(DEFAULT_TX) + + 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' + ) + + await sleep(5_000) + + const received = !!receivedBatchHeaderHash + received.should.equal(true, `Did not receive expected event!`) + + receivedBatchHeaderHash.should.equal( + await localBatch.hashBatchHeader(false), + `Incorrect batch header hash!` + ) + receivedTxhash.should.equal( + await localBatch.getMerkleRoot(), + `Incorrect tx hash!` + ) + }) + }) }) diff --git a/packages/contracts/test/contracts/chain/StateCommitmentChain.spec.ts b/packages/contracts/test/contracts/chain/StateCommitmentChain.spec.ts index cdf7481783bb9..b21d0c7f7c85e 100644 --- a/packages/contracts/test/contracts/chain/StateCommitmentChain.spec.ts +++ b/packages/contracts/test/contracts/chain/StateCommitmentChain.spec.ts @@ -2,7 +2,7 @@ import '../../setup' /* External Imports */ import { ethers } from '@nomiclabs/buidler' -import { getLogger, TestUtils } from '@eth-optimism/core-utils' +import { getLogger, sleep, TestUtils } from '@eth-optimism/core-utils' import { Contract, Signer, ContractFactory } from 'ethers' /* Internal Imports */ @@ -371,4 +371,26 @@ describe('StateCommitmentChain', () => { ) }) }) + + describe('Event Emitting', () => { + it('should emit StateBatchAppended when state batch is appended', async () => { + let receivedBatchHeaderHash: string + stateChain.on(stateChain.filters['StateBatchAppended'](), (...data) => { + receivedBatchHeaderHash = data[0] + }) + const localBatch = await appendAndGenerateStateBatch(DEFAULT_STATE_BATCH) + + await sleep(5_000) + + const batchReceived: boolean = !!receivedBatchHeaderHash + batchReceived.should.equal( + true, + `State Batch Appended event not received!` + ) + receivedBatchHeaderHash.should.equal( + await localBatch.hashBatchHeader(), + `State Batch Appended event has incorrect batch header hash!` + ) + }) + }) }) diff --git a/packages/contracts/test/contracts/queue/RollupQueue.spec.ts b/packages/contracts/test/contracts/queue/RollupQueue.spec.ts index 961bd7bbf75de..e037772c548a5 100644 --- a/packages/contracts/test/contracts/queue/RollupQueue.spec.ts +++ b/packages/contracts/test/contracts/queue/RollupQueue.spec.ts @@ -2,7 +2,7 @@ import '../../setup' /* External Imports */ import { ethers } from '@nomiclabs/buidler' -import { getLogger, TestUtils } from '@eth-optimism/core-utils' +import { getLogger, sleep, TestUtils } from '@eth-optimism/core-utils' import { Signer, ContractFactory, Contract } from 'ethers' /* Internal Imports */ @@ -72,6 +72,23 @@ describe('RollupQueue', () => { const batchesLength = await rollupQueue.getBatchHeadersLength() batchesLength.toNumber().should.equal(numBatches) }) + + it('should emit event on enqueue', async () => { + let receivedTxhash: string + rollupQueue.on(rollupQueue.filters['TxEnqueued'](), (...data) => { + receivedTxhash = data[0] + }) + + const localBatch: TxQueueBatch = await enqueueAndGenerateBatch(DEFAULT_TX) + + await sleep(5_000) + + const received = !!receivedTxhash + received.should.equal(true, `Did not receive expected event!`) + + const root = await localBatch.getMerkleRoot() + receivedTxhash.should.equal(root, `Incorrect batch root!`) + }) }) describe('dequeue()', async () => { diff --git a/packages/contracts/test/test-helpers/rl-helpers.ts b/packages/contracts/test/test-helpers/rl-helpers.ts index 0c619d5a72e04..3592d379dc9ec 100644 --- a/packages/contracts/test/test-helpers/rl-helpers.ts +++ b/packages/contracts/test/test-helpers/rl-helpers.ts @@ -175,7 +175,7 @@ export class TxChainBatch extends ChainBatch { export class StateChainBatch extends ChainBatch { constructor( - batchIndex: number, // index in batchs array (first batch has batchIndex of 0) + batchIndex: number, // index in batches array (first batch has batchIndex of 0) cumulativePrevElements: number, elements: string[] ) { @@ -230,4 +230,15 @@ export class TxQueueBatch { const bufferRoot = await this.elementsMerkleTree.getRootHash() return bufToHexString(bufferRoot) } + + public async hashBatchHeader( + isL1ToL2Tx: boolean, + cumulativePrevElements: number = 0 + ): Promise { + const txHash = await this.getMerkleRoot() + return utils.solidityKeccak256( + ['uint', 'bool', 'bytes32', 'uint', 'uint'], + [this.timestamp, isL1ToL2Tx, txHash, 1, cumulativePrevElements] + ) + } } diff --git a/packages/core-utils/src/app/misc.ts b/packages/core-utils/src/app/misc.ts index d794663075cc7..26597ce5a9703 100644 --- a/packages/core-utils/src/app/misc.ts +++ b/packages/core-utils/src/app/misc.ts @@ -6,6 +6,7 @@ import { BigNumber } from './number' import { RLP, hexlify } from 'ethers/utils' export const NULL_ADDRESS = '0x0000000000000000000000000000000000000000' +const hexRegex = /^(0x)?[0-9a-fA-F]*$/ /** * JSON-stringifies a value if it's not already a string. @@ -141,6 +142,16 @@ export const reverse = (str: string): string => { .join('') } +/** + * Returns whether or not the provided string is a hex string. + * + * @param str The string to test. + * @returns True if the provided string is a hex string, false otherwise. + */ +export const isHexString = (str: string): boolean => { + return hexRegex.test(str) +} + /** * Converts a big number to a hex string. * @param bn the big number to be converted. @@ -180,7 +191,7 @@ export const hexStringify = (value: BigNumber | Buffer): string => { * @returns the hexString as a buffer. */ export const hexStrToBuf = (hexString: string): Buffer => { - if (!/^(0x)?[0-9a-fA-F]*$/.test(hexString)) { + if (!isHexString(hexString)) { throw new RangeError(`Invalid hex string [${hexString}]`) } if (hexString.length % 2 !== 0) { From 992470c870592590d38840dedc0ca93e2aa2c2a4 Mon Sep 17 00:00:00 2001 From: Will Meister Date: Tue, 7 Jul 2020 14:07:06 -0500 Subject: [PATCH 02/14] Removes tx hashes from event emission. Adds tx data to L1ToL2 event --- .../chain/CanonicalTransactionChain.sol | 13 ++- .../queue/L1ToL2TransactionQueue.sol | 4 + .../optimistic-ethereum/queue/RollupQueue.sol | 14 +++- .../chain/CanonicalTransactionChain.spec.ts | 80 +++++++------------ .../test/contracts/queue/RollupQueue.spec.ts | 14 ++-- 5 files changed, 59 insertions(+), 66 deletions(-) diff --git a/packages/contracts/contracts/optimistic-ethereum/chain/CanonicalTransactionChain.sol b/packages/contracts/contracts/optimistic-ethereum/chain/CanonicalTransactionChain.sol index 8dad7b10401c8..9fda4905272fc 100644 --- a/packages/contracts/contracts/optimistic-ethereum/chain/CanonicalTransactionChain.sol +++ b/packages/contracts/contracts/optimistic-ethereum/chain/CanonicalTransactionChain.sol @@ -24,7 +24,8 @@ contract CanonicalTransactionChain { * Events */ - event QueueBatchAppended( bytes32 _batchHeaderHash, bytes32 _txHash); + event L1ToL2BatchAppended( bytes32 _batchHeaderHash); + event SafetyQueueBatchAppended( bytes32 _batchHeaderHash); event SequencerBatchAppended(bytes32 _batchHeaderHash); /* @@ -83,8 +84,8 @@ contract CanonicalTransactionChain { "Must process older SafetyQueue batches first to enforce timestamp monotonicity" ); - l1ToL2Queue.dequeue(); _appendQueueBatch(l1ToL2Header, true); + l1ToL2Queue.dequeue(); } function appendSafetyBatch() public { @@ -95,8 +96,8 @@ contract CanonicalTransactionChain { "Must process older L1ToL2Queue batches first to enforce timestamp monotonicity" ); - safetyQueue.dequeue(); _appendQueueBatch(safetyHeader, false); + safetyQueue.dequeue(); } function _appendQueueBatch( @@ -125,7 +126,11 @@ contract CanonicalTransactionChain { batches.push(batchHeaderHash); cumulativeNumElements += numElementsInBatch; - emit QueueBatchAppended(batchHeaderHash, timestampedHash.txHash); + if (isL1ToL2Tx) { + emit L1ToL2BatchAppended(batchHeaderHash); + } else { + emit SafetyQueueBatchAppended(batchHeaderHash); + } } function appendSequencerBatch( diff --git a/packages/contracts/contracts/optimistic-ethereum/queue/L1ToL2TransactionQueue.sol b/packages/contracts/contracts/optimistic-ethereum/queue/L1ToL2TransactionQueue.sol index d9d6cd8491a36..4b1257737427f 100644 --- a/packages/contracts/contracts/optimistic-ethereum/queue/L1ToL2TransactionQueue.sol +++ b/packages/contracts/contracts/optimistic-ethereum/queue/L1ToL2TransactionQueue.sol @@ -23,4 +23,8 @@ contract L1ToL2TransactionQueue is RollupQueue { function authenticateDequeue(address _sender) public view returns (bool) { return _sender == canonicalTransactionChain; } + + function isCalldataTxQueue() public returns (bool) { + return false; + } } diff --git a/packages/contracts/contracts/optimistic-ethereum/queue/RollupQueue.sol b/packages/contracts/contracts/optimistic-ethereum/queue/RollupQueue.sol index 8babcaac8635a..14c2954ed04c6 100644 --- a/packages/contracts/contracts/optimistic-ethereum/queue/RollupQueue.sol +++ b/packages/contracts/contracts/optimistic-ethereum/queue/RollupQueue.sol @@ -15,8 +15,10 @@ contract RollupQueue { /* * Events */ - event TxEnqueued(bytes32 _txHash); + event CalldataTxEnqueued(); + event L1ToL2TxEnqueued(bytes _tx); + /* /* * Public Functions */ @@ -51,6 +53,10 @@ contract RollupQueue { return true; } + function isCalldataTxQueue() public returns (bool) { + return true; + } + function enqueueTx(bytes memory _tx) public { // Authentication. require( @@ -65,7 +71,11 @@ contract RollupQueue { txHash: txHash })); - emit TxEnqueued(txHash); + if (isCalldataTxQueue()) { + emit CalldataTxEnqueued(); + } else { + emit L1ToL2TxEnqueued(_tx); + } } function dequeue() public { diff --git a/packages/contracts/test/contracts/chain/CanonicalTransactionChain.spec.ts b/packages/contracts/test/contracts/chain/CanonicalTransactionChain.spec.ts index 68a2c9abdc625..c7ed0f3e52449 100644 --- a/packages/contracts/test/contracts/chain/CanonicalTransactionChain.spec.ts +++ b/packages/contracts/test/contracts/chain/CanonicalTransactionChain.spec.ts @@ -2,13 +2,7 @@ import '../../setup' /* External Imports */ import { ethers } from '@nomiclabs/buidler' -import { - getLogger, - isHexString, - remove0x, - sleep, - TestUtils, -} from '@eth-optimism/core-utils' +import { getLogger, sleep, TestUtils } from '@eth-optimism/core-utils' import { Signer, ContractFactory } from 'ethers' /* Internal Imports */ @@ -266,14 +260,14 @@ describe('CanonicalTransactionChain', () => { localBatch = await enqueueAndGenerateL1ToL2Batch(DEFAULT_TX) }) - it('should succesfully append a batch with an older timestamp', async () => { + it('should successfully 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 () => { + it('should successfully append a batch with an equal timestamp', async () => { await canonicalTxChain .connect(sequencer) .appendSequencerBatch(DEFAULT_BATCH, localBatch.timestamp) @@ -301,14 +295,14 @@ describe('CanonicalTransactionChain', () => { localBatch = await enqueueAndGenerateSafetyBatch(DEFAULT_TX) }) - it('should succesfully append a batch with an older timestamp', async () => { + it('should successfully 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 () => { + it('should successfully append a batch with an equal timestamp', async () => { await canonicalTxChain .connect(sequencer) .appendSequencerBatch(DEFAULT_BATCH, localBatch.timestamp) @@ -345,14 +339,14 @@ describe('CanonicalTransactionChain', () => { await provider.send('evm_revert', [snapshotID]) }) - it('should succesfully append a batch with an older timestamp than the oldest batch', async () => { + it('should successfully 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 () => { + it('should successfully append a batch with a timestamp equal to the oldest batch', async () => { await canonicalTxChain .connect(sequencer) .appendSequencerBatch(DEFAULT_BATCH, safetyTimestamp) @@ -400,14 +394,14 @@ describe('CanonicalTransactionChain', () => { await provider.send('evm_revert', [snapshotID]) }) - it('should succesfully append a batch with an older timestamp than both batches', async () => { + it('should successfully 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 () => { + it('should successfully append a batch with a timestamp equal to the older batch', async () => { await canonicalTxChain .connect(sequencer) .appendSequencerBatch(DEFAULT_BATCH, l1ToL2Timestamp) @@ -754,29 +748,23 @@ describe('CanonicalTransactionChain', () => { ) }) - it('should emit TxEnqueued event when enqueuing safety batch', async () => { - let receivedTxhash: string - safetyQueue.on(safetyQueue.filters['TxEnqueued'](), (...data) => { - receivedTxhash = data[0] + it('should emit CalldataTxEnqueued event when enqueuing safety batch', async () => { + let txEnqueued: boolean = false + safetyQueue.on(safetyQueue.filters['CalldataTxEnqueued'](), () => { + txEnqueued = true }) - const localBatch: TxQueueBatch = await enqueueAndGenerateSafetyBatch( - DEFAULT_TX - ) + await enqueueAndGenerateSafetyBatch(DEFAULT_TX) await sleep(5_000) - const received = !!receivedTxhash - received.should.equal(true, `Did not receive expected event!`) - - const root = await localBatch.getMerkleRoot() - receivedTxhash.should.equal(root, `Incorrect batch root!`) + txEnqueued.should.equal(true, `Did not receive expected event!`) }) - it('should emit TxEnqueued event when enqueuing L1 To L2 batch', async () => { - let receivedTxhash: string - l1ToL2Queue.on(l1ToL2Queue.filters['TxEnqueued'](), (...data) => { - receivedTxhash = data[0] + it('should emit L1ToL2TxEnqueued event when enqueuing L1 To L2 batch', async () => { + let enqueuedTx: string + l1ToL2Queue.on(l1ToL2Queue.filters['L1ToL2TxEnqueued'](), (...data) => { + enqueuedTx = data[0] }) const localBatch: TxQueueBatch = await enqueueAndGenerateL1ToL2Batch( @@ -785,21 +773,21 @@ describe('CanonicalTransactionChain', () => { await sleep(5_000) - const received = !!receivedTxhash - received.should.equal(true, `Did not receive expected event!`) + const receivedTx: boolean = !!enqueuedTx + receivedTx.should.equal(true, `Did not receive expected event!`) - const root = await localBatch.getMerkleRoot() - receivedTxhash.should.equal(root, `Incorrect batch root!`) + enqueuedTx.should.equal( + localBatch.elements[0], + `Emitted tx did not match submitted tx!` + ) }) - it('should emit QueueBatchAppended event when appending L1 to L2 batch', async () => { + it('should emit L1ToL2BatchAppended event when appending L1 to L2 batch', async () => { let receivedBatchHeaderHash: string - let receivedTxhash: string canonicalTxChain.on( - canonicalTxChain.filters['QueueBatchAppended'](), + canonicalTxChain.filters['L1ToL2BatchAppended'](), (...data) => { receivedBatchHeaderHash = data[0] - receivedTxhash = data[1] } ) @@ -824,20 +812,14 @@ describe('CanonicalTransactionChain', () => { await localBatch.hashBatchHeader(true), `Incorrect batch header hash!` ) - receivedTxhash.should.equal( - await localBatch.getMerkleRoot(), - `Incorrect tx hash!` - ) }) - it('should emit QueueBatchAppended event when appending Safety Queue batch', async () => { + it('should emit SafetyQueueBatchAppended event when appending Safety Queue batch', async () => { let receivedBatchHeaderHash: string - let receivedTxhash: string canonicalTxChain.on( - canonicalTxChain.filters['QueueBatchAppended'](), + canonicalTxChain.filters['SafetyQueueBatchAppended'](), (...data) => { receivedBatchHeaderHash = data[0] - receivedTxhash = data[1] } ) @@ -861,10 +843,6 @@ describe('CanonicalTransactionChain', () => { await localBatch.hashBatchHeader(false), `Incorrect batch header hash!` ) - receivedTxhash.should.equal( - await localBatch.getMerkleRoot(), - `Incorrect tx hash!` - ) }) }) }) diff --git a/packages/contracts/test/contracts/queue/RollupQueue.spec.ts b/packages/contracts/test/contracts/queue/RollupQueue.spec.ts index e037772c548a5..9e358a7039759 100644 --- a/packages/contracts/test/contracts/queue/RollupQueue.spec.ts +++ b/packages/contracts/test/contracts/queue/RollupQueue.spec.ts @@ -74,20 +74,16 @@ describe('RollupQueue', () => { }) it('should emit event on enqueue', async () => { - let receivedTxhash: string - rollupQueue.on(rollupQueue.filters['TxEnqueued'](), (...data) => { - receivedTxhash = data[0] + let receivedEvent: boolean = false + rollupQueue.on(rollupQueue.filters['CalldataTxEnqueued'](), () => { + receivedEvent = true }) - const localBatch: TxQueueBatch = await enqueueAndGenerateBatch(DEFAULT_TX) + await enqueueAndGenerateBatch(DEFAULT_TX) await sleep(5_000) - const received = !!receivedTxhash - received.should.equal(true, `Did not receive expected event!`) - - const root = await localBatch.getMerkleRoot() - receivedTxhash.should.equal(root, `Incorrect batch root!`) + receivedEvent.should.equal(true, `Did not receive expected event!`) }) }) From d29c2fecaaf21c4a569ce0e2b9df67907ac5a121 Mon Sep 17 00:00:00 2001 From: Will Meister Date: Wed, 8 Jul 2020 16:12:15 -0500 Subject: [PATCH 03/14] Adding L1 Batch Submitter --- packages/core-utils/src/app/misc.ts | 9 +- .../app/data/consumers/l1-batch-submitter.ts | 216 ++++++++++++++++++ .../rollup-core/src/app/data/data-service.ts | 102 ++++++++- .../rollup-core/src/app/data/query-utils.ts | 4 +- .../src/types/data/l2-data-service.ts | 45 ++++ packages/rollup-core/src/types/data/types.ts | 19 ++ .../test/app/l2-chain-data-persister.spec.ts | 8 +- 7 files changed, 390 insertions(+), 13 deletions(-) create mode 100644 packages/rollup-core/src/app/data/consumers/l1-batch-submitter.ts diff --git a/packages/core-utils/src/app/misc.ts b/packages/core-utils/src/app/misc.ts index 26597ce5a9703..10c004a6800e1 100644 --- a/packages/core-utils/src/app/misc.ts +++ b/packages/core-utils/src/app/misc.ts @@ -164,10 +164,15 @@ export const bnToHexString = (bn: BigNumber): string => { /** * Converts a JavaScript number to a hex string. * @param number the JavaScript number to be converted. + * @param padToBytes the number of numeric bytes the resulting string should be, -1 if no padding should be done. * @returns the JavaScript number as a string. */ -export const numberToHexString = (number: number): string => { - return add0x(number.toString(16)) +export const numberToHexString = (number: number, padToBytes: number = -1): string => { + let str = number.toString(16) + if (padToBytes > 0 || str.length < padToBytes * 2) { + str = `${'0'.repeat(padToBytes*2 - str.length)}${str}` + } + return add0x(str) } /** diff --git a/packages/rollup-core/src/app/data/consumers/l1-batch-submitter.ts b/packages/rollup-core/src/app/data/consumers/l1-batch-submitter.ts new file mode 100644 index 0000000000000..8fcdd40e08c8c --- /dev/null +++ b/packages/rollup-core/src/app/data/consumers/l1-batch-submitter.ts @@ -0,0 +1,216 @@ +/* External Imports */ +import {getLogger, logError, numberToHexString, remove0x, ScheduledTask} from '@eth-optimism/core-utils' +import {Contract, Wallet} from 'ethers' + +/* Internal Imports */ +import {L1BatchSubmission, L2BatchStatus, L2DataService} from '../../../types/data' +import {TransactionReceipt, TransactionResponse} from 'ethers/providers' + +const log = getLogger('l2-batch-creator') + +/** + * Polls the DB for L2 batches ready to send to L1 and submits them. + */ +export class L1BatchSubmitter extends ScheduledTask { + constructor( + private readonly dataService: L2DataService, + private readonly sequencerWallet: Wallet, + private readonly canonicalTransactionChain: Contract, + private readonly stateCommitmentChain: Contract, + private readonly confirmationsUntilFinal: number = 1, + periodMilliseconds = 10_000 + ) { + super(periodMilliseconds) + } + + /** + * @inheritDoc + * + * Submits L2 batches from L2 Transactions in the DB whenever there is a batch that is ready. + * + */ + public async runTask(): Promise { + let l2Batch: L1BatchSubmission + try { + l2Batch = await this.dataService.getNextBatchForL1Submission() + } catch (e) { + logError(log, `Error fetching batch for L1 submission! Continuing...`, e) + return + } + + if (!l2Batch || !l2Batch.transactions || !l2Batch.transactions.length) { + log.debug(`No batches found for L1 submission.`) + return + } + + let txBatchTxHash: string = l2Batch.l1TxBatchTxHash + let rootBatchTxHash: string = l2Batch.l1StateRootBatchTxHash + switch (l2Batch.status) { + case L2BatchStatus.BATCHED: + txBatchTxHash = await this.buildAndSendRollupBatchTransaction(l2Batch) + if (!txBatchTxHash) { + return + } + // Fallthrough on purpose -- this is a workflow + case L2BatchStatus.TXS_SUBMITTED: + await this.waitForTxBatchConfirms(txBatchTxHash, l2Batch.l2BatchNumber) + // Fallthrough on purpose -- this is a workflow + case L2BatchStatus.TXS_CONFIRMED: + rootBatchTxHash = await this.buildAndSendStateRootBatchTransaction(l2Batch) + if (!rootBatchTxHash) { + return + } + // Fallthrough on purpose -- this is a workflow + case L2BatchStatus.ROOTS_SUBMITTED: + await this.waitForStateRootBatchConfirms(rootBatchTxHash, l2Batch.l2BatchNumber) + break + default: + log.error(`Received L1 Batch submission in unexpected state: ${l2Batch.status}!`) + break + } + } + + /** + * Builds and sends a Rollup Batch transaction to L1, returning its tx hash. + * + * @param l2Batch The L2 batch to send to L1. + * @returns The L1 tx hash. + */ + private async buildAndSendRollupBatchTransaction(l2Batch: L1BatchSubmission): Promise { + let txHash: string + try { + const txsCalldata: string[] = this.getTransactionBatchCalldata(l2Batch) + + const timestamp = l2Batch.transactions[0].timestamp + log.debug(`Submitting tx batch ${l2Batch.l2BatchNumber} to canonical chain. Batch: ${JSON.stringify(l2Batch)}, txs bytes: ${JSON.stringify(txsCalldata)}, timestamp: ${timestamp}`) + const txRes: TransactionResponse = await this.canonicalTransactionChain.appendSequencerBatch(txsCalldata, timestamp) + log.debug(`Tx batch ${l2Batch.l2BatchNumber} appended with at least one confirmation! Tx Hash: ${txRes.hash}`) + txHash = txRes.hash + } catch (e) { + logError(log, `Error submitting tx batch ${l2Batch.l2BatchNumber} to canonical chain!`, e) + return undefined + } + + try { + log.debug(`Marking tx batch ${l2Batch.l2BatchNumber} submitted`) + await this.dataService.markTransactionBatchSubmittedToL1(l2Batch.l2BatchNumber, txHash) + } catch (e) { + logError(log, `Error marking tx batch ${l2Batch.l2BatchNumber} as submitted!`, e) + // TODO: Should we return here? Don't want to resubmit, so I think we should update the DB + } + return txHash + } + + /** + * Waits for the configured number of confirms for the provided rollup tx transaction hash and + * marks the tx as + * + * @param txHash The tx hash to wait for. + * @param batchNumber The rollup batch number in question. + */ + private async waitForTxBatchConfirms(txHash: string, batchNumber: number): Promise { + if (this.confirmationsUntilFinal > 1) { + try { + log.debug(`Waiting for ${this.confirmationsUntilFinal} confirmations before treating tx batch ${batchNumber} submission as final.`) + const receipt: TransactionReceipt = await this.canonicalTransactionChain.provider.waitForTransaction(txHash, this.confirmationsUntilFinal) + log.debug(`Batch submission finalized for tx batch ${batchNumber}!`) + } catch (e) { + logError(log, `Error waiting for necessary block confirmations until final!`, e) + // TODO: Should we return here? Don't want to resubmit, so I think we should update the DB + } + } + + try { + log.debug(`Marking tx batch ${batchNumber} confirmed!`) + await this.dataService.markTransactionBatchConfirmedOnL1(batchNumber, txHash) + log.debug(`Tx batch ${batchNumber} marked confirmed!`) + } catch (e) { + logError(log, `Error marking tx batch ${batchNumber} as confirmed!`, e) + } + } + + /** + * Builds and sends a State Root Batch transaction to L1, returning its tx hash. + * + * @param l2Batch The l2 batch from which state roots may be retrieved. + * @returns The L1 tx hash. + */ + private async buildAndSendStateRootBatchTransaction(l2Batch: L1BatchSubmission): Promise { + let txHash: string + try { + const stateRoots: string[] = l2Batch.transactions.map(x => x.stateRoot) + + log.debug(`Submitting state root batch ${l2Batch.l2BatchNumber} to state commitment chain. State roots: ${JSON.stringify(stateRoots)}`) + const stateRootRes: TransactionResponse = await this.stateCommitmentChain.appendStateBatch(stateRoots) + log.debug(`State batch ${l2Batch.l2BatchNumber} appended with at least one confirmation! Tx Hash: ${stateRootRes.hash}`) + txHash = stateRootRes.hash + } catch (e) { + logError(log, `Error submitting state root batch ${l2Batch.l2BatchNumber} to state commitment chain!`, e) + return undefined + } + + try { + log.debug(`Marking state root batch ${l2Batch.l2BatchNumber} submitted`) + await this.dataService.markStateRootBatchSubmittedToL1(l2Batch.l2BatchNumber, txHash) + } catch (e) { + logError(log, `Error marking state root batch ${l2Batch.l2BatchNumber} as submitted!`, e) + // TODO: Should we return here? Don't want to resubmit, so I think we should update the DB + } + return txHash + } + + /** + * Waits for the configured number of confirms for the provided rollup tx transaction hash and + * marks the tx as + * + * @param txHash The tx hash to wait for. + * @param batchNumber The rollup batch number in question. + */ + private async waitForStateRootBatchConfirms(txHash: string, batchNumber: number): Promise { + if (this.confirmationsUntilFinal > 1) { + try { + log.debug(`Waiting for ${this.confirmationsUntilFinal} confirmations before treating state root batch ${batchNumber} submission as final.`) + const receipt: TransactionReceipt = await this.stateCommitmentChain.provider.waitForTransaction(txHash, this.confirmationsUntilFinal) + log.debug(`State root batch submission finalized for batch ${batchNumber}!`) + } catch (e) { + logError(log, `Error waiting for necessary block confirmations until final!`, e) + // TODO: Should we return here? Don't want to resubmit, so I think we should update the DB + } + } + + try { + log.debug(`Marking state root batch ${batchNumber} confirmed!`) + await this.dataService.markStateRootBatchConfirmedOnL1(batchNumber, txHash) + log.debug(`State root batch ${batchNumber} marked confirmed!`) + } catch (e) { + logError(log, `Error marking batch ${batchNumber} as confirmed!`, e) + } + } + + /** + * Gets the calldata bytes for a transaction batch to be submitted by the sequencer. + * Rollup Transaction Format: + * sender: 20-byte address 0-20 + * target: 20-byte address 21-40 + * nonce: 32-byte uint 41-72 + * gasLimit: 32-byte uint 73-104 + * signature: 65-byte bytes 105-169 + * calldata: bytes 170-end + * + * @param batch The batch to turn into ABI-encoded calldata bytes. + * @returns The ABI-encoded bytes[] of the Rollup Transactions in the format listed above. + */ + private getTransactionBatchCalldata(batch: L1BatchSubmission): string[] { + const txs: string[] = [] + for (const tx of batch.transactions) { + const to: string = remove0x(tx.to) + const nonce: string = remove0x(numberToHexString(tx.nonce, 32)) + const gasLimit: string = tx.gasLimit ? tx.gasLimit.toString('hex', 64) : '00'.repeat(32) + const signature: string = remove0x(tx.signature) + const calldata: string = remove0x(tx.calldata) + txs.push(`${tx.from}${to}${nonce}${gasLimit}${signature}${calldata}`) + } + + return txs + } +} diff --git a/packages/rollup-core/src/app/data/data-service.ts b/packages/rollup-core/src/app/data/data-service.ts index b708ea065f7c9..4daacbe1120bf 100644 --- a/packages/rollup-core/src/app/data/data-service.ts +++ b/packages/rollup-core/src/app/data/data-service.ts @@ -8,7 +8,7 @@ import { Block, TransactionResponse } from 'ethers/providers' import { BlockBatches, DataService, - L1BatchRecord, + L1BatchRecord, L1BatchSubmission, L2BatchStatus, RollupTransaction, TransactionAndRoot, VerificationCandidate, @@ -276,7 +276,7 @@ export class DefaultDataService implements DataService { const timestampRes = await this.rdb.select( `SELECT DISTINCT block_timestamp FROM l2_tx - WHERE status = 'UNBATCHED' + WHERE status = '${L2BatchStatus.UNBATCHED}' ORDER BY block_timestamp ASC ` ) @@ -292,8 +292,8 @@ export class DefaultDataService implements DataService { const batchNumber = await this.insertNewL2TransactionBatch() await this.rdb.execute(` UPDATE l2_tx - SET status = 'BATCHED', batch_number = ${batchNumber} - WHERE status = 'UNBATCHED' AND block_timestamp = ${batchTimestamp} + SET status = '${L2BatchStatus.BATCHED}', batch_number = ${batchNumber} + WHERE status = '${L2BatchStatus.UNBATCHED}' AND block_timestamp = ${batchTimestamp} `) await this.rdb.commit() @@ -320,7 +320,7 @@ export class DefaultDataService implements DataService { const transactionsToBatchRes = await this.rdb.select(` SELECT COUNT(*) as batchable_tx_count, block_timestamp FROM l2_tx - WHERE status = 'UNBATCHED' + WHERE status = '${L2BatchStatus.UNBATCHED}' GROUP BY block_timestamp ORDER BY block_timestamp ASC `) @@ -357,11 +357,11 @@ export class DefaultDataService implements DataService { } await this.rdb.execute(` UPDATE l2_tx l - SET l.status = 'BATCHED', l.batch_number = ${batchNumber} + SET l.status = '${L2BatchStatus.BATCHED}', l.batch_number = ${batchNumber} FROM ( SELECT * FROM l2_tx - WHERE status = 'UNBATCHED' + WHERE status = '${L2BatchStatus.UNBATCHED}' LIMIT ${l1BatchSize} ) t WHERE l.id = t.id @@ -379,6 +379,94 @@ export class DefaultDataService implements DataService { } } + /** + * @inheritDoc + */ + public async getNextBatchForL1Submission(): Promise { + const res = await this.rdb.select(` + SELECT b.batch_number, b.status, b.tx_batch_tx_hash, b.state_batch_tx_hash, tx.block_number, tx.block_timestamp, tx.tx_index, tx.tx_hash, tx.sender, tx.l1_message_sender, tx.target, tx.calldata, tx.nonce, tx.signature, tx.state_root + FROM l2_tx tx + INNER JOIN l2_tx_batch b + WHERE b.status = '${L2BatchStatus.BATCHED}' + ORDER BY block_number ASC, tx_index ASC + `) + + if (!res || !res.length || !res[0].data.length) { + return undefined + } + + const batch: L1BatchSubmission = { + l1TxBatchTxHash: res[0].columns['tx_batch_tx_hash'], + l1StateRootBatchTxHash: res[0].columns['state_batch_tx_hash'], + status: res[0].columns['status'], + l2BatchNumber: res[0].columns['batch_number'], + transactions: [] + } + for (const row of res) { + batch.transactions.push({ + timestamp: row.columns['block_timestamp'], + blockNumber: row.columns['block_number'], + transactionIndex: row.columns['tx_index'], + transactionHash: row.columns['tx_hash'], + to: row.columns['target'], + from: row.columns['sender'], + nonce: row.columns['nonce'], + calldata: row.columns['calldata'], + stateRoot: row.columns['state_root'], + gasPrice: row.columns['gas_price'], + gasLimit: row.columns['gas_limit'], + l1MessageSender: row.columns['l1_message_sender'], // should never be present in this case + signature: row.columns['signature'] + }) + } + + return batch + } + + /** + * @inheritDoc + */ + public async markTransactionBatchSubmittedToL1(batchNumber: number, l1TxHash: string): Promise { + return this.rdb.execute(` + UPDATE l2_tx_batch + SET status = '${L2BatchStatus.TXS_SUBMITTED}', tx_batch_tx_hash = '${l1TxHash}' + WHERE batch_number = ${batchNumber} + `) + } + + /** + * @inheritDoc + */ + public async markTransactionBatchConfirmedOnL1(batchNumber: number, l1TxHash: string): Promise { + return this.rdb.execute(` + UPDATE l2_tx_batch + SET status = '${L2BatchStatus.TXS_CONFIRMED}', tx_batch_tx_hash = '${l1TxHash}' + WHERE batch_number = ${batchNumber} + `) + } + + /** + * @inheritDoc + */ + public async markStateRootBatchSubmittedToL1(batchNumber: number, l1TxHash: string): Promise { + return this.rdb.execute(` + UPDATE l2_tx_batch + SET status = '${L2BatchStatus.ROOTS_SUBMITTED}', state_batch_tx_hash = '${l1TxHash}' + WHERE batch_number = ${batchNumber} + `) + } + + /** + * @inheritDoc + */ + public async markStateRootBatchConfirmedOnL1(batchNumber: number, l1TxHash: string): Promise { + return this.rdb.execute(` + UPDATE l2_tx_batch + SET status = '${L2BatchStatus.ROOTS_CONFIRMED}', state_batch_tx_hash = '${l1TxHash}' + WHERE batch_number = ${batchNumber} + `) + } + /************ * VERIFIER * ************/ diff --git a/packages/rollup-core/src/app/data/query-utils.ts b/packages/rollup-core/src/app/data/query-utils.ts index 0fa50995dcc83..c951a4652c07d 100644 --- a/packages/rollup-core/src/app/data/query-utils.ts +++ b/packages/rollup-core/src/app/data/query-utils.ts @@ -48,13 +48,13 @@ export const getL1RollupStateRootInsertValue = ( return `'${stateRoot}', ${batchNumber}, ${batchIndex}` } -export const l2TransactionInsertStatement = `INSERT INTO l2_tx(block_number, block_timestamp, tx_index, tx_hash, sender, l1_message_sender, target, calldata, nonce, signature) ` +export const l2TransactionInsertStatement = `INSERT INTO l2_tx(block_number, block_timestamp, tx_index, tx_hash, sender, l1_message_sender, target, calldata, nonce, signature, state_root) ` export const getL2TransactionInsertValue = (tx: TransactionAndRoot): string => { return `'${tx.blockNumber}', ${tx.timestamp}, ${tx.transactionIndex}, '${ tx.transactionHash }' ${stringOrNull(tx.from)}, ${stringOrNull(tx.l1MessageSender)}, '${ tx.to - }', '${tx.calldata}', ${tx.nonce}, ${stringOrNull(tx.signature)}` + }', '${tx.calldata}', ${tx.nonce}, ${stringOrNull(tx.signature)}, '${tx.stateRoot}'` } export const bigNumOrNull = (bn: any): string => { diff --git a/packages/rollup-core/src/types/data/l2-data-service.ts b/packages/rollup-core/src/types/data/l2-data-service.ts index 77f9716b0d882..01abb1f17a8cd 100644 --- a/packages/rollup-core/src/types/data/l2-data-service.ts +++ b/packages/rollup-core/src/types/data/l2-data-service.ts @@ -1,5 +1,6 @@ /* Internal Imports */ import { TransactionAndRoot } from '../types' +import {L1BatchSubmission, L2BatchStatus} from './types' export interface L2DataService { /** @@ -29,4 +30,48 @@ export interface L2DataService { batchNumber: number, batchSize: number ): Promise + + /** + * Gets the next L2 Batch for submission to L1, if one exists. + * + * @returns The L1BatchSubmission object, or undefined + * @throws An error if there is a DB error. + */ + getNextBatchForL1Submission(): Promise + + /** + * Marks the tx batch with the provided batch number as submitted to the L1 chain. + * + * @param batchNumber The batch number to mark as submitted. + * @param l1TxHash The L1 transaction hash for the batch submission. + * @throws An error if there is a DB error. + */ + markTransactionBatchSubmittedToL1(batchNumber: number, l1TxHash: string): Promise + + /** + * Marks the tx batch with the provided batch number as confirmed on the L1 chain. + * + * @param batchNumber The batch number to mark as confirmed. + * @param l1TxHash The L1 transaction hash for the batch submission. + * @throws An error if there is a DB error. + */ + markTransactionBatchConfirmedOnL1(batchNumber: number, l1TxHash: string): Promise + + /** + * Marks the state root batch with the provided batch number as submitted to the L1 chain. + * + * @param batchNumber The batch number to mark as submitted. + * @param l1TxHash The L1 transaction hash for the batch submission. + * @throws An error if there is a DB error. + */ + markStateRootBatchSubmittedToL1(batchNumber: number, l1TxHash: string): Promise + + /** + * Marks the state root batch with the provided batch number as confirmed on the L1 chain. + * + * @param batchNumber The batch number to mark as confirmed. + * @param l1TxHash The L1 transaction hash for the batch submission. + * @throws An error if there is a DB error. + */ + markStateRootBatchConfirmedOnL1(batchNumber: number, l1TxHash: string): Promise } diff --git a/packages/rollup-core/src/types/data/types.ts b/packages/rollup-core/src/types/data/types.ts index b086e4ef35e29..48e7497eeb119 100644 --- a/packages/rollup-core/src/types/data/types.ts +++ b/packages/rollup-core/src/types/data/types.ts @@ -1,5 +1,24 @@ +import {TransactionAndRoot} from '../types' + +export enum L2BatchStatus { + UNBATCHED = 'UNBATCHED', + BATCHED = 'BATCHED', + TXS_SUBMITTED = 'TXS_SUBMITTED', + TXS_CONFIRMED = 'TXS_CONFIRMED', + ROOTS_SUBMITTED = 'ROOTS_SUBMITTED', + ROOTS_CONFIRMED = 'ROOTS_CONFIRMED' +} + export interface L1BatchRecord { blockTimestamp: number batchNumber: number batchSize: number } + +export interface L1BatchSubmission { + l1TxBatchTxHash: string + l1StateRootBatchTxHash: string + status: string + l2BatchNumber: number + transactions: TransactionAndRoot[] +} \ No newline at end of file diff --git a/packages/rollup-core/test/app/l2-chain-data-persister.spec.ts b/packages/rollup-core/test/app/l2-chain-data-persister.spec.ts index 180134661e70b..2478761a93cec 100644 --- a/packages/rollup-core/test/app/l2-chain-data-persister.spec.ts +++ b/packages/rollup-core/test/app/l2-chain-data-persister.spec.ts @@ -15,11 +15,15 @@ import { /* Internal Imports */ import { L2DataService, TransactionAndRoot } from '../../src/types' -import { CHAIN_ID, L2ChainDataPersister } from '../../src/app' +import {CHAIN_ID, DefaultDataService, L2ChainDataPersister} from '../../src/app' -class MockDataService implements L2DataService { +class MockDataService extends DefaultDataService { public readonly transactionAndRoots: TransactionAndRoot[] = [] + constructor() { + super(undefined); + } + public async insertL2Transaction(transaction: TransactionAndRoot) { this.transactionAndRoots.push(transaction) } From 1cce2aa75f0ac1ae908303cb35faff4232dabdda Mon Sep 17 00:00:00 2001 From: Will Meister Date: Wed, 8 Jul 2020 21:35:58 -0500 Subject: [PATCH 04/14] Adding QueueOrigin enum and log handlers --- .../app/data/consumers/l1-batch-submitter.ts | 174 ++++++++--- .../rollup-core/src/app/data/data-service.ts | 44 ++- .../src/app/data/producers/log-handlers.ts | 281 ++++++++++++++++++ .../rollup-core/src/app/data/query-utils.ts | 4 +- .../src/types/data/l1-data-service.ts | 16 + .../src/types/data/l2-data-service.ts | 22 +- packages/rollup-core/src/types/data/types.ts | 12 +- packages/rollup-core/src/types/types.ts | 1 + 8 files changed, 499 insertions(+), 55 deletions(-) create mode 100644 packages/rollup-core/src/app/data/producers/log-handlers.ts diff --git a/packages/rollup-core/src/app/data/consumers/l1-batch-submitter.ts b/packages/rollup-core/src/app/data/consumers/l1-batch-submitter.ts index 8fcdd40e08c8c..fb8722e09b77c 100644 --- a/packages/rollup-core/src/app/data/consumers/l1-batch-submitter.ts +++ b/packages/rollup-core/src/app/data/consumers/l1-batch-submitter.ts @@ -1,10 +1,20 @@ /* External Imports */ -import {getLogger, logError, numberToHexString, remove0x, ScheduledTask} from '@eth-optimism/core-utils' -import {Contract, Wallet} from 'ethers' +import { + getLogger, + logError, + numberToHexString, + remove0x, + ScheduledTask, +} from '@eth-optimism/core-utils' +import { Contract, Wallet } from 'ethers' /* Internal Imports */ -import {L1BatchSubmission, L2BatchStatus, L2DataService} from '../../../types/data' -import {TransactionReceipt, TransactionResponse} from 'ethers/providers' +import { + L1BatchSubmission, + L2BatchStatus, + L2DataService, +} from '../../../types/data' +import { TransactionReceipt, TransactionResponse } from 'ethers/providers' const log = getLogger('l2-batch-creator') @@ -56,16 +66,23 @@ export class L1BatchSubmitter extends ScheduledTask { await this.waitForTxBatchConfirms(txBatchTxHash, l2Batch.l2BatchNumber) // Fallthrough on purpose -- this is a workflow case L2BatchStatus.TXS_CONFIRMED: - rootBatchTxHash = await this.buildAndSendStateRootBatchTransaction(l2Batch) + rootBatchTxHash = await this.buildAndSendStateRootBatchTransaction( + l2Batch + ) if (!rootBatchTxHash) { return } // Fallthrough on purpose -- this is a workflow case L2BatchStatus.ROOTS_SUBMITTED: - await this.waitForStateRootBatchConfirms(rootBatchTxHash, l2Batch.l2BatchNumber) + await this.waitForStateRootBatchConfirms( + rootBatchTxHash, + l2Batch.l2BatchNumber + ) break default: - log.error(`Received L1 Batch submission in unexpected state: ${l2Batch.status}!`) + log.error( + `Received L1 Batch submission in unexpected state: ${l2Batch.status}!` + ) break } } @@ -76,26 +93,50 @@ export class L1BatchSubmitter extends ScheduledTask { * @param l2Batch The L2 batch to send to L1. * @returns The L1 tx hash. */ - private async buildAndSendRollupBatchTransaction(l2Batch: L1BatchSubmission): Promise { + private async buildAndSendRollupBatchTransaction( + l2Batch: L1BatchSubmission + ): Promise { let txHash: string try { const txsCalldata: string[] = this.getTransactionBatchCalldata(l2Batch) const timestamp = l2Batch.transactions[0].timestamp - log.debug(`Submitting tx batch ${l2Batch.l2BatchNumber} to canonical chain. Batch: ${JSON.stringify(l2Batch)}, txs bytes: ${JSON.stringify(txsCalldata)}, timestamp: ${timestamp}`) - const txRes: TransactionResponse = await this.canonicalTransactionChain.appendSequencerBatch(txsCalldata, timestamp) - log.debug(`Tx batch ${l2Batch.l2BatchNumber} appended with at least one confirmation! Tx Hash: ${txRes.hash}`) + log.debug( + `Submitting tx batch ${ + l2Batch.l2BatchNumber + } to canonical chain. Batch: ${JSON.stringify( + l2Batch + )}, txs bytes: ${JSON.stringify(txsCalldata)}, timestamp: ${timestamp}` + ) + const txRes: TransactionResponse = await this.canonicalTransactionChain.appendSequencerBatch( + txsCalldata, + timestamp + ) + log.debug( + `Tx batch ${l2Batch.l2BatchNumber} appended with at least one confirmation! Tx Hash: ${txRes.hash}` + ) txHash = txRes.hash } catch (e) { - logError(log, `Error submitting tx batch ${l2Batch.l2BatchNumber} to canonical chain!`, e) + logError( + log, + `Error submitting tx batch ${l2Batch.l2BatchNumber} to canonical chain!`, + e + ) return undefined } try { log.debug(`Marking tx batch ${l2Batch.l2BatchNumber} submitted`) - await this.dataService.markTransactionBatchSubmittedToL1(l2Batch.l2BatchNumber, txHash) + await this.dataService.markTransactionBatchSubmittedToL1( + l2Batch.l2BatchNumber, + txHash + ) } catch (e) { - logError(log, `Error marking tx batch ${l2Batch.l2BatchNumber} as submitted!`, e) + logError( + log, + `Error marking tx batch ${l2Batch.l2BatchNumber} as submitted!`, + e + ) // TODO: Should we return here? Don't want to resubmit, so I think we should update the DB } return txHash @@ -108,21 +149,36 @@ export class L1BatchSubmitter extends ScheduledTask { * @param txHash The tx hash to wait for. * @param batchNumber The rollup batch number in question. */ - private async waitForTxBatchConfirms(txHash: string, batchNumber: number): Promise { + private async waitForTxBatchConfirms( + txHash: string, + batchNumber: number + ): Promise { if (this.confirmationsUntilFinal > 1) { try { - log.debug(`Waiting for ${this.confirmationsUntilFinal} confirmations before treating tx batch ${batchNumber} submission as final.`) - const receipt: TransactionReceipt = await this.canonicalTransactionChain.provider.waitForTransaction(txHash, this.confirmationsUntilFinal) + log.debug( + `Waiting for ${this.confirmationsUntilFinal} confirmations before treating tx batch ${batchNumber} submission as final.` + ) + const receipt: TransactionReceipt = await this.canonicalTransactionChain.provider.waitForTransaction( + txHash, + this.confirmationsUntilFinal + ) log.debug(`Batch submission finalized for tx batch ${batchNumber}!`) } catch (e) { - logError(log, `Error waiting for necessary block confirmations until final!`, e) + logError( + log, + `Error waiting for necessary block confirmations until final!`, + e + ) // TODO: Should we return here? Don't want to resubmit, so I think we should update the DB } } try { log.debug(`Marking tx batch ${batchNumber} confirmed!`) - await this.dataService.markTransactionBatchConfirmedOnL1(batchNumber, txHash) + await this.dataService.markTransactionBatchConfirmedOnL1( + batchNumber, + txHash + ) log.debug(`Tx batch ${batchNumber} marked confirmed!`) } catch (e) { logError(log, `Error marking tx batch ${batchNumber} as confirmed!`, e) @@ -135,25 +191,46 @@ export class L1BatchSubmitter extends ScheduledTask { * @param l2Batch The l2 batch from which state roots may be retrieved. * @returns The L1 tx hash. */ - private async buildAndSendStateRootBatchTransaction(l2Batch: L1BatchSubmission): Promise { + private async buildAndSendStateRootBatchTransaction( + l2Batch: L1BatchSubmission + ): Promise { let txHash: string try { - const stateRoots: string[] = l2Batch.transactions.map(x => x.stateRoot) + const stateRoots: string[] = l2Batch.transactions.map((x) => x.stateRoot) - log.debug(`Submitting state root batch ${l2Batch.l2BatchNumber} to state commitment chain. State roots: ${JSON.stringify(stateRoots)}`) - const stateRootRes: TransactionResponse = await this.stateCommitmentChain.appendStateBatch(stateRoots) - log.debug(`State batch ${l2Batch.l2BatchNumber} appended with at least one confirmation! Tx Hash: ${stateRootRes.hash}`) + log.debug( + `Submitting state root batch ${ + l2Batch.l2BatchNumber + } to state commitment chain. State roots: ${JSON.stringify(stateRoots)}` + ) + const stateRootRes: TransactionResponse = await this.stateCommitmentChain.appendStateBatch( + stateRoots + ) + log.debug( + `State batch ${l2Batch.l2BatchNumber} appended with at least one confirmation! Tx Hash: ${stateRootRes.hash}` + ) txHash = stateRootRes.hash } catch (e) { - logError(log, `Error submitting state root batch ${l2Batch.l2BatchNumber} to state commitment chain!`, e) + logError( + log, + `Error submitting state root batch ${l2Batch.l2BatchNumber} to state commitment chain!`, + e + ) return undefined } try { log.debug(`Marking state root batch ${l2Batch.l2BatchNumber} submitted`) - await this.dataService.markStateRootBatchSubmittedToL1(l2Batch.l2BatchNumber, txHash) + await this.dataService.markStateRootBatchSubmittedToL1( + l2Batch.l2BatchNumber, + txHash + ) } catch (e) { - logError(log, `Error marking state root batch ${l2Batch.l2BatchNumber} as submitted!`, e) + logError( + log, + `Error marking state root batch ${l2Batch.l2BatchNumber} as submitted!`, + e + ) // TODO: Should we return here? Don't want to resubmit, so I think we should update the DB } return txHash @@ -166,21 +243,38 @@ export class L1BatchSubmitter extends ScheduledTask { * @param txHash The tx hash to wait for. * @param batchNumber The rollup batch number in question. */ - private async waitForStateRootBatchConfirms(txHash: string, batchNumber: number): Promise { + private async waitForStateRootBatchConfirms( + txHash: string, + batchNumber: number + ): Promise { if (this.confirmationsUntilFinal > 1) { try { - log.debug(`Waiting for ${this.confirmationsUntilFinal} confirmations before treating state root batch ${batchNumber} submission as final.`) - const receipt: TransactionReceipt = await this.stateCommitmentChain.provider.waitForTransaction(txHash, this.confirmationsUntilFinal) - log.debug(`State root batch submission finalized for batch ${batchNumber}!`) + log.debug( + `Waiting for ${this.confirmationsUntilFinal} confirmations before treating state root batch ${batchNumber} submission as final.` + ) + const receipt: TransactionReceipt = await this.stateCommitmentChain.provider.waitForTransaction( + txHash, + this.confirmationsUntilFinal + ) + log.debug( + `State root batch submission finalized for batch ${batchNumber}!` + ) } catch (e) { - logError(log, `Error waiting for necessary block confirmations until final!`, e) + logError( + log, + `Error waiting for necessary block confirmations until final!`, + e + ) // TODO: Should we return here? Don't want to resubmit, so I think we should update the DB } } try { log.debug(`Marking state root batch ${batchNumber} confirmed!`) - await this.dataService.markStateRootBatchConfirmedOnL1(batchNumber, txHash) + await this.dataService.markStateRootBatchConfirmedOnL1( + batchNumber, + txHash + ) log.debug(`State root batch ${batchNumber} marked confirmed!`) } catch (e) { logError(log, `Error marking batch ${batchNumber} as confirmed!`, e) @@ -191,11 +285,11 @@ export class L1BatchSubmitter extends ScheduledTask { * Gets the calldata bytes for a transaction batch to be submitted by the sequencer. * Rollup Transaction Format: * sender: 20-byte address 0-20 - * target: 20-byte address 21-40 - * nonce: 32-byte uint 41-72 - * gasLimit: 32-byte uint 73-104 - * signature: 65-byte bytes 105-169 - * calldata: bytes 170-end + * target: 20-byte address 20-40 + * nonce: 32-byte uint 40-72 + * gasLimit: 32-byte uint 72-104 + * signature: 65-byte bytes 104-169 + * calldata: bytes 169-end * * @param batch The batch to turn into ABI-encoded calldata bytes. * @returns The ABI-encoded bytes[] of the Rollup Transactions in the format listed above. @@ -205,7 +299,9 @@ export class L1BatchSubmitter extends ScheduledTask { for (const tx of batch.transactions) { const to: string = remove0x(tx.to) const nonce: string = remove0x(numberToHexString(tx.nonce, 32)) - const gasLimit: string = tx.gasLimit ? tx.gasLimit.toString('hex', 64) : '00'.repeat(32) + const gasLimit: string = tx.gasLimit + ? tx.gasLimit.toString('hex', 64) + : '00'.repeat(32) const signature: string = remove0x(tx.signature) const calldata: string = remove0x(tx.calldata) txs.push(`${tx.from}${to}${nonce}${gasLimit}${signature}${calldata}`) diff --git a/packages/rollup-core/src/app/data/data-service.ts b/packages/rollup-core/src/app/data/data-service.ts index 4daacbe1120bf..6ef9d893b755d 100644 --- a/packages/rollup-core/src/app/data/data-service.ts +++ b/packages/rollup-core/src/app/data/data-service.ts @@ -8,7 +8,9 @@ import { Block, TransactionResponse } from 'ethers/providers' import { BlockBatches, DataService, - L1BatchRecord, L1BatchSubmission, L2BatchStatus, + L1BatchRecord, + L1BatchSubmission, + L2BatchStatus, RollupTransaction, TransactionAndRoot, VerificationCandidate, @@ -122,6 +124,22 @@ export class DefaultDataService implements DataService { } } + /** + * @inheritDoc + */ + public async createNextL1ToL2Batch(): Promise { + // ***************************** TODO: THIS ************************************ + return undefined + } + + /** + * @inheritDoc + */ + public async createNextSafetyQueueBatch(): Promise { + // ***************************** TODO: THIS ************************************ + return undefined + } + /** * @inheritDoc */ @@ -400,7 +418,7 @@ export class DefaultDataService implements DataService { l1StateRootBatchTxHash: res[0].columns['state_batch_tx_hash'], status: res[0].columns['status'], l2BatchNumber: res[0].columns['batch_number'], - transactions: [] + transactions: [], } for (const row of res) { batch.transactions.push({ @@ -416,7 +434,7 @@ export class DefaultDataService implements DataService { gasPrice: row.columns['gas_price'], gasLimit: row.columns['gas_limit'], l1MessageSender: row.columns['l1_message_sender'], // should never be present in this case - signature: row.columns['signature'] + signature: row.columns['signature'], }) } @@ -426,7 +444,10 @@ export class DefaultDataService implements DataService { /** * @inheritDoc */ - public async markTransactionBatchSubmittedToL1(batchNumber: number, l1TxHash: string): Promise { + public async markTransactionBatchSubmittedToL1( + batchNumber: number, + l1TxHash: string + ): Promise { return this.rdb.execute(` UPDATE l2_tx_batch SET status = '${L2BatchStatus.TXS_SUBMITTED}', tx_batch_tx_hash = '${l1TxHash}' @@ -437,7 +458,10 @@ export class DefaultDataService implements DataService { /** * @inheritDoc */ - public async markTransactionBatchConfirmedOnL1(batchNumber: number, l1TxHash: string): Promise { + public async markTransactionBatchConfirmedOnL1( + batchNumber: number, + l1TxHash: string + ): Promise { return this.rdb.execute(` UPDATE l2_tx_batch SET status = '${L2BatchStatus.TXS_CONFIRMED}', tx_batch_tx_hash = '${l1TxHash}' @@ -448,7 +472,10 @@ export class DefaultDataService implements DataService { /** * @inheritDoc */ - public async markStateRootBatchSubmittedToL1(batchNumber: number, l1TxHash: string): Promise { + public async markStateRootBatchSubmittedToL1( + batchNumber: number, + l1TxHash: string + ): Promise { return this.rdb.execute(` UPDATE l2_tx_batch SET status = '${L2BatchStatus.ROOTS_SUBMITTED}', state_batch_tx_hash = '${l1TxHash}' @@ -459,7 +486,10 @@ export class DefaultDataService implements DataService { /** * @inheritDoc */ - public async markStateRootBatchConfirmedOnL1(batchNumber: number, l1TxHash: string): Promise { + public async markStateRootBatchConfirmedOnL1( + batchNumber: number, + l1TxHash: string + ): Promise { return this.rdb.execute(` UPDATE l2_tx_batch SET status = '${L2BatchStatus.ROOTS_CONFIRMED}', state_batch_tx_hash = '${l1TxHash}' diff --git a/packages/rollup-core/src/app/data/producers/log-handlers.ts b/packages/rollup-core/src/app/data/producers/log-handlers.ts new file mode 100644 index 0000000000000..4e9edc5f8553a --- /dev/null +++ b/packages/rollup-core/src/app/data/producers/log-handlers.ts @@ -0,0 +1,281 @@ +/* External Imports */ +import { + add0x, + BigNumber, + getLogger, + logError, + remove0x, +} from '@eth-optimism/core-utils' +import { Log, TransactionResponse } from 'ethers/providers/abstract-provider' +import { ethers } from 'ethers' + +/* Internal Imports */ +import { L1DataService, QueueOrigin, RollupTransaction } from '../../../types' + +const abi = new ethers.utils.AbiCoder() +const log = getLogger('log-handler') + +/** + * Handles the L1ToL2TxEnqueued event by parsing a RollupTransaction + * from the event data and storing it in the DB. + * + * Assumed Log Data Format: + * - sender: 20-byte address 0-20 + * - target: 20-byte address 20-40 + * - gasLimit: 32-byte uint 40-72 + * - calldata: bytes 72-end + * + * @param ds The L1DataService to use for persistence. + * @param l The log event that was emitted. + * @param tx The transaction that emitted the event. + * @throws Error if there's an error with persistence. + */ +export const L1ToL2TxEnqueuedLogHandler = async ( + ds: L1DataService, + l: Log, + tx: TransactionResponse +): Promise => { + log.debug( + `L1ToL2TxEnqueued event received at block ${tx.blockNumber}, tx ${l.transactionIndex}, log: ${l.transactionLogIndex}. TxHash: ${tx.hash}. Log Data: ${l.data}` + ) + + let rollupTransaction: RollupTransaction + try { + rollupTransaction = { + l1TxHash: l.transactionHash, + l1Timestamp: tx.timestamp, + l1BlockNumber: tx.blockNumber, + queueOrigin: QueueOrigin.L1_TO_L2_QUEUE, + batchIndex: 0, + sender: l.address, + l1MessageSender: add0x(l.data.substr(0, 40)), + target: add0x(l.data.substr(40, 40)), + // TODO: Change gasLimit to a BigNumber so it can support 256 bits + gasLimit: new BigNumber(l.data.substr(80, 64), 'hex').toNumber(), + calldata: add0x(l.data.substr(144)), + } + } catch (e) { + // This is, by definition, just an ill-formatted, and therefore invalid, tx. + log.debug( + `Error parsing calldata tx from CalldataTxEnqueued event. Calldata: ${tx.data}. Error: ${e.message}. Stack: ${e.stack}.` + ) + return + } + + await ds.insertL1RollupTransactions(l.transactionHash, [rollupTransaction]) +} + +/** + * Handles the CalldataTxEnqueued event by parsing a RollupTransaction + * from the transaction calldata and storing it in the DB. + * + * Assumed calldata format: + * - sender: 20-byte address 0-20 + * - target: 20-byte address 20-40 + * - nonce: 32-byte uint 40-72 + * - gasLimit: 32-byte uint 72-104 + * - signature: 65-byte bytes 104-169 + * - calldata: bytes 169-end + * + * @param ds The L1DataService to use for persistence. + * @param l The log event that was emitted. + * @param tx The transaction that emitted the event. + * @throws Error if there's an error with persistence. + */ +export const CalldataTxEnqueuedLogHandler = async ( + ds: L1DataService, + l: Log, + tx: TransactionResponse +): Promise => { + log.debug( + `CalldataTxEnqueued event received at block ${tx.blockNumber}, tx ${l.transactionIndex}, log: ${l.transactionLogIndex}. TxHash: ${tx.hash}. Calldata: ${tx.data}` + ) + + let rollupTransaction: RollupTransaction + try { + // Skip the 4 bytes of MethodID + const calldata = remove0x(tx.data).substr(8) + rollupTransaction = { + l1TxHash: l.transactionHash, + l1Timestamp: tx.timestamp, + l1BlockNumber: tx.blockNumber, + queueOrigin: QueueOrigin.SAFETY_QUEUE, + batchIndex: 0, + sender: add0x(calldata.substr(0, 40)), + target: add0x(calldata.substr(40, 40)), + // TODO Change nonce to a BigNumber so it can support 256 bits + nonce: new BigNumber(calldata.substr(80, 64), 'hex').toNumber(), + // TODO: Change gasLimit to a BigNumber so it can support 256 bits + gasLimit: new BigNumber(calldata.substr(144, 64), 'hex').toNumber(), + signature: add0x(calldata.substr(210, 65)), + calldata: add0x(calldata.substr(275)), + } + } catch (e) { + // This is, by definition, just an ill-formatted, and therefore invalid, tx. + log.debug( + `Error parsing calldata tx from CalldataTxEnqueued event. Calldata: ${tx.data}. Error: ${e.message}. Stack: ${e.stack}.` + ) + return + } + + await ds.insertL1RollupTransactions(l.transactionHash, [rollupTransaction]) +} + +/** + * Handles the L1ToL2BatchAppended event by parsing a RollupTransaction + * from the log event and storing it in the DB. + * + * @param ds The L1DataService to use for persistence. + * @param l The log event that was emitted. + * @param tx The transaction that emitted the event. + * @throws Error if there's an error with persistence. + */ +export const L1ToL2BatchAppendedLogHandler = async ( + ds: L1DataService, + l: Log, + tx: TransactionResponse +): Promise => { + log.debug( + `L1ToL2BatchAppended event received at block ${tx.blockNumber}, tx ${l.transactionIndex}, log: ${l.transactionLogIndex}. TxHash: ${tx.hash}` + ) + try { + await ds.createNextL1ToL2Batch() + } catch (e) { + logError( + log, + `Error creating next L1ToL2Batch after receiving an event to do so!`, + e + ) + throw e + } +} + +/** + * Handles the SafetyQueueBatchAppended event by parsing a RollupTransaction + * from the transaction calldata and storing it in the DB. + * + * @param ds The L1DataService to use for persistence. + * @param l The log event that was emitted. + * @param tx The transaction that emitted the event. + * @throws Error if there's an error with persistence. + */ +export const SafetyQueueBatchAppendedLogHandler = async ( + ds: L1DataService, + l: Log, + tx: TransactionResponse +): Promise => { + log.debug( + `SafetyQueueBatchAppended event received at block ${tx.blockNumber}, tx ${l.transactionIndex}, log: ${l.transactionLogIndex}. TxHash: ${tx.hash}` + ) + try { + await ds.createNextSafetyQueueBatch() + } catch (e) { + logError( + log, + `Error creating next L1ToL2Batch after receiving an event to do so!`, + e + ) + throw e + } +} + +/** + * Handles the SequencerBatchAppended event by parsing: + * - a list of RollupTransactions + * - L1 Block Timestamp at the time of L2 Execution + * from the transaction calldata and storing it in the DB. + * + * Assumed calldata format: + * - sender: 20-byte address 0-20 + * - target: 20-byte address 20-40 + * - nonce: 32-byte uint 40-72 + * - gasLimit: 32-byte uint 72-104 + * - signature: 65-byte bytes 104-169 + * - calldata: bytes 169-end + * + * @param ds The L1DataService to use for persistence. + * @param l The log event that was emitted. + * @param tx The transaction that emitted the event. + * @throws Error if there's an error with persistence. + */ +export const SequencerBatchAppendedLogHandler = async ( + ds: L1DataService, + l: Log, + tx: TransactionResponse +): Promise => { + log.debug( + `SequencerBatchAppended event received at block ${tx.blockNumber}, tx ${l.transactionIndex}, log: ${l.transactionLogIndex}. TxHash: ${tx.hash}. Calldata: ${tx.data}` + ) + + const rollupTransactions: RollupTransaction[] = [] + let timestamp: number + try { + let transactionsBytes: string[] + ;[transactionsBytes, timestamp] = abi.decode( + ['bytes[]', 'uint256'], + ethers.utils.hexDataSlice(tx.data, 4) + ) + + for (let i = 0; i < transactionsBytes.length; i++) { + const txBytes = transactionsBytes[i] + rollupTransactions.push({ + l1TxHash: l.transactionHash, + l1Timestamp: timestamp, + l1BlockNumber: tx.blockNumber, + queueOrigin: QueueOrigin.SEQUENCER, + batchIndex: i, + sender: add0x(txBytes.substr(0, 40)), + target: add0x(txBytes.substr(40, 40)), + // TODO Change nonce to a BigNumber so it can support 256 bits + nonce: new BigNumber(txBytes.substr(80, 64), 'hex').toNumber(), + // TODO: Change gasLimit to a BigNumber so it can support 256 bits + gasLimit: new BigNumber(txBytes.substr(144, 64), 'hex').toNumber(), + signature: add0x(txBytes.substr(210, 65)), + calldata: add0x(txBytes.substr(275)), + }) + } + } catch (e) { + // This is, by definition, just an ill-formatted, and therefore invalid, tx. + log.debug( + `Error parsing calldata tx from CalldataTxEnqueued event. Calldata: ${tx.data}. Error: ${e.message}. Stack: ${e.stack}.` + ) + return + } + + await ds.insertL1RollupTransactions(l.transactionHash, rollupTransactions) +} + +/** + * Handles the StateBatchAppended event by parsing a batch of state roots + * from the provided transaction calldata and storing it in the DB. + * + * @param ds The L1DataService to use for persistence. + * @param l The log event that was emitted. + * @param tx The transaction that emitted the event. + * @throws Error if there's an error with persistence. + */ +export const StateBatchAppendedLogHandler = async ( + ds: L1DataService, + l: Log, + tx: TransactionResponse +): Promise => { + log.debug( + `StateBatchAppended event received at block ${tx.blockNumber}, tx ${l.transactionIndex}, log: ${l.transactionLogIndex}. TxHash: ${tx.hash}. Calldata: ${tx.data}` + ) + + let stateRoots: string[] + try { + ;[stateRoots] = abi.decode( + ['bytes32[]'], + ethers.utils.hexDataSlice(tx.data, 4) + ) + } catch (e) { + // This is, by definition, just an ill-formatted, and therefore invalid, tx. + log.debug( + `Error parsing calldata tx from CalldataTxEnqueued event. Calldata: ${tx.data}. Error: ${e.message}. Stack: ${e.stack}.` + ) + return + } + + await ds.insertL1RollupStateRoots(l.transactionHash, stateRoots) +} diff --git a/packages/rollup-core/src/app/data/query-utils.ts b/packages/rollup-core/src/app/data/query-utils.ts index c951a4652c07d..439f5b3449562 100644 --- a/packages/rollup-core/src/app/data/query-utils.ts +++ b/packages/rollup-core/src/app/data/query-utils.ts @@ -54,7 +54,9 @@ export const getL2TransactionInsertValue = (tx: TransactionAndRoot): string => { tx.transactionHash }' ${stringOrNull(tx.from)}, ${stringOrNull(tx.l1MessageSender)}, '${ tx.to - }', '${tx.calldata}', ${tx.nonce}, ${stringOrNull(tx.signature)}, '${tx.stateRoot}'` + }', '${tx.calldata}', ${tx.nonce}, ${stringOrNull(tx.signature)}, '${ + tx.stateRoot + }'` } export const bigNumOrNull = (bn: any): string => { diff --git a/packages/rollup-core/src/types/data/l1-data-service.ts b/packages/rollup-core/src/types/data/l1-data-service.ts index 5b198616f48fa..4c1418fb1437b 100644 --- a/packages/rollup-core/src/types/data/l1-data-service.ts +++ b/packages/rollup-core/src/types/data/l1-data-service.ts @@ -65,6 +65,22 @@ export interface L1DataService { rollupTransactions: RollupTransaction[] ): Promise + /** + * Creates a batch from the oldest un-batched transaction that is from the L1 To L2 queue. + * + * @returns The created batch number + * @throws Error if there is a DB error or no such transaction exists. + */ + createNextL1ToL2Batch(): Promise + + /** + * Creates a batch from the oldest un-batched transaction that is from the Safety queue. + * + * @returns The created batch number + * @throws Error if there is a DB error or no such transaction exists. + */ + createNextSafetyQueueBatch(): Promise + /** * Atomically inserts the provided State Roots, creating a batch for them. * diff --git a/packages/rollup-core/src/types/data/l2-data-service.ts b/packages/rollup-core/src/types/data/l2-data-service.ts index 01abb1f17a8cd..eda2ad4de9dd0 100644 --- a/packages/rollup-core/src/types/data/l2-data-service.ts +++ b/packages/rollup-core/src/types/data/l2-data-service.ts @@ -1,6 +1,6 @@ /* Internal Imports */ import { TransactionAndRoot } from '../types' -import {L1BatchSubmission, L2BatchStatus} from './types' +import { L1BatchSubmission, L2BatchStatus } from './types' export interface L2DataService { /** @@ -46,7 +46,10 @@ export interface L2DataService { * @param l1TxHash The L1 transaction hash for the batch submission. * @throws An error if there is a DB error. */ - markTransactionBatchSubmittedToL1(batchNumber: number, l1TxHash: string): Promise + markTransactionBatchSubmittedToL1( + batchNumber: number, + l1TxHash: string + ): Promise /** * Marks the tx batch with the provided batch number as confirmed on the L1 chain. @@ -55,7 +58,10 @@ export interface L2DataService { * @param l1TxHash The L1 transaction hash for the batch submission. * @throws An error if there is a DB error. */ - markTransactionBatchConfirmedOnL1(batchNumber: number, l1TxHash: string): Promise + markTransactionBatchConfirmedOnL1( + batchNumber: number, + l1TxHash: string + ): Promise /** * Marks the state root batch with the provided batch number as submitted to the L1 chain. @@ -64,7 +70,10 @@ export interface L2DataService { * @param l1TxHash The L1 transaction hash for the batch submission. * @throws An error if there is a DB error. */ - markStateRootBatchSubmittedToL1(batchNumber: number, l1TxHash: string): Promise + markStateRootBatchSubmittedToL1( + batchNumber: number, + l1TxHash: string + ): Promise /** * Marks the state root batch with the provided batch number as confirmed on the L1 chain. @@ -73,5 +82,8 @@ export interface L2DataService { * @param l1TxHash The L1 transaction hash for the batch submission. * @throws An error if there is a DB error. */ - markStateRootBatchConfirmedOnL1(batchNumber: number, l1TxHash: string): Promise + markStateRootBatchConfirmedOnL1( + batchNumber: number, + l1TxHash: string + ): Promise } diff --git a/packages/rollup-core/src/types/data/types.ts b/packages/rollup-core/src/types/data/types.ts index 48e7497eeb119..7c6e16bd72989 100644 --- a/packages/rollup-core/src/types/data/types.ts +++ b/packages/rollup-core/src/types/data/types.ts @@ -1,4 +1,10 @@ -import {TransactionAndRoot} from '../types' +import { TransactionAndRoot } from '../types' + +export enum QueueOrigin { + L1_TO_L2_QUEUE = 0, + SAFETY_QUEUE = 1, + SEQUENCER = 2, +} export enum L2BatchStatus { UNBATCHED = 'UNBATCHED', @@ -6,7 +12,7 @@ export enum L2BatchStatus { TXS_SUBMITTED = 'TXS_SUBMITTED', TXS_CONFIRMED = 'TXS_CONFIRMED', ROOTS_SUBMITTED = 'ROOTS_SUBMITTED', - ROOTS_CONFIRMED = 'ROOTS_CONFIRMED' + ROOTS_CONFIRMED = 'ROOTS_CONFIRMED', } export interface L1BatchRecord { @@ -21,4 +27,4 @@ export interface L1BatchSubmission { status: string l2BatchNumber: number transactions: TransactionAndRoot[] -} \ No newline at end of file +} diff --git a/packages/rollup-core/src/types/types.ts b/packages/rollup-core/src/types/types.ts index 3b6267684bdb0..5e5ddc7e10bb0 100644 --- a/packages/rollup-core/src/types/types.ts +++ b/packages/rollup-core/src/types/types.ts @@ -62,6 +62,7 @@ export type LogHandler = ( l: Log, tx: TransactionResponse ) => Promise + export interface LogHandlerContext { topic: string contractAddress: Address From 55fad61a4bbecabf3a0e83fc0e8bd487e86e680a Mon Sep 17 00:00:00 2001 From: Will Meister Date: Thu, 9 Jul 2020 12:24:53 -0500 Subject: [PATCH 05/14] fixing up rollup tx data structure, table, & queries to have necessary info. --- .../rollup-core/src/app/data/data-service.ts | 95 ++++++++++++++++--- .../src/app/data/producers/log-handlers.ts | 52 ++++++++-- .../rollup-core/src/app/data/query-utils.ts | 9 +- .../src/types/data/l1-data-service.ts | 12 ++- packages/rollup-core/src/types/types.ts | 2 + .../test/app/l1-chain-data-persister.spec.ts | 5 +- .../test/app/l2-batch-submitter.spec.ts | 2 + .../test/app/l2-chain-data-persister.spec.ts | 8 +- .../test/app/l2-node-service.spec.ts | 12 ++- 9 files changed, 162 insertions(+), 35 deletions(-) diff --git a/packages/rollup-core/src/app/data/data-service.ts b/packages/rollup-core/src/app/data/data-service.ts index 6ef9d893b755d..773c0f85189e0 100644 --- a/packages/rollup-core/src/app/data/data-service.ts +++ b/packages/rollup-core/src/app/data/data-service.ts @@ -1,6 +1,6 @@ /* External Imports */ import { RDB, Row } from '@eth-optimism/core-db' -import { getLogger, logError } from '@eth-optimism/core-utils' +import { add0x, getLogger, logError } from '@eth-optimism/core-utils' import { Block, TransactionResponse } from 'ethers/providers' @@ -11,6 +11,7 @@ import { L1BatchRecord, L1BatchSubmission, L2BatchStatus, + QueueOrigin, RollupTransaction, TransactionAndRoot, VerificationCandidate, @@ -90,22 +91,26 @@ export class DefaultDataService implements DataService { */ public async insertL1RollupTransactions( l1TxHash: string, - rollupTransactions: RollupTransaction[] + rollupTransactions: RollupTransaction[], + createBatch: boolean = false ): Promise { if (!rollupTransactions || !rollupTransactions.length) { return } - let batchNumber + let batchNumber: number await this.rdb.startTransaction() try { - batchNumber = await this.insertNewL1TransactionBatch( - rollupTransactions[0].l1TxHash - ) + if (createBatch) { + batchNumber = await this.insertNewL1TransactionBatch( + rollupTransactions[0].l1TxHash + ) + } const values: string[] = rollupTransactions.map( (x) => `(${getL1RollupTransactionInsertValue(x, batchNumber)})` ) + await this.rdb.execute( `${l1RollupTxInsertStatement} VALUES ${values.join(',')}` ) @@ -128,16 +133,80 @@ export class DefaultDataService implements DataService { * @inheritDoc */ public async createNextL1ToL2Batch(): Promise { - // ***************************** TODO: THIS ************************************ - return undefined + const txHashRes = await this.rdb.select(` + SELECT l1_tx_hash, l1_tx_log_index + FROM rollup_tx + WHERE batch_number IS NULL AND queue_origin = ${QueueOrigin.L1_TO_L2_QUEUE} + ORDER BY l1_block_number ASC, l1_tx_index ASC, l1_tx_log_index ASC + LIMIT 1 + `) + if (!txHashRes || !txHashRes.length || !txHashRes[0].columns['tx_hash']) { + return undefined + } + + const txHash = txHashRes[0].columns['l1_tx_hash'] + const txLogIndex = txHashRes[0].columns['l1_tx_log_index'] + + await this.rdb.startTransaction() + try { + const batchNumber: number = await this.insertNewL1TransactionBatch(txHash) + + await this.rdb.execute(` + UPDATE rollup_tx + SET batch_number = ${batchNumber}, batch_index = 0 + WHERE l1_tx_hash = '${txHash}' AND l1_tx_log_index = ${txLogIndex} + `) + + return batchNumber + } catch (e) { + logError( + log, + `Error executing createNextL1ToL2Batch for tx hash ${txHash}... rolling back`, + e + ) + await this.rdb.rollback() + throw e + } } /** * @inheritDoc */ public async createNextSafetyQueueBatch(): Promise { - // ***************************** TODO: THIS ************************************ - return undefined + const txHashRes = await this.rdb.select(` + SELECT l1_tx_hash, l1_tx_log_index + FROM rollup_tx + WHERE batch_number IS NULL AND queue_origin = ${QueueOrigin.SAFETY_QUEUE} + ORDER BY l1_block_number ASC, l1_tx_index ASC, l1_tx_log_index ASC + LIMIT 1 + `) + if (!txHashRes || !txHashRes.length || !txHashRes[0].columns['tx_hash']) { + return undefined + } + + const txHash = txHashRes[0].columns['l1_tx_hash'] + const txLogIndex = txHashRes[0].columns['l1_tx_log_index'] + + await this.rdb.startTransaction() + try { + const batchNumber: number = await this.insertNewL1TransactionBatch(txHash) + + await this.rdb.execute(` + UPDATE rollup_tx + SET batch_number = ${batchNumber}, batch_index = 0 + WHERE l1_tx_hash = '${txHash}' AND l1_tx_log_index = ${txLogIndex} + `) + + return batchNumber + } catch (e) { + logError( + log, + `Error executing createNextSafetyQueueBatch for tx hash ${txHash}... rolling back`, + e + ) + await this.rdb.rollback() + throw e + } } /** @@ -202,7 +271,7 @@ export class DefaultDataService implements DataService { */ public async getNextBatchForL2Submission(): Promise { const res: Row[] = await this.rdb.select(` - SELECT batch_number, target, calldata, block_timestamp, block_number, l1_tx_hash, queue_origin, sender, l1_message_sender, gas_limit, nonce, signature + SELECT batch_number, target, calldata, block_timestamp, block_number, l1_tx_hash, l1_tx_index, l1_tx_log_index, queue_origin, sender, l1_message_sender, gas_limit, nonce, signature FROM next_l2_submission_batch `) @@ -222,11 +291,13 @@ export class DefaultDataService implements DataService { res.map((row: Row, batchIndex: number) => { const tx: RollupTransaction = { batchIndex, + l1TxHash: row.columns['l1_tx_hash'], + l1TxIndex: row.columns['l1_tx_index'], + l1TxLogIndex: row.columns['l1_tx_log_index'], target: row.columns['target'], calldata: row.columns['calldata'], // TODO: may have to format Buffer => string l1Timestamp: row.columns['block_timestamp'], l1BlockNumber: row.columns['block_number'], - l1TxHash: row.columns['l1_tx_hash'], queueOrigin: row.columns['queue_origin'], } diff --git a/packages/rollup-core/src/app/data/producers/log-handlers.ts b/packages/rollup-core/src/app/data/producers/log-handlers.ts index 4e9edc5f8553a..2b45e69b516e5 100644 --- a/packages/rollup-core/src/app/data/producers/log-handlers.ts +++ b/packages/rollup-core/src/app/data/producers/log-handlers.ts @@ -42,9 +42,11 @@ export const L1ToL2TxEnqueuedLogHandler = async ( let rollupTransaction: RollupTransaction try { rollupTransaction = { - l1TxHash: l.transactionHash, - l1Timestamp: tx.timestamp, l1BlockNumber: tx.blockNumber, + l1Timestamp: tx.timestamp, + l1TxHash: l.transactionHash, + l1TxIndex: l.transactionIndex, + l1TxLogIndex: l.transactionLogIndex, queueOrigin: QueueOrigin.L1_TO_L2_QUEUE, batchIndex: 0, sender: l.address, @@ -96,9 +98,11 @@ export const CalldataTxEnqueuedLogHandler = async ( // Skip the 4 bytes of MethodID const calldata = remove0x(tx.data).substr(8) rollupTransaction = { - l1TxHash: l.transactionHash, - l1Timestamp: tx.timestamp, l1BlockNumber: tx.blockNumber, + l1Timestamp: tx.timestamp, + l1TxHash: l.transactionHash, + l1TxIndex: l.transactionIndex, + l1TxLogIndex: l.transactionLogIndex, queueOrigin: QueueOrigin.SAFETY_QUEUE, batchIndex: 0, sender: add0x(calldata.substr(0, 40)), @@ -138,8 +142,9 @@ export const L1ToL2BatchAppendedLogHandler = async ( log.debug( `L1ToL2BatchAppended event received at block ${tx.blockNumber}, tx ${l.transactionIndex}, log: ${l.transactionLogIndex}. TxHash: ${tx.hash}` ) + let batchNumber: number try { - await ds.createNextL1ToL2Batch() + batchNumber = await ds.createNextL1ToL2Batch() } catch (e) { logError( log, @@ -148,6 +153,16 @@ export const L1ToL2BatchAppendedLogHandler = async ( ) throw e } + + if (!batchNumber) { + const msg = `Attempted to create L1 to L2 Batch upon receiving L1ToL2BatchAppended log, but no tx was available for batching!` + log.error(msg) + throw Error(msg) + } else { + log.debug( + `Successfully created L1 to L2 Batch! Batch number: ${batchNumber}` + ) + } } /** @@ -167,8 +182,10 @@ export const SafetyQueueBatchAppendedLogHandler = async ( log.debug( `SafetyQueueBatchAppended event received at block ${tx.blockNumber}, tx ${l.transactionIndex}, log: ${l.transactionLogIndex}. TxHash: ${tx.hash}` ) + let batchNumber: number + try { - await ds.createNextSafetyQueueBatch() + batchNumber = await ds.createNextSafetyQueueBatch() } catch (e) { logError( log, @@ -177,6 +194,16 @@ export const SafetyQueueBatchAppendedLogHandler = async ( ) throw e } + + if (!batchNumber) { + const msg = `Attempted to create Safety Queue Batch upon receiving L1ToL2BatchAppended log, but no tx was available for batching!` + log.error(msg) + throw Error(msg) + } else { + log.debug( + `Successfully created Safety Queue Batch! Batch number: ${batchNumber}` + ) + } } /** @@ -219,9 +246,11 @@ export const SequencerBatchAppendedLogHandler = async ( for (let i = 0; i < transactionsBytes.length; i++) { const txBytes = transactionsBytes[i] rollupTransactions.push({ - l1TxHash: l.transactionHash, - l1Timestamp: timestamp, l1BlockNumber: tx.blockNumber, + l1Timestamp: timestamp, + l1TxHash: l.transactionHash, + l1TxIndex: l.transactionIndex, + l1TxLogIndex: l.transactionLogIndex, queueOrigin: QueueOrigin.SEQUENCER, batchIndex: i, sender: add0x(txBytes.substr(0, 40)), @@ -242,7 +271,12 @@ export const SequencerBatchAppendedLogHandler = async ( return } - await ds.insertL1RollupTransactions(l.transactionHash, rollupTransactions) + const batchNumber = await ds.insertL1RollupTransactions( + l.transactionHash, + rollupTransactions, + true + ) + log.debug(`Sequencer batch number ${batchNumber} successfully created!`) } /** diff --git a/packages/rollup-core/src/app/data/query-utils.ts b/packages/rollup-core/src/app/data/query-utils.ts index 439f5b3449562..68c2ea0c087b8 100644 --- a/packages/rollup-core/src/app/data/query-utils.ts +++ b/packages/rollup-core/src/app/data/query-utils.ts @@ -27,16 +27,19 @@ export const getL1BlockInsertValue = ( )}` } -export const l1RollupTxInsertStatement = `INSERT INTO rollup_tx(sender, l1_message_sender, target, calldata, queue_origin, nonce, gas_limit, signature, batch_number, batch_index) ` +export const l1RollupTxInsertStatement = `INSERT INTO rollup_tx(sender, l1_message_sender, target, calldata, queue_origin, nonce, gas_limit, signature, batch_number, batch_index, l1_block_number, l1_tx_hash, l1_tx_index, l1_tx_log_index) ` export const getL1RollupTransactionInsertValue = ( tx: RollupTransaction, - batchNumber: number + batchNumber?: number ): string => { + const batchNum = batchNumber || 'NULL' return `${stringOrNull(tx.sender)}, ${stringOrNull(tx.l1MessageSender)}, '${ tx.target }', '${tx.calldata}', ${tx.queueOrigin}, ${numOrNull(tx.nonce)}, ${numOrNull( tx.gasLimit - )}, ${stringOrNull(tx.signature)}, ${batchNumber}, ${tx.batchIndex}` + )}, ${stringOrNull(tx.signature)}, ${batchNum}, ${tx.batchIndex}, ${ + tx.l1BlockNumber + }, '${tx.l1TxHash}', ${tx.l1TxIndex}, ${numOrNull(tx.l1TxLogIndex)}` } export const l1RollupStateRootInsertStatement = `INSERT into l1_state_root(state_root, batch_number, batch_index) ` diff --git a/packages/rollup-core/src/types/data/l1-data-service.ts b/packages/rollup-core/src/types/data/l1-data-service.ts index 4c1418fb1437b..1f949e6ee975a 100644 --- a/packages/rollup-core/src/types/data/l1-data-service.ts +++ b/packages/rollup-core/src/types/data/l1-data-service.ts @@ -57,26 +57,28 @@ export interface L1DataService { * * @param l1TxHash The L1 Transaction hash. * @param rollupTransactions The RollupTransactions to insert. - * @returns The inserted transaction batch number. + * @param createBatch Whether or not to create a batch from the provided RollupTransactions (whether or not they're part of the canonical chain). + * @returns The inserted transaction batch number if a batch was created. * @throws An error if there is a DB error. */ insertL1RollupTransactions( l1TxHash: string, - rollupTransactions: RollupTransaction[] + rollupTransactions: RollupTransaction[], + createBatch?: boolean ): Promise /** * Creates a batch from the oldest un-batched transaction that is from the L1 To L2 queue. * - * @returns The created batch number - * @throws Error if there is a DB error or no such transaction exists. + * @returns The created batch number or undefined if no fitting L1ToL2 transaction exists. + * @throws Error if there is a DB error */ createNextL1ToL2Batch(): Promise /** * Creates a batch from the oldest un-batched transaction that is from the Safety queue. * - * @returns The created batch number + * @returns The created batch number or undefined if no fitting Safety Queue transaction exists. * @throws Error if there is a DB error or no such transaction exists. */ createNextSafetyQueueBatch(): Promise diff --git a/packages/rollup-core/src/types/types.ts b/packages/rollup-core/src/types/types.ts index 5e5ddc7e10bb0..e85d3cf660da3 100644 --- a/packages/rollup-core/src/types/types.ts +++ b/packages/rollup-core/src/types/types.ts @@ -26,7 +26,9 @@ export interface RollupTransaction { gasLimit?: number l1Timestamp: number l1BlockNumber: number + l1TxIndex: number l1TxHash: string + l1TxLogIndex?: number nonce?: number queueOrigin: number signature?: string diff --git a/packages/rollup-core/test/app/l1-chain-data-persister.spec.ts b/packages/rollup-core/test/app/l1-chain-data-persister.spec.ts index 7de4416723378..6903912139f9f 100644 --- a/packages/rollup-core/test/app/l1-chain-data-persister.spec.ts +++ b/packages/rollup-core/test/app/l1-chain-data-persister.spec.ts @@ -72,7 +72,8 @@ class MockDataService extends DefaultDataService { public async insertL1RollupTransactions( l1TxHash: string, - rollupTransactions: RollupTransaction[] + rollupTransactions: RollupTransaction[], + createBatch: boolean = false ): Promise { this.rollupTransactions.set(l1TxHash, rollupTransactions) return this.rollupTransactions.size @@ -136,6 +137,8 @@ const getRollupTransaction = (): RollupTransaction => { l1Timestamp: 0, l1BlockNumber: 0, l1TxHash: keccak256FromUtf8('0xdeadbeef'), + l1TxIndex: 0, + l1TxLogIndex: 0, nonce: 0, queueOrigin: 0, } diff --git a/packages/rollup-core/test/app/l2-batch-submitter.spec.ts b/packages/rollup-core/test/app/l2-batch-submitter.spec.ts index 3eacfa9cf4210..04477e5924b23 100644 --- a/packages/rollup-core/test/app/l2-batch-submitter.spec.ts +++ b/packages/rollup-core/test/app/l2-batch-submitter.spec.ts @@ -77,6 +77,8 @@ describe('L2 Batch Submitter', () => { l1Timestamp: 1, l1BlockNumber: 1, l1TxHash: keccak256FromUtf8('tx hash'), + l1TxIndex: 0, + l1TxLogIndex: 0, queueOrigin: 1, }, ], diff --git a/packages/rollup-core/test/app/l2-chain-data-persister.spec.ts b/packages/rollup-core/test/app/l2-chain-data-persister.spec.ts index 2478761a93cec..95663f8601d5d 100644 --- a/packages/rollup-core/test/app/l2-chain-data-persister.spec.ts +++ b/packages/rollup-core/test/app/l2-chain-data-persister.spec.ts @@ -15,13 +15,17 @@ import { /* Internal Imports */ import { L2DataService, TransactionAndRoot } from '../../src/types' -import {CHAIN_ID, DefaultDataService, L2ChainDataPersister} from '../../src/app' +import { + CHAIN_ID, + DefaultDataService, + L2ChainDataPersister, +} from '../../src/app' class MockDataService extends DefaultDataService { public readonly transactionAndRoots: TransactionAndRoot[] = [] constructor() { - super(undefined); + super(undefined) } public async insertL2Transaction(transaction: TransactionAndRoot) { diff --git a/packages/rollup-core/test/app/l2-node-service.spec.ts b/packages/rollup-core/test/app/l2-node-service.spec.ts index 2bab87ceead60..bd3dc550dd204 100644 --- a/packages/rollup-core/test/app/l2-node-service.spec.ts +++ b/packages/rollup-core/test/app/l2-node-service.spec.ts @@ -11,7 +11,7 @@ import { Wallet } from 'ethers' import { JsonRpcProvider } from 'ethers/providers' /* Internal Imports */ -import { BlockBatches, RollupTransaction } from '../../src/types' +import { BlockBatches, QueueOrigin, RollupTransaction } from '../../src/types' import { DefaultL2NodeService } from '../../src/app' import { verifyMessage } from 'ethers/utils' @@ -58,7 +58,9 @@ const rollupTx: RollupTransaction = { l1Timestamp: timestamp, l1BlockNumber: blockNumber, l1TxHash, - queueOrigin: 1, + l1TxIndex: 0, + l1TxLogIndex: 0, + queueOrigin: QueueOrigin.SAFETY_QUEUE, } const nonce2: number = 1 @@ -76,7 +78,9 @@ const rollupTx2: RollupTransaction = { l1Timestamp: timestamp, l1BlockNumber: blockNumber, l1TxHash, - queueOrigin: 1, + l1TxIndex: 0, + l1TxLogIndex: 1, + queueOrigin: QueueOrigin.SAFETY_QUEUE, } const deserializeBlockBatches = (serialized: string): BlockBatches => { @@ -90,6 +94,8 @@ const deserializeBlockBatches = (serialized: string): BlockBatches => { case 'l1BlockNumber': case 'l1Timestamp': case 'queueOrigin': + case 'l1TxIndex': + case 'l1TxLogIndex': return hexStrToNumber(v) default: return v From bb42a0bca265bde8e3cfeaad3d4c58418e1a87e0 Mon Sep 17 00:00:00 2001 From: Will Meister Date: Thu, 9 Jul 2020 15:22:11 -0500 Subject: [PATCH 06/14] Adding L1BatchSubmitter tests --- packages/core-utils/src/app/log.ts | 2 +- .../app/data/consumers/l1-batch-submitter.ts | 1 - .../test/app/l1-batch-submitter.spec.ts | 601 ++++++++++++++++++ 3 files changed, 602 insertions(+), 2 deletions(-) create mode 100644 packages/rollup-core/test/app/l1-batch-submitter.spec.ts diff --git a/packages/core-utils/src/app/log.ts b/packages/core-utils/src/app/log.ts index bf316467a9464..2b562d315c096 100644 --- a/packages/core-utils/src/app/log.ts +++ b/packages/core-utils/src/app/log.ts @@ -1,7 +1,7 @@ import debug from 'debug' import { Logger } from '../types' -export const LOG_NEWLINE_STRING = ' <\\n> ' +export const LOG_NEWLINE_STRING = process.env.LOG_NEW_LINES ? '\n' : ' <\\n> ' /** * Gets a logger specific to the provided identifier. diff --git a/packages/rollup-core/src/app/data/consumers/l1-batch-submitter.ts b/packages/rollup-core/src/app/data/consumers/l1-batch-submitter.ts index fb8722e09b77c..257de4f603e6b 100644 --- a/packages/rollup-core/src/app/data/consumers/l1-batch-submitter.ts +++ b/packages/rollup-core/src/app/data/consumers/l1-batch-submitter.ts @@ -24,7 +24,6 @@ const log = getLogger('l2-batch-creator') export class L1BatchSubmitter extends ScheduledTask { constructor( private readonly dataService: L2DataService, - private readonly sequencerWallet: Wallet, private readonly canonicalTransactionChain: Contract, private readonly stateCommitmentChain: Contract, private readonly confirmationsUntilFinal: number = 1, diff --git a/packages/rollup-core/test/app/l1-batch-submitter.spec.ts b/packages/rollup-core/test/app/l1-batch-submitter.spec.ts new file mode 100644 index 0000000000000..325d2d50bd1c1 --- /dev/null +++ b/packages/rollup-core/test/app/l1-batch-submitter.spec.ts @@ -0,0 +1,601 @@ +/* External Imports */ +import { + keccak256FromUtf8, + sleep, + ZERO_ADDRESS, +} from '@eth-optimism/core-utils' +import { TransactionReceipt, TransactionResponse } from 'ethers/providers' +import { Wallet } from 'ethers' + +/* Internal Imports */ +import { DefaultDataService } from '../../src/app/data' +import { L1BatchSubmission, L2BatchStatus } from '../../src/types/data' +import { L1BatchSubmitter } from '../../src/app/data/consumers/l1-batch-submitter' + +interface BatchNumberHash { + batchNumber: number + txHash: string +} + +class MockDataService extends DefaultDataService { + public readonly nextBatch: L1BatchSubmission[] = [] + public readonly txBatchesSubmitted: BatchNumberHash[] = [] + public readonly txBatchesConfirmed: BatchNumberHash[] = [] + public readonly stateBatchesSubmitted: BatchNumberHash[] = [] + public readonly stateBatchesConfirmed: BatchNumberHash[] = [] + + constructor() { + super(undefined) + } + + public async getNextBatchForL1Submission(): Promise { + return this.nextBatch.shift() + } + + public async markTransactionBatchSubmittedToL1( + batchNumber: number, + l1TxHash: string + ): Promise { + this.txBatchesSubmitted.push({ batchNumber, txHash: l1TxHash }) + } + + public async markTransactionBatchConfirmedOnL1( + batchNumber: number, + l1TxHash: string + ): Promise { + this.txBatchesConfirmed.push({ batchNumber, txHash: l1TxHash }) + } + + public async markStateRootBatchSubmittedToL1( + batchNumber: number, + l1TxHash: string + ): Promise { + this.stateBatchesSubmitted.push({ batchNumber, txHash: l1TxHash }) + } + + public async markStateRootBatchConfirmedOnL1( + batchNumber: number, + l1TxHash: string + ): Promise { + this.stateBatchesConfirmed.push({ batchNumber, txHash: l1TxHash }) + } +} + +class MockProvider { + public confirmedTxs: Map = new Map< + string, + TransactionReceipt + >() + + public async waitForTransaction( + hash: string, + numConfirms: number + ): Promise { + while (!this.confirmedTxs.get(hash)) { + await sleep(100) + } + return this.confirmedTxs.get(hash) + } +} + +class MockCanonicalTransactionChain { + public responses: TransactionResponse[] = [] + + constructor(public readonly provider: MockProvider) {} + + public async appendSequencerBatch( + calldata: string, + timestamp: number + ): Promise { + const response: TransactionResponse = this.responses.shift() + if (!response) { + throw Error('no response') + } + return response + } +} + +class MockStatCommitmentChain { + public responses: TransactionResponse[] = [] + + constructor(public readonly provider: MockProvider) {} + + public async appendStateBatch( + batches: string[] + ): Promise { + const response: TransactionResponse = this.responses.shift() + if (!response) { + throw Error('no response') + } + return response + } +} + +describe.only('L1 Batch Submitter', () => { + let batchSubmitter: L1BatchSubmitter + let dataService: MockDataService + let canonicalProvider: MockProvider + let canonicalTransactionChain: MockCanonicalTransactionChain + let stateCommitmentProvider: MockProvider + let stateCommitmentChain: MockStatCommitmentChain + + beforeEach(async () => { + dataService = new MockDataService() + canonicalProvider = new MockProvider() + canonicalTransactionChain = new MockCanonicalTransactionChain( + canonicalProvider + ) + stateCommitmentProvider = new MockProvider() + stateCommitmentChain = new MockStatCommitmentChain(stateCommitmentProvider) + batchSubmitter = new L1BatchSubmitter( + dataService, + canonicalTransactionChain as any, + stateCommitmentChain as any + ) + }) + + it('should not do anything if there are no batches', async () => { + await batchSubmitter.runTask() + + dataService.txBatchesSubmitted.length.should.equal( + 0, + 'No tx batches should have been submitted!' + ) + dataService.txBatchesConfirmed.length.should.equal( + 0, + 'No tx batches should have been confirmed!' + ) + + dataService.stateBatchesSubmitted.length.should.equal( + 0, + 'No state batches should have been submitted!' + ) + dataService.stateBatchesConfirmed.length.should.equal( + 0, + 'No state batches should have been confirmed!' + ) + }) + + it('should not do anything if the next batch has an invalid status', async () => { + dataService.nextBatch.push({ + l1TxBatchTxHash: undefined, + l1StateRootBatchTxHash: undefined, + status: L2BatchStatus.UNBATCHED, + l2BatchNumber: 1, + transactions: [ + { + timestamp: 1, + blockNumber: 2, + transactionHash: keccak256FromUtf8('l2 tx hash'), + transactionIndex: 0, + to: Wallet.createRandom().address, + from: Wallet.createRandom().address, + nonce: 1, + calldata: keccak256FromUtf8('some calldata'), + stateRoot: keccak256FromUtf8('l2 state root'), + signature: 'ab'.repeat(65), + }, + ], + }) + + await batchSubmitter.runTask() + + dataService.txBatchesSubmitted.length.should.equal( + 0, + 'No tx batches should have been submitted!' + ) + dataService.txBatchesConfirmed.length.should.equal( + 0, + 'No tx batches should have been confirmed!' + ) + + dataService.stateBatchesSubmitted.length.should.equal( + 0, + 'No state batches should have been submitted!' + ) + dataService.stateBatchesConfirmed.length.should.equal( + 0, + 'No state batches should have been confirmed!' + ) + }) + + it('should send txs and roots if there is a batch', async () => { + const hash: string = keccak256FromUtf8('l1 tx hash') + const batchNumber: number = 1 + dataService.nextBatch.push({ + l1TxBatchTxHash: undefined, + l1StateRootBatchTxHash: undefined, + status: L2BatchStatus.BATCHED, + l2BatchNumber: batchNumber, + transactions: [ + { + timestamp: 1, + blockNumber: 2, + transactionHash: keccak256FromUtf8('l2 tx hash'), + transactionIndex: 0, + to: Wallet.createRandom().address, + from: Wallet.createRandom().address, + nonce: 1, + calldata: keccak256FromUtf8('some calldata'), + stateRoot: keccak256FromUtf8('l2 state root'), + signature: 'ab'.repeat(65), + }, + ], + }) + + canonicalTransactionChain.responses.push({ hash } as any) + stateCommitmentChain.responses.push({ hash } as any) + + await batchSubmitter.runTask() + + dataService.txBatchesSubmitted.length.should.equal( + 1, + 'No tx batches submitted!' + ) + dataService.txBatchesSubmitted[0].txHash.should.equal( + hash, + 'Incorrect tx hash submitted!' + ) + dataService.txBatchesSubmitted[0].batchNumber.should.equal( + batchNumber, + 'Incorrect tx batch number submitted!' + ) + + dataService.txBatchesConfirmed.length.should.equal( + 1, + 'No tx batches confirmed!' + ) + dataService.txBatchesConfirmed[0].txHash.should.equal( + hash, + 'Incorrect tx hash confirmed!' + ) + dataService.txBatchesConfirmed[0].batchNumber.should.equal( + batchNumber, + 'Incorrect tx batch number confirmed!' + ) + + dataService.stateBatchesSubmitted.length.should.equal( + 1, + 'No state batches submitted!' + ) + dataService.stateBatchesSubmitted[0].txHash.should.equal( + hash, + 'Incorrect tx hash state root confirmed!' + ) + dataService.stateBatchesSubmitted[0].batchNumber.should.equal( + batchNumber, + 'Incorrect tx batch number state root confirmed!' + ) + + dataService.stateBatchesConfirmed.length.should.equal( + 1, + 'No state batches confirmed!' + ) + dataService.stateBatchesConfirmed[0].txHash.should.equal( + hash, + 'Incorrect tx hash state root confirmed!' + ) + dataService.stateBatchesConfirmed[0].batchNumber.should.equal( + batchNumber, + 'Incorrect tx batch number state root confirmed!' + ) + }) + + it('should wait for tx confirmation and send roots if there is a batch in TXS_SUBMITTED status', async () => { + const hash: string = keccak256FromUtf8('l1 tx hash') + const batchNumber: number = 1 + dataService.nextBatch.push({ + l1TxBatchTxHash: hash, + l1StateRootBatchTxHash: undefined, + status: L2BatchStatus.TXS_SUBMITTED, + l2BatchNumber: batchNumber, + transactions: [ + { + timestamp: 1, + blockNumber: 2, + transactionHash: keccak256FromUtf8('l2 tx hash'), + transactionIndex: 0, + to: Wallet.createRandom().address, + from: Wallet.createRandom().address, + nonce: 1, + calldata: keccak256FromUtf8('some calldata'), + stateRoot: keccak256FromUtf8('l2 state root'), + signature: 'ab'.repeat(65), + }, + ], + }) + + canonicalTransactionChain.responses.push({ hash } as any) + stateCommitmentChain.responses.push({ hash } as any) + + await batchSubmitter.runTask() + + dataService.txBatchesSubmitted.length.should.equal( + 0, + 'No tx batches should have been submitted!' + ) + + dataService.txBatchesConfirmed.length.should.equal( + 1, + 'No tx batches confirmed!' + ) + dataService.txBatchesConfirmed[0].txHash.should.equal( + hash, + 'Incorrect tx hash confirmed!' + ) + dataService.txBatchesConfirmed[0].batchNumber.should.equal( + batchNumber, + 'Incorrect tx batch number confirmed!' + ) + + dataService.stateBatchesSubmitted.length.should.equal( + 1, + 'No state batches submitted!' + ) + dataService.stateBatchesSubmitted[0].txHash.should.equal( + hash, + 'Incorrect tx hash state root confirmed!' + ) + dataService.stateBatchesSubmitted[0].batchNumber.should.equal( + batchNumber, + 'Incorrect tx batch number state root confirmed!' + ) + + dataService.stateBatchesConfirmed.length.should.equal( + 1, + 'No state batches confirmed!' + ) + dataService.stateBatchesConfirmed[0].txHash.should.equal( + hash, + 'Incorrect tx hash state root confirmed!' + ) + dataService.stateBatchesConfirmed[0].batchNumber.should.equal( + batchNumber, + 'Incorrect tx batch number state root confirmed!' + ) + }) + + it('should send roots if there is a batch in TXS_CONFIRMED status', async () => { + const hash: string = keccak256FromUtf8('l1 tx hash') + const batchNumber: number = 1 + dataService.nextBatch.push({ + l1TxBatchTxHash: hash, + l1StateRootBatchTxHash: undefined, + status: L2BatchStatus.TXS_CONFIRMED, + l2BatchNumber: batchNumber, + transactions: [ + { + timestamp: 1, + blockNumber: 2, + transactionHash: keccak256FromUtf8('l2 tx hash'), + transactionIndex: 0, + to: Wallet.createRandom().address, + from: Wallet.createRandom().address, + nonce: 1, + calldata: keccak256FromUtf8('some calldata'), + stateRoot: keccak256FromUtf8('l2 state root'), + signature: 'ab'.repeat(65), + }, + ], + }) + + canonicalTransactionChain.responses.push({ hash } as any) + stateCommitmentChain.responses.push({ hash } as any) + + await batchSubmitter.runTask() + + dataService.txBatchesSubmitted.length.should.equal( + 0, + 'No tx batches should have been submitted!' + ) + + dataService.txBatchesConfirmed.length.should.equal( + 0, + 'No tx batches should have been confirmed!' + ) + + dataService.stateBatchesSubmitted.length.should.equal( + 1, + 'No state batches submitted!' + ) + dataService.stateBatchesSubmitted[0].txHash.should.equal( + hash, + 'Incorrect tx hash state root confirmed!' + ) + dataService.stateBatchesSubmitted[0].batchNumber.should.equal( + batchNumber, + 'Incorrect tx batch number state root confirmed!' + ) + + dataService.stateBatchesConfirmed.length.should.equal( + 1, + 'No state batches confirmed!' + ) + dataService.stateBatchesConfirmed[0].txHash.should.equal( + hash, + 'Incorrect tx hash state root confirmed!' + ) + dataService.stateBatchesConfirmed[0].batchNumber.should.equal( + batchNumber, + 'Incorrect tx batch number state root confirmed!' + ) + }) + + it('should wait for roots tx if there is a batch in ROOTS_SUBMITTED status', async () => { + const hash: string = keccak256FromUtf8('l1 tx hash') + const batchNumber: number = 1 + dataService.nextBatch.push({ + l1TxBatchTxHash: hash, + l1StateRootBatchTxHash: hash, + status: L2BatchStatus.ROOTS_SUBMITTED, + l2BatchNumber: batchNumber, + transactions: [ + { + timestamp: 1, + blockNumber: 2, + transactionHash: keccak256FromUtf8('l2 tx hash'), + transactionIndex: 0, + to: Wallet.createRandom().address, + from: Wallet.createRandom().address, + nonce: 1, + calldata: keccak256FromUtf8('some calldata'), + stateRoot: keccak256FromUtf8('l2 state root'), + signature: 'ab'.repeat(65), + }, + ], + }) + + canonicalTransactionChain.responses.push({ hash } as any) + stateCommitmentChain.responses.push({ hash } as any) + + await batchSubmitter.runTask() + + dataService.txBatchesSubmitted.length.should.equal( + 0, + 'No tx batches should have been submitted!' + ) + + dataService.txBatchesConfirmed.length.should.equal( + 0, + 'No tx batches should have been confirmed!' + ) + + dataService.stateBatchesSubmitted.length.should.equal( + 0, + 'No state batches should have been submitted!' + ) + + dataService.stateBatchesConfirmed.length.should.equal( + 1, + 'No state batches confirmed!' + ) + dataService.stateBatchesConfirmed[0].txHash.should.equal( + hash, + 'Incorrect tx hash state root confirmed!' + ) + dataService.stateBatchesConfirmed[0].batchNumber.should.equal( + batchNumber, + 'Incorrect tx batch number state root confirmed!' + ) + }) + + describe('waiting for confirmations', () => { + beforeEach(() => { + batchSubmitter = new L1BatchSubmitter( + dataService, + canonicalTransactionChain as any, + stateCommitmentChain as any, + 2 + ) + }) + + it('should wait for tx confirmations', async () => { + const hash: string = keccak256FromUtf8('l1 tx hash') + const batchNumber: number = 1 + dataService.nextBatch.push({ + l1TxBatchTxHash: hash, + l1StateRootBatchTxHash: undefined, + status: L2BatchStatus.TXS_SUBMITTED, + l2BatchNumber: batchNumber, + transactions: [ + { + timestamp: 1, + blockNumber: 2, + transactionHash: keccak256FromUtf8('l2 tx hash'), + transactionIndex: 0, + to: Wallet.createRandom().address, + from: Wallet.createRandom().address, + nonce: 1, + calldata: keccak256FromUtf8('some calldata'), + stateRoot: keccak256FromUtf8('l2 state root'), + signature: 'ab'.repeat(65), + }, + ], + }) + + canonicalTransactionChain.responses.push({ hash } as any) + stateCommitmentChain.responses.push({ hash } as any) + + batchSubmitter.runTask() + + await sleep(1000) + + dataService.txBatchesConfirmed.length.should.equal( + 0, + 'batch should not yet be confirmed' + ) + + canonicalProvider.confirmedTxs.set(hash, {} as any) + + await sleep(2_000) + + dataService.txBatchesConfirmed.length.should.equal( + 1, + 'No tx batches confirmed!' + ) + dataService.txBatchesConfirmed[0].txHash.should.equal( + hash, + 'Incorrect tx hash confirmed!' + ) + dataService.txBatchesConfirmed[0].batchNumber.should.equal( + batchNumber, + 'Incorrect tx batch number confirmed!' + ) + + // the rest omitted because they're confirmed in tests above + }) + + it('should wait for state root confirmations', async () => { + const hash: string = keccak256FromUtf8('l1 tx hash') + const batchNumber: number = 1 + dataService.nextBatch.push({ + l1TxBatchTxHash: hash, + l1StateRootBatchTxHash: hash, + status: L2BatchStatus.ROOTS_SUBMITTED, + l2BatchNumber: batchNumber, + transactions: [ + { + timestamp: 1, + blockNumber: 2, + transactionHash: keccak256FromUtf8('l2 tx hash'), + transactionIndex: 0, + to: Wallet.createRandom().address, + from: Wallet.createRandom().address, + nonce: 1, + calldata: keccak256FromUtf8('some calldata'), + stateRoot: keccak256FromUtf8('l2 state root'), + signature: 'ab'.repeat(65), + }, + ], + }) + + stateCommitmentChain.responses.push({ hash } as any) + + batchSubmitter.runTask() + + await sleep(1000) + + dataService.stateBatchesConfirmed.length.should.equal( + 0, + 'batch should not yet be confirmed' + ) + + stateCommitmentProvider.confirmedTxs.set(hash, {} as any) + + await sleep(2_000) + + dataService.stateBatchesConfirmed.length.should.equal( + 1, + 'No state root batches confirmed!' + ) + dataService.stateBatchesConfirmed[0].txHash.should.equal( + hash, + 'Incorrect state root hash confirmed!' + ) + dataService.stateBatchesConfirmed[0].batchNumber.should.equal( + batchNumber, + 'Incorrect state root batch number confirmed!' + ) + }) + }) +}) From d28cbce49430dedfd39ec8b1b7c80be24ed06f1f Mon Sep 17 00:00:00 2001 From: Will Meister Date: Thu, 9 Jul 2020 18:21:57 -0500 Subject: [PATCH 07/14] Added happy path tests for the Log Handlers --- .../src/app/data/consumers/index.ts | 1 + packages/rollup-core/src/app/data/index.ts | 4 +- .../src/app/data/producers/index.ts | 1 + .../src/app/data/producers/log-handlers.ts | 26 +- .../test/app/l1-batch-submitter.spec.ts | 2 +- .../rollup-core/test/app/log-handlers.spec.ts | 332 ++++++++++++++++++ 6 files changed, 351 insertions(+), 15 deletions(-) create mode 100644 packages/rollup-core/test/app/log-handlers.spec.ts diff --git a/packages/rollup-core/src/app/data/consumers/index.ts b/packages/rollup-core/src/app/data/consumers/index.ts index ff39044bb38e3..7e3a4489a5315 100644 --- a/packages/rollup-core/src/app/data/consumers/index.ts +++ b/packages/rollup-core/src/app/data/consumers/index.ts @@ -1,3 +1,4 @@ +export * from './l1-batch-submitter' export * from './l2-batch-creator' export * from './l2-batch-submitter' export * from './verifier' diff --git a/packages/rollup-core/src/app/data/index.ts b/packages/rollup-core/src/app/data/index.ts index 54c30899e583e..e14cc384bacf3 100644 --- a/packages/rollup-core/src/app/data/index.ts +++ b/packages/rollup-core/src/app/data/index.ts @@ -1,4 +1,4 @@ -export * from './consumers' -export * from './producers' +export * from './consumers/' +export * from './producers/' export * from './data-service' diff --git a/packages/rollup-core/src/app/data/producers/index.ts b/packages/rollup-core/src/app/data/producers/index.ts index 1eef20ee6cb78..801fc99dd8fd7 100644 --- a/packages/rollup-core/src/app/data/producers/index.ts +++ b/packages/rollup-core/src/app/data/producers/index.ts @@ -1,3 +1,4 @@ export * from './chain-data-processor' export * from './l1-chain-data-persister' export * from './l2-chain-data-persister' +export * from './log-handlers' diff --git a/packages/rollup-core/src/app/data/producers/log-handlers.ts b/packages/rollup-core/src/app/data/producers/log-handlers.ts index 2b45e69b516e5..cc9cf9be19c76 100644 --- a/packages/rollup-core/src/app/data/producers/log-handlers.ts +++ b/packages/rollup-core/src/app/data/producers/log-handlers.ts @@ -39,6 +39,8 @@ export const L1ToL2TxEnqueuedLogHandler = async ( `L1ToL2TxEnqueued event received at block ${tx.blockNumber}, tx ${l.transactionIndex}, log: ${l.transactionLogIndex}. TxHash: ${tx.hash}. Log Data: ${l.data}` ) + const data: string = remove0x(l.data) + let rollupTransaction: RollupTransaction try { rollupTransaction = { @@ -50,11 +52,11 @@ export const L1ToL2TxEnqueuedLogHandler = async ( queueOrigin: QueueOrigin.L1_TO_L2_QUEUE, batchIndex: 0, sender: l.address, - l1MessageSender: add0x(l.data.substr(0, 40)), - target: add0x(l.data.substr(40, 40)), + l1MessageSender: add0x(data.substr(0, 40)), + target: add0x(data.substr(40, 40)), // TODO: Change gasLimit to a BigNumber so it can support 256 bits - gasLimit: new BigNumber(l.data.substr(80, 64), 'hex').toNumber(), - calldata: add0x(l.data.substr(144)), + gasLimit: new BigNumber(data.substr(80, 64), 'hex').toNumber(), + calldata: add0x(data.substr(144)), } } catch (e) { // This is, by definition, just an ill-formatted, and therefore invalid, tx. @@ -96,7 +98,7 @@ export const CalldataTxEnqueuedLogHandler = async ( let rollupTransaction: RollupTransaction try { // Skip the 4 bytes of MethodID - const calldata = remove0x(tx.data).substr(8) + const calldata = remove0x(ethers.utils.hexDataSlice(tx.data, 4)) rollupTransaction = { l1BlockNumber: tx.blockNumber, l1Timestamp: tx.timestamp, @@ -111,8 +113,8 @@ export const CalldataTxEnqueuedLogHandler = async ( nonce: new BigNumber(calldata.substr(80, 64), 'hex').toNumber(), // TODO: Change gasLimit to a BigNumber so it can support 256 bits gasLimit: new BigNumber(calldata.substr(144, 64), 'hex').toNumber(), - signature: add0x(calldata.substr(210, 65)), - calldata: add0x(calldata.substr(275)), + signature: add0x(calldata.substr(208, 130)), + calldata: add0x(calldata.substr(338)), } } catch (e) { // This is, by definition, just an ill-formatted, and therefore invalid, tx. @@ -235,7 +237,7 @@ export const SequencerBatchAppendedLogHandler = async ( ) const rollupTransactions: RollupTransaction[] = [] - let timestamp: number + let timestamp: any try { let transactionsBytes: string[] ;[transactionsBytes, timestamp] = abi.decode( @@ -244,10 +246,10 @@ export const SequencerBatchAppendedLogHandler = async ( ) for (let i = 0; i < transactionsBytes.length; i++) { - const txBytes = transactionsBytes[i] + const txBytes = remove0x(transactionsBytes[i]) rollupTransactions.push({ l1BlockNumber: tx.blockNumber, - l1Timestamp: timestamp, + l1Timestamp: timestamp.toNumber(), l1TxHash: l.transactionHash, l1TxIndex: l.transactionIndex, l1TxLogIndex: l.transactionLogIndex, @@ -259,8 +261,8 @@ export const SequencerBatchAppendedLogHandler = async ( nonce: new BigNumber(txBytes.substr(80, 64), 'hex').toNumber(), // TODO: Change gasLimit to a BigNumber so it can support 256 bits gasLimit: new BigNumber(txBytes.substr(144, 64), 'hex').toNumber(), - signature: add0x(txBytes.substr(210, 65)), - calldata: add0x(txBytes.substr(275)), + signature: add0x(txBytes.substr(208, 130)), + calldata: add0x(txBytes.substr(338)), }) } } catch (e) { diff --git a/packages/rollup-core/test/app/l1-batch-submitter.spec.ts b/packages/rollup-core/test/app/l1-batch-submitter.spec.ts index 325d2d50bd1c1..565a5ae49df70 100644 --- a/packages/rollup-core/test/app/l1-batch-submitter.spec.ts +++ b/packages/rollup-core/test/app/l1-batch-submitter.spec.ts @@ -111,7 +111,7 @@ class MockStatCommitmentChain { } } -describe.only('L1 Batch Submitter', () => { +describe('L1 Batch Submitter', () => { let batchSubmitter: L1BatchSubmitter let dataService: MockDataService let canonicalProvider: MockProvider diff --git a/packages/rollup-core/test/app/log-handlers.spec.ts b/packages/rollup-core/test/app/log-handlers.spec.ts new file mode 100644 index 0000000000000..64c8564071c14 --- /dev/null +++ b/packages/rollup-core/test/app/log-handlers.spec.ts @@ -0,0 +1,332 @@ +/* External Imports */ +import { + keccak256FromUtf8, + remove0x, + ZERO_ADDRESS, +} from '@eth-optimism/core-utils' + +import { ethers, Wallet } from 'ethers' +import { Log } from 'ethers/providers' +import { TransactionResponse } from 'ethers/providers/abstract-provider' + +/* Internal Imports */ +import { + L1ToL2TxEnqueuedLogHandler, + DefaultDataService, + RollupTransaction, + QueueOrigin, + CalldataTxEnqueuedLogHandler, + L1ToL2BatchAppendedLogHandler, + SafetyQueueBatchAppendedLogHandler, + SequencerBatchAppendedLogHandler, + StateBatchAppendedLogHandler, +} from '../../src' + +const abi = new ethers.utils.AbiCoder() + +const createLog = ( + data: string, + txHash: string = keccak256FromUtf8('tx hash'), + txIndex: number = 0, + txLogIndex: number = 0 +): Log => { + return { + blockNumber: 1, + blockHash: keccak256FromUtf8('block'), + transactionHash: txHash, + transactionIndex: txIndex, + transactionLogIndex: txLogIndex, + address: ZERO_ADDRESS, + data, + logIndex: txLogIndex, + topics: [], + } +} + +const createTx = ( + calldata: string, + txHash: string = keccak256FromUtf8('tx hash'), + blockNumber: number = 1, + timestamp: number = 1 +): TransactionResponse => { + return { + hash: txHash, + blockHash: keccak256FromUtf8('block'), + blockNumber, + timestamp, + from: Wallet.createRandom().address, + to: Wallet.createRandom().address, + nonce: 1, + data: calldata, + chainId: 108, + confirmations: 0, + gasLimit: undefined, + gasPrice: undefined, + value: undefined, + wait: undefined, + } +} + +class MockDataService extends DefaultDataService { + public createdL1ToL2Batches: number = 0 + public createdSafetyQueueBatches: number = 0 + public rollupTransactionsInserted: RollupTransaction[][] = [] + public txHashToRollupRootsInserted: Map = new Map< + string, + string[] + >() + + public txHashBatchesCreated: Set = new Set() + + constructor() { + super(undefined) + } + + public async createNextL1ToL2Batch(): Promise { + return ++this.createdL1ToL2Batches + } + + public async createNextSafetyQueueBatch(): Promise { + return ++this.createdSafetyQueueBatches + } + + public async insertL1RollupTransactions( + l1TxHash: string, + rollupTransactions: RollupTransaction[], + createBatch: boolean = false + ): Promise { + this.rollupTransactionsInserted.push(rollupTransactions) + if (createBatch) { + this.txHashBatchesCreated.add(l1TxHash) + return this.txHashBatchesCreated.size + } + return undefined + } + + public async insertL1RollupStateRoots( + l1TxHash: string, + stateRoots: string[] + ): Promise { + this.txHashToRollupRootsInserted.set(l1TxHash, stateRoots) + return undefined + } +} + +describe('Log Handlers', () => { + let dataService: MockDataService + beforeEach(() => { + dataService = new MockDataService() + }) + + it('should parse and insert L1ToL2Tx', async () => { + const sender: string = 'aa'.repeat(20) + const target: string = 'bb'.repeat(20) + const gasLimit: string = '00'.repeat(32) + const calldata: string = 'abcd'.repeat(40) + + const data = `0x${sender}${target}${gasLimit}${calldata}` + + const l = createLog(data) + const tx = createTx('00'.repeat(64)) + + await L1ToL2TxEnqueuedLogHandler(dataService, l, tx) + + dataService.rollupTransactionsInserted.length.should.equal( + 1, + `No tx batch inserted!` + ) + dataService.rollupTransactionsInserted[0].length.should.equal( + 1, + `No tx inserted!` + ) + const received: RollupTransaction = + dataService.rollupTransactionsInserted[0][0] + + received.l1BlockNumber.should.equal(tx.blockNumber, 'Block number mismatch') + received.l1Timestamp.should.equal(tx.timestamp, 'Timestamp mismatch') + received.l1TxHash.should.equal(l.transactionHash, 'Tx hash mismatch') + received.l1TxIndex.should.equal(l.transactionIndex, 'Tx index mismatch') + received.l1TxLogIndex.should.equal(l.logIndex, 'Tx log index mismatch') + received.queueOrigin.should.equal( + QueueOrigin.L1_TO_L2_QUEUE, + 'Queue Origin mismatch' + ) + received.batchIndex.should.equal(0, 'Batch index mismatch') + received.sender.should.equal(l.address, 'Sender mismatch') + remove0x(received.l1MessageSender).should.equal( + sender, + 'L1 Message Sender mismatch' + ) + remove0x(received.target).should.equal(target, 'Target mismatch') + received.gasLimit.should.equal(0, 'Gas Limit mismatch') + remove0x(received.calldata).should.equal(calldata, 'Calldata mismatch') + + dataService.txHashBatchesCreated.size.should.equal( + 0, + 'Should not have created batch!' + ) + }) + + it('should parse and insert Slow Queue Tx', async () => { + const sender: string = 'aa'.repeat(20) + const target: string = 'bb'.repeat(20) + const nonce: string = '00'.repeat(32) + const gasLimit: string = '00'.repeat(31) + '01' + const signature: string = '99'.repeat(65) + const calldata: string = 'abcd'.repeat(40) + + const data = `0x22222222${sender}${target}${nonce}${gasLimit}${signature}${calldata}` + + const l = createLog('00'.repeat(64)) + const tx = createTx(data) + + await CalldataTxEnqueuedLogHandler(dataService, l, tx) + + dataService.rollupTransactionsInserted.length.should.equal( + 1, + `No tx batch inserted!` + ) + dataService.rollupTransactionsInserted[0].length.should.equal( + 1, + `No tx inserted!` + ) + const received: RollupTransaction = + dataService.rollupTransactionsInserted[0][0] + + received.l1BlockNumber.should.equal(tx.blockNumber, 'Block number mismatch') + received.l1Timestamp.should.equal(tx.timestamp, 'Timestamp mismatch') + received.l1TxHash.should.equal(l.transactionHash, 'Tx hash mismatch') + received.l1TxIndex.should.equal(l.transactionIndex, 'Tx index mismatch') + received.l1TxLogIndex.should.equal(l.logIndex, 'Tx log index mismatch') + received.queueOrigin.should.equal( + QueueOrigin.SAFETY_QUEUE, + 'Queue Origin mismatch' + ) + received.batchIndex.should.equal(0, 'Batch index mismatch') + remove0x(received.sender).should.equal(sender, 'Sender mismatch') + remove0x(received.target).should.equal(target, 'Target mismatch') + received.nonce.should.equal(0, 'Nonce mismatch') + received.gasLimit.should.equal(1, 'Gas Limit mismatch') + remove0x(received.signature).should.equal(signature, 'Signature mismatch') + remove0x(received.calldata).should.equal(calldata, 'Calldata mismatch') + + dataService.txHashBatchesCreated.size.should.equal( + 0, + 'Should not have created batch!' + ) + }) + + it('should append L1ToL2Batch on L1ToL2BatchAppendedLogHandler call', async () => { + dataService.createdL1ToL2Batches.should.equal( + 0, + 'starting batch count should be 0!' + ) + await L1ToL2BatchAppendedLogHandler( + dataService, + createLog(''), + createTx('') + ) + dataService.createdL1ToL2Batches.should.equal(1, 'batch not created!') + }) + + it('should append L1ToL2Batch on L1ToL2BatchAppendedLogHandler call', async () => { + dataService.createdSafetyQueueBatches.should.equal( + 0, + 'starting batch count should be 0!' + ) + await SafetyQueueBatchAppendedLogHandler( + dataService, + createLog(''), + createTx('') + ) + dataService.createdSafetyQueueBatches.should.equal(1, 'batch not created!') + }) + + it('should parse and insert Sequencer Batch', async () => { + const timestamp = 1 + + const sender: string = 'aa'.repeat(20) + const target: string = 'bb'.repeat(20) + const nonce: string = '00'.repeat(32) + const gasLimit: string = '00'.repeat(31) + '01' + const signature: string = '99'.repeat(65) + const calldata: string = 'abcd'.repeat(40) + + let data = `0x${sender}${target}${nonce}${gasLimit}${signature}${calldata}` + data = abi.encode(['bytes[]', 'uint256'], [[data, data, data], timestamp]) + + const l = createLog('00'.repeat(64)) + const tx = createTx(`0x22222222${remove0x(data)}`) + + await SequencerBatchAppendedLogHandler(dataService, l, tx) + + dataService.rollupTransactionsInserted.length.should.equal( + 1, + `No tx batch inserted!` + ) + dataService.rollupTransactionsInserted[0].length.should.equal( + 3, + `Tx inserted count mismatch!` + ) + for (let i = 0; i < dataService.rollupTransactionsInserted[0].length; i++) { + const received = dataService.rollupTransactionsInserted[0][i] + + received.l1BlockNumber.should.equal( + tx.blockNumber, + 'Block number mismatch' + ) + received.l1Timestamp.should.equal(tx.timestamp, 'Timestamp mismatch') + received.l1TxHash.should.equal(l.transactionHash, 'Tx hash mismatch') + received.l1TxIndex.should.equal(l.transactionIndex, 'Tx index mismatch') + received.l1TxLogIndex.should.equal(l.logIndex, 'Tx log index mismatch') + received.queueOrigin.should.equal( + QueueOrigin.SEQUENCER, + 'Queue Origin mismatch' + ) + received.batchIndex.should.equal(i, 'Batch index mismatch') + remove0x(received.sender).should.equal(sender, 'Sender mismatch') + remove0x(received.target).should.equal(target, 'Target mismatch') + received.nonce.should.equal(0, 'Nonce mismatch') + received.gasLimit.should.equal(1, 'Gas Limit mismatch') + remove0x(received.signature).should.equal(signature, 'Signature mismatch') + remove0x(received.calldata).should.equal(calldata, 'Calldata mismatch') + } + + dataService.txHashBatchesCreated.size.should.equal( + 1, + 'Should have created batch!' + ) + }) + + it('should parse and insert State Batch', async () => { + const timestamp = 1 + + const roots: string[] = [ + keccak256FromUtf8('1'), + keccak256FromUtf8('2'), + keccak256FromUtf8('3'), + ] + const data = abi.encode(['bytes32[]'], [roots]) + + const l = createLog('00'.repeat(64)) + const tx = createTx(`0x22222222${remove0x(data)}`) + + await StateBatchAppendedLogHandler(dataService, l, tx) + + dataService.txHashToRollupRootsInserted.size.should.equal( + 1, + `No root batch inserted!` + ) + dataService.txHashToRollupRootsInserted + .get(tx.hash) + .length.should.equal(3, `State root inserted count mismatch!`) + for ( + let i = 0; + i < dataService.txHashToRollupRootsInserted.get(tx.hash).length; + i++ + ) { + const received = dataService.txHashToRollupRootsInserted.get(tx.hash)[i] + received.should.equal(roots[i], `Root ${i} mismatch!`) + } + }) +}) From c802c9a992b678e0f160b6e8d3a079be21ebd5a9 Mon Sep 17 00:00:00 2001 From: Will Meister Date: Thu, 9 Jul 2020 23:30:23 -0500 Subject: [PATCH 08/14] merging in master --- .github/workflows/dev-ecr-deploy.yml | 109 +++++++++--------- .../chain/CanonicalTransactionChain.spec.ts | 2 +- packages/core-utils/src/app/misc.ts | 7 +- 3 files changed, 61 insertions(+), 57 deletions(-) diff --git a/.github/workflows/dev-ecr-deploy.yml b/.github/workflows/dev-ecr-deploy.yml index 3fa04c21cc94a..416ca7aa9c348 100644 --- a/.github/workflows/dev-ecr-deploy.yml +++ b/.github/workflows/dev-ecr-deploy.yml @@ -1,54 +1,55 @@ -name: Build & Tag Container, Push to ECR, Deploy to Dev - -on: - push: - branches: - - master - -jobs: - build: - name: Build, Tag & push to ECR, Deploy task - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: Setup node - uses: actions/setup-node@v1 - - - name: Install Dependencies - run: yarn install - - - name: Build - run: | - yarn clean - yarn build - - - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v1 - with: - aws-access-key-id: ${{ secrets.AWS_CI_USER_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_CI_USER_SECRET_ACCESS_KEY }} - aws-region: us-east-2 - - - name: Login to Amazon ECR - id: login-ecr - uses: aws-actions/amazon-ecr-login@v1 - - - name: Build, tag, and push image to Amazon ECR - env: - ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} - ECR_REPOSITORY: optimism/rollup-full-node - IMAGE_TAG: latest - run: | - docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . - docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG - - - name: Stop existing dev-rollup-full-node ECS task to auto-start task with new image - run: | - ./.github/scripts/stop-ecs-task.sh dev-full-node full-node - ./.github/scripts/stop-ecs-task.sh synthetix-dev-full-node full-node - - - - name: Logout of Amazon ECR - if: always() - run: docker logout ${{ steps.login-ecr.outputs.registry }} +# TODO: Uncomment when Dev works +#name: Build & Tag Container, Push to ECR, Deploy to Dev +# +#on: +# push: +# branches: +# - master +# +#jobs: +# build: +# name: Build, Tag & push to ECR, Deploy task +# runs-on: ubuntu-latest +# +# steps: +# - uses: actions/checkout@v2 +# - name: Setup node +# uses: actions/setup-node@v1 +# +# - name: Install Dependencies +# run: yarn install +# +# - name: Build +# run: | +# yarn clean +# yarn build +# +# - name: Configure AWS Credentials +# uses: aws-actions/configure-aws-credentials@v1 +# with: +# aws-access-key-id: ${{ secrets.AWS_CI_USER_ACCESS_KEY_ID }} +# aws-secret-access-key: ${{ secrets.AWS_CI_USER_SECRET_ACCESS_KEY }} +# aws-region: us-east-2 +# +# - name: Login to Amazon ECR +# id: login-ecr +# uses: aws-actions/amazon-ecr-login@v1 +# +# - name: Build, tag, and push image to Amazon ECR +# env: +# ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} +# ECR_REPOSITORY: optimism/rollup-full-node +# IMAGE_TAG: latest +# run: | +# docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . +# docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG +# +# - name: Stop existing dev-rollup-full-node ECS task to auto-start task with new image +# run: | +# ./.github/scripts/stop-ecs-task.sh dev-full-node full-node +# ./.github/scripts/stop-ecs-task.sh synthetix-dev-full-node full-node +# +# +# - name: Logout of Amazon ECR +# if: always() +# run: docker logout ${{ steps.login-ecr.outputs.registry }} diff --git a/packages/contracts/test/contracts/chain/CanonicalTransactionChain.spec.ts b/packages/contracts/test/contracts/chain/CanonicalTransactionChain.spec.ts index 64d90bfcb58a9..36fdd889f2287 100644 --- a/packages/contracts/test/contracts/chain/CanonicalTransactionChain.spec.ts +++ b/packages/contracts/test/contracts/chain/CanonicalTransactionChain.spec.ts @@ -3,7 +3,7 @@ import '../../setup' /* External Imports */ import { ethers } from '@nomiclabs/buidler' import { getLogger, sleep, TestUtils } from '@eth-optimism/core-utils' -import { Signer, ContractFactory } from 'ethers' +import { Contract, Signer, ContractFactory } from 'ethers' /* Internal Imports */ import { diff --git a/packages/core-utils/src/app/misc.ts b/packages/core-utils/src/app/misc.ts index 10c004a6800e1..38873bdc550c0 100644 --- a/packages/core-utils/src/app/misc.ts +++ b/packages/core-utils/src/app/misc.ts @@ -167,10 +167,13 @@ export const bnToHexString = (bn: BigNumber): string => { * @param padToBytes the number of numeric bytes the resulting string should be, -1 if no padding should be done. * @returns the JavaScript number as a string. */ -export const numberToHexString = (number: number, padToBytes: number = -1): string => { +export const numberToHexString = ( + number: number, + padToBytes: number = -1 +): string => { let str = number.toString(16) if (padToBytes > 0 || str.length < padToBytes * 2) { - str = `${'0'.repeat(padToBytes*2 - str.length)}${str}` + str = `${'0'.repeat(padToBytes * 2 - str.length)}${str}` } return add0x(str) } From da5e08583c645780fdbc373e4824d36a7d396833 Mon Sep 17 00:00:00 2001 From: Will Meister Date: Fri, 10 Jul 2020 15:05:47 -0500 Subject: [PATCH 09/14] Update packages/core-utils/src/app/misc.ts Co-authored-by: ben-chain --- packages/core-utils/src/app/misc.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core-utils/src/app/misc.ts b/packages/core-utils/src/app/misc.ts index 38873bdc550c0..35fb0ee9f73b6 100644 --- a/packages/core-utils/src/app/misc.ts +++ b/packages/core-utils/src/app/misc.ts @@ -162,7 +162,7 @@ export const bnToHexString = (bn: BigNumber): string => { } /** - * Converts a JavaScript number to a hex string. + * Converts a JavaScript number to a big-endian hex string. * @param number the JavaScript number to be converted. * @param padToBytes the number of numeric bytes the resulting string should be, -1 if no padding should be done. * @returns the JavaScript number as a string. From 4e8f267feab766ea10d9965f34d36163b497fd0a Mon Sep 17 00:00:00 2001 From: Will Meister Date: Mon, 13 Jul 2020 13:18:17 -0500 Subject: [PATCH 10/14] Removing sender address from rollup format in favor of recovering it from the signature --- packages/core-utils/src/app/constants.ts | 1 + .../app/data/consumers/l1-batch-submitter.ts | 14 +- .../src/app/data/producers/log-handlers.ts | 150 ++++++++++++++---- .../test/app/l1-batch-submitter.spec.ts | 6 +- .../rollup-core/test/app/log-handlers.spec.ts | 72 +++++++-- 5 files changed, 192 insertions(+), 51 deletions(-) diff --git a/packages/core-utils/src/app/constants.ts b/packages/core-utils/src/app/constants.ts index aab266df5fd2c..f56a2749956e8 100644 --- a/packages/core-utils/src/app/constants.ts +++ b/packages/core-utils/src/app/constants.ts @@ -1 +1,2 @@ export const ZERO_ADDRESS = '0x' + '00'.repeat(20) +export const INVALID_ADDRESS = '0xdeaDDeADDEaDdeaDdEAddEADDEAdDeadDEADDEaD' diff --git a/packages/rollup-core/src/app/data/consumers/l1-batch-submitter.ts b/packages/rollup-core/src/app/data/consumers/l1-batch-submitter.ts index 257de4f603e6b..1f5cb0755ee8e 100644 --- a/packages/rollup-core/src/app/data/consumers/l1-batch-submitter.ts +++ b/packages/rollup-core/src/app/data/consumers/l1-batch-submitter.ts @@ -283,12 +283,11 @@ export class L1BatchSubmitter extends ScheduledTask { /** * Gets the calldata bytes for a transaction batch to be submitted by the sequencer. * Rollup Transaction Format: - * sender: 20-byte address 0-20 - * target: 20-byte address 20-40 - * nonce: 32-byte uint 40-72 - * gasLimit: 32-byte uint 72-104 - * signature: 65-byte bytes 104-169 - * calldata: bytes 169-end + * target: 20-byte address 0-20 + * nonce: 32-byte uint 20-52 + * gasLimit: 32-byte uint 52-84 + * signature: 65-byte bytes 84-149 + * calldata: bytes 149-end * * @param batch The batch to turn into ABI-encoded calldata bytes. * @returns The ABI-encoded bytes[] of the Rollup Transactions in the format listed above. @@ -296,14 +295,13 @@ export class L1BatchSubmitter extends ScheduledTask { private getTransactionBatchCalldata(batch: L1BatchSubmission): string[] { const txs: string[] = [] for (const tx of batch.transactions) { - const to: string = remove0x(tx.to) const nonce: string = remove0x(numberToHexString(tx.nonce, 32)) const gasLimit: string = tx.gasLimit ? tx.gasLimit.toString('hex', 64) : '00'.repeat(32) const signature: string = remove0x(tx.signature) const calldata: string = remove0x(tx.calldata) - txs.push(`${tx.from}${to}${nonce}${gasLimit}${signature}${calldata}`) + txs.push(`${tx.to}${nonce}${gasLimit}${signature}${calldata}`) } return txs diff --git a/packages/rollup-core/src/app/data/producers/log-handlers.ts b/packages/rollup-core/src/app/data/producers/log-handlers.ts index cc9cf9be19c76..6ad11640305bb 100644 --- a/packages/rollup-core/src/app/data/producers/log-handlers.ts +++ b/packages/rollup-core/src/app/data/producers/log-handlers.ts @@ -6,11 +6,26 @@ import { logError, remove0x, } from '@eth-optimism/core-utils' -import { Log, TransactionResponse } from 'ethers/providers/abstract-provider' +import { + Log, + TransactionRequest, + TransactionResponse, +} from 'ethers/providers/abstract-provider' import { ethers } from 'ethers' /* Internal Imports */ -import { L1DataService, QueueOrigin, RollupTransaction } from '../../../types' +import { + Address, + L1DataService, + QueueOrigin, + RollupTransaction, +} from '../../../types' +import { CHAIN_ID } from '../../constants' +import { + joinSignature, + resolveProperties, + serializeTransaction, +} from 'ethers/utils' const abi = new ethers.utils.AbiCoder() const log = getLogger('log-handler') @@ -74,12 +89,11 @@ export const L1ToL2TxEnqueuedLogHandler = async ( * from the transaction calldata and storing it in the DB. * * Assumed calldata format: - * - sender: 20-byte address 0-20 - * - target: 20-byte address 20-40 - * - nonce: 32-byte uint 40-72 - * - gasLimit: 32-byte uint 72-104 - * - signature: 65-byte bytes 104-169 - * - calldata: bytes 169-end + * - target: 20-byte address 0-20 + * - nonce: 32-byte uint 20-52 + * - gasLimit: 32-byte uint 52-84 + * - signature: 65-byte bytes 84-149 + * - calldata: bytes 149-end * * @param ds The L1DataService to use for persistence. * @param l The log event that was emitted. @@ -98,7 +112,29 @@ export const CalldataTxEnqueuedLogHandler = async ( let rollupTransaction: RollupTransaction try { // Skip the 4 bytes of MethodID - const calldata = remove0x(ethers.utils.hexDataSlice(tx.data, 4)) + const l1TxCalldata = remove0x(ethers.utils.hexDataSlice(tx.data, 4)) + + const target = add0x(l1TxCalldata.substr(0, 40)) + const nonce = new BigNumber(l1TxCalldata.substr(40, 64), 'hex') + const gasLimit = new BigNumber(l1TxCalldata.substr(104, 64), 'hex') + const signature = add0x(l1TxCalldata.substr(168, 130)) + const calldata = add0x(l1TxCalldata.substr(298)) + + const unsigned: TransactionRequest = { + to: target, + nonce: add0x(nonce.toString('hex')), + gasPrice: 0, + gasLimit: add0x(gasLimit.toString('hex')), + value: 0, + data: calldata, + chainId: CHAIN_ID, + } + + const r = add0x(signature.substr(2, 64)) + const s = add0x(signature.substr(66, 64)) + const v = parseInt(signature.substr(130, 2), 16) + const sender: string = await getTxSigner(unsigned, r, s, v) + rollupTransaction = { l1BlockNumber: tx.blockNumber, l1Timestamp: tx.timestamp, @@ -107,14 +143,14 @@ export const CalldataTxEnqueuedLogHandler = async ( l1TxLogIndex: l.transactionLogIndex, queueOrigin: QueueOrigin.SAFETY_QUEUE, batchIndex: 0, - sender: add0x(calldata.substr(0, 40)), - target: add0x(calldata.substr(40, 40)), + sender, + target, // TODO Change nonce to a BigNumber so it can support 256 bits - nonce: new BigNumber(calldata.substr(80, 64), 'hex').toNumber(), + nonce: nonce.toNumber(), // TODO: Change gasLimit to a BigNumber so it can support 256 bits - gasLimit: new BigNumber(calldata.substr(144, 64), 'hex').toNumber(), - signature: add0x(calldata.substr(208, 130)), - calldata: add0x(calldata.substr(338)), + gasLimit: gasLimit.toNumber(), + signature, + calldata, } } catch (e) { // This is, by definition, just an ill-formatted, and therefore invalid, tx. @@ -191,14 +227,14 @@ export const SafetyQueueBatchAppendedLogHandler = async ( } catch (e) { logError( log, - `Error creating next L1ToL2Batch after receiving an event to do so!`, + `Error creating next SafetyQueueBatch after receiving an event to do so!`, e ) throw e } if (!batchNumber) { - const msg = `Attempted to create Safety Queue Batch upon receiving L1ToL2BatchAppended log, but no tx was available for batching!` + const msg = `Attempted to create Safety Queue Batch upon receiving SafetyQueueBatchAppended log, but no tx was available for batching!` log.error(msg) throw Error(msg) } else { @@ -211,16 +247,15 @@ export const SafetyQueueBatchAppendedLogHandler = async ( /** * Handles the SequencerBatchAppended event by parsing: * - a list of RollupTransactions - * - L1 Block Timestamp at the time of L2 Execution + * - L1 Block Timestamp as monotonically assigned by the sequencer * from the transaction calldata and storing it in the DB. * * Assumed calldata format: - * - sender: 20-byte address 0-20 - * - target: 20-byte address 20-40 - * - nonce: 32-byte uint 40-72 - * - gasLimit: 32-byte uint 72-104 - * - signature: 65-byte bytes 104-169 - * - calldata: bytes 169-end + * - target: 20-byte address 0-20 + * - nonce: 32-byte uint 20-52 + * - gasLimit: 32-byte uint 52-84 + * - signature: 65-byte bytes 84-149 + * - calldata: bytes 149-end * * @param ds The L1DataService to use for persistence. * @param l The log event that was emitted. @@ -247,6 +282,28 @@ export const SequencerBatchAppendedLogHandler = async ( for (let i = 0; i < transactionsBytes.length; i++) { const txBytes = remove0x(transactionsBytes[i]) + + const target = add0x(txBytes.substr(0, 40)) + const nonce = new BigNumber(txBytes.substr(40, 64), 'hex') + const gasLimit = new BigNumber(txBytes.substr(104, 64), 'hex') + const signature = add0x(txBytes.substr(168, 130)) + const calldata = add0x(txBytes.substr(298)) + + const unsigned: TransactionRequest = { + to: target, + nonce: nonce.toNumber(), + gasPrice: 0, + gasLimit: add0x(gasLimit.toString('hex')), + value: 0, + data: calldata, + chainId: CHAIN_ID, + } + + const r = add0x(signature.substr(2, 64)) + const s = add0x(signature.substr(66, 64)) + const v = parseInt(signature.substr(130, 2), 16) + const sender: string = await getTxSigner(unsigned, r, s, v) + rollupTransactions.push({ l1BlockNumber: tx.blockNumber, l1Timestamp: timestamp.toNumber(), @@ -255,14 +312,14 @@ export const SequencerBatchAppendedLogHandler = async ( l1TxLogIndex: l.transactionLogIndex, queueOrigin: QueueOrigin.SEQUENCER, batchIndex: i, - sender: add0x(txBytes.substr(0, 40)), - target: add0x(txBytes.substr(40, 40)), + sender, + target, // TODO Change nonce to a BigNumber so it can support 256 bits - nonce: new BigNumber(txBytes.substr(80, 64), 'hex').toNumber(), + nonce: nonce.toNumber(), // TODO: Change gasLimit to a BigNumber so it can support 256 bits - gasLimit: new BigNumber(txBytes.substr(144, 64), 'hex').toNumber(), - signature: add0x(txBytes.substr(208, 130)), - calldata: add0x(txBytes.substr(338)), + gasLimit: gasLimit.toNumber(), + signature, + calldata, }) } } catch (e) { @@ -315,3 +372,36 @@ export const StateBatchAppendedLogHandler = async ( await ds.insertL1RollupStateRoots(l.transactionHash, stateRoots) } + +/** + * Gets the tx signer address from the Tx Request and r, s, v. + * + * @param tx The Transaction Request. + * @param r The r parameter of the signature. + * @param s The s parameter of the signature. + * @param v The v parameter of the signature. + * @returns The signer's address. + */ +const getTxSigner = async ( + tx: TransactionRequest, + r: string, + s: string, + v: number +): Promise
=> { + const txHash: string = ethers.utils.keccak256( + serializeTransaction(await resolveProperties(tx)) + ) + + try { + return ethers.utils.recoverAddress( + ethers.utils.arrayify(txHash), + joinSignature({ + s: add0x(s), + r: add0x(r), + v, + }) + ) + } catch (e) { + return undefined + } +} diff --git a/packages/rollup-core/test/app/l1-batch-submitter.spec.ts b/packages/rollup-core/test/app/l1-batch-submitter.spec.ts index 565a5ae49df70..ab682647cbcda 100644 --- a/packages/rollup-core/test/app/l1-batch-submitter.spec.ts +++ b/packages/rollup-core/test/app/l1-batch-submitter.spec.ts @@ -95,7 +95,7 @@ class MockCanonicalTransactionChain { } } -class MockStatCommitmentChain { +class MockStateCommitmentChain { public responses: TransactionResponse[] = [] constructor(public readonly provider: MockProvider) {} @@ -117,7 +117,7 @@ describe('L1 Batch Submitter', () => { let canonicalProvider: MockProvider let canonicalTransactionChain: MockCanonicalTransactionChain let stateCommitmentProvider: MockProvider - let stateCommitmentChain: MockStatCommitmentChain + let stateCommitmentChain: MockStateCommitmentChain beforeEach(async () => { dataService = new MockDataService() @@ -126,7 +126,7 @@ describe('L1 Batch Submitter', () => { canonicalProvider ) stateCommitmentProvider = new MockProvider() - stateCommitmentChain = new MockStatCommitmentChain(stateCommitmentProvider) + stateCommitmentChain = new MockStateCommitmentChain(stateCommitmentProvider) batchSubmitter = new L1BatchSubmitter( dataService, canonicalTransactionChain as any, diff --git a/packages/rollup-core/test/app/log-handlers.spec.ts b/packages/rollup-core/test/app/log-handlers.spec.ts index 64c8564071c14..5aabd10f9e3df 100644 --- a/packages/rollup-core/test/app/log-handlers.spec.ts +++ b/packages/rollup-core/test/app/log-handlers.spec.ts @@ -1,5 +1,6 @@ /* External Imports */ import { + add0x, keccak256FromUtf8, remove0x, ZERO_ADDRESS, @@ -20,7 +21,20 @@ import { SafetyQueueBatchAppendedLogHandler, SequencerBatchAppendedLogHandler, StateBatchAppendedLogHandler, + CHAIN_ID, } from '../../src' +import { + arrayify, + joinSignature, + keccak256, + parseTransaction, + recoverAddress, + resolveProperties, + serializeTransaction, + Transaction, + UnsignedTransaction, + verifyMessage, +} from 'ethers/utils' const abi = new ethers.utils.AbiCoder() @@ -112,6 +126,30 @@ class MockDataService extends DefaultDataService { } } +const wallet = Wallet.createRandom() + +const getTxSignature = async ( + to: string, + nonce: string, + gasLimit: string, + data: string, + w: Wallet = wallet +): Promise => { + const trans = { + to: add0x(to), + nonce: add0x(nonce), + gasLimit: add0x(gasLimit), + data: add0x(data), + value: 0, + chainId: CHAIN_ID, + } + + const sig = await w.sign(trans) + const t: Transaction = parseTransaction(sig) + + return add0x(`${t.r}${remove0x(t.s)}${t.v.toString(16)}`) +} + describe('Log Handlers', () => { let dataService: MockDataService beforeEach(() => { @@ -168,14 +206,16 @@ describe('Log Handlers', () => { }) it('should parse and insert Slow Queue Tx', async () => { - const sender: string = 'aa'.repeat(20) const target: string = 'bb'.repeat(20) const nonce: string = '00'.repeat(32) const gasLimit: string = '00'.repeat(31) + '01' - const signature: string = '99'.repeat(65) const calldata: string = 'abcd'.repeat(40) - const data = `0x22222222${sender}${target}${nonce}${gasLimit}${signature}${calldata}` + const signature = await getTxSignature(target, nonce, gasLimit, calldata) + + const data = `0x22222222${target}${nonce}${gasLimit}${remove0x( + signature + )}${calldata}` const l = createLog('00'.repeat(64)) const tx = createTx(data) @@ -203,11 +243,17 @@ describe('Log Handlers', () => { 'Queue Origin mismatch' ) received.batchIndex.should.equal(0, 'Batch index mismatch') - remove0x(received.sender).should.equal(sender, 'Sender mismatch') + remove0x(received.sender).should.equal( + remove0x(wallet.address), + 'Sender mismatch' + ) remove0x(received.target).should.equal(target, 'Target mismatch') received.nonce.should.equal(0, 'Nonce mismatch') received.gasLimit.should.equal(1, 'Gas Limit mismatch') - remove0x(received.signature).should.equal(signature, 'Signature mismatch') + remove0x(received.signature).should.equal( + remove0x(signature), + 'Signature mismatch' + ) remove0x(received.calldata).should.equal(calldata, 'Calldata mismatch') dataService.txHashBatchesCreated.size.should.equal( @@ -245,14 +291,14 @@ describe('Log Handlers', () => { it('should parse and insert Sequencer Batch', async () => { const timestamp = 1 - const sender: string = 'aa'.repeat(20) const target: string = 'bb'.repeat(20) const nonce: string = '00'.repeat(32) const gasLimit: string = '00'.repeat(31) + '01' - const signature: string = '99'.repeat(65) const calldata: string = 'abcd'.repeat(40) - let data = `0x${sender}${target}${nonce}${gasLimit}${signature}${calldata}` + const signature = await getTxSignature(target, nonce, gasLimit, calldata) + + let data = `0x${target}${nonce}${gasLimit}${remove0x(signature)}${calldata}` data = abi.encode(['bytes[]', 'uint256'], [[data, data, data], timestamp]) const l = createLog('00'.repeat(64)) @@ -284,11 +330,17 @@ describe('Log Handlers', () => { 'Queue Origin mismatch' ) received.batchIndex.should.equal(i, 'Batch index mismatch') - remove0x(received.sender).should.equal(sender, 'Sender mismatch') + remove0x(received.sender).should.equal( + remove0x(wallet.address), + 'Sender mismatch' + ) remove0x(received.target).should.equal(target, 'Target mismatch') received.nonce.should.equal(0, 'Nonce mismatch') received.gasLimit.should.equal(1, 'Gas Limit mismatch') - remove0x(received.signature).should.equal(signature, 'Signature mismatch') + remove0x(received.signature).should.equal( + remove0x(signature), + 'Signature mismatch' + ) remove0x(received.calldata).should.equal(calldata, 'Calldata mismatch') } From 29ddff626a6ca81a8e4ee848c77f23864e87b968 Mon Sep 17 00:00:00 2001 From: Will Meister Date: Mon, 13 Jul 2020 13:26:35 -0500 Subject: [PATCH 11/14] moving getTxSigner to core-utils --- packages/core-utils/src/app/crypto.ts | 39 +++++++++++++++++++ .../src/app/data/producers/log-handlers.ts | 34 +--------------- 2 files changed, 40 insertions(+), 33 deletions(-) diff --git a/packages/core-utils/src/app/crypto.ts b/packages/core-utils/src/app/crypto.ts index 0b0d4f15886f3..0bc91d55cb7bb 100644 --- a/packages/core-utils/src/app/crypto.ts +++ b/packages/core-utils/src/app/crypto.ts @@ -1,6 +1,12 @@ /* External Imports */ import { Md5 } from 'ts-md5' import { ethers } from 'ethers' +import { TransactionRequest } from 'ethers/providers/abstract-provider' +import { + joinSignature, + resolveProperties, + serializeTransaction, +} from 'ethers/utils' /* Internal Imports */ import { HashAlgorithm, HashFunction } from '../types' @@ -54,3 +60,36 @@ export const hashFunctionFor = (algo: HashAlgorithm): HashFunction => { throw Error(`HashAlgorithm ${algo} not supported.`) } } + +/** + * Gets the tx signer address from the Tx Request and r, s, v. + * + * @param tx The Transaction Request. + * @param r The r parameter of the signature. + * @param s The s parameter of the signature. + * @param v The v parameter of the signature. + * @returns The signer's address. + */ +export const getTxSigner = async ( + tx: TransactionRequest, + r: string, + s: string, + v: number +): Promise => { + const txHash: string = keccak256( + serializeTransaction(await resolveProperties(tx)) + ) + + try { + return ethers.utils.recoverAddress( + ethers.utils.arrayify(txHash), + joinSignature({ + s: add0x(s), + r: add0x(r), + v, + }) + ) + } catch (e) { + return undefined + } +} diff --git a/packages/rollup-core/src/app/data/producers/log-handlers.ts b/packages/rollup-core/src/app/data/producers/log-handlers.ts index 6ad11640305bb..8462eb57eb4a3 100644 --- a/packages/rollup-core/src/app/data/producers/log-handlers.ts +++ b/packages/rollup-core/src/app/data/producers/log-handlers.ts @@ -3,6 +3,7 @@ import { add0x, BigNumber, getLogger, + getTxSigner, logError, remove0x, } from '@eth-optimism/core-utils' @@ -372,36 +373,3 @@ export const StateBatchAppendedLogHandler = async ( await ds.insertL1RollupStateRoots(l.transactionHash, stateRoots) } - -/** - * Gets the tx signer address from the Tx Request and r, s, v. - * - * @param tx The Transaction Request. - * @param r The r parameter of the signature. - * @param s The s parameter of the signature. - * @param v The v parameter of the signature. - * @returns The signer's address. - */ -const getTxSigner = async ( - tx: TransactionRequest, - r: string, - s: string, - v: number -): Promise
=> { - const txHash: string = ethers.utils.keccak256( - serializeTransaction(await resolveProperties(tx)) - ) - - try { - return ethers.utils.recoverAddress( - ethers.utils.arrayify(txHash), - joinSignature({ - s: add0x(s), - r: add0x(r), - v, - }) - ) - } catch (e) { - return undefined - } -} From 68b86ff23cc2338a5a7b48fa564d2e568993ca17 Mon Sep 17 00:00:00 2001 From: Will Meister Date: Mon, 13 Jul 2020 13:36:53 -0500 Subject: [PATCH 12/14] fixing add0x and remove0x on undefined --- packages/core-utils/src/app/misc.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/core-utils/src/app/misc.ts b/packages/core-utils/src/app/misc.ts index 35fb0ee9f73b6..d12f3ec287b96 100644 --- a/packages/core-utils/src/app/misc.ts +++ b/packages/core-utils/src/app/misc.ts @@ -80,6 +80,9 @@ export const sleep = (ms: number): Promise => { * @returns the string without "0x". */ export const remove0x = (str: string): string => { + if (!str) { + return str + } return str.startsWith('0x') ? str.slice(2) : str } @@ -89,6 +92,9 @@ export const remove0x = (str: string): string => { * @returns the string with "0x". */ export const add0x = (str: string): string => { + if (!str) { + return str + } return str.startsWith('0x') ? str : '0x' + str } From 7d7f5974edc9ae41d2c7ad710a4c2f88c5318c92 Mon Sep 17 00:00:00 2001 From: Will Meister Date: Mon, 13 Jul 2020 13:47:48 -0500 Subject: [PATCH 13/14] fixing keccak 0x issue --- packages/core-utils/src/app/crypto.ts | 2 +- packages/rollup-core/src/app/data/producers/log-handlers.ts | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/core-utils/src/app/crypto.ts b/packages/core-utils/src/app/crypto.ts index 0bc91d55cb7bb..1ee42ce1fa965 100644 --- a/packages/core-utils/src/app/crypto.ts +++ b/packages/core-utils/src/app/crypto.ts @@ -76,7 +76,7 @@ export const getTxSigner = async ( s: string, v: number ): Promise => { - const txHash: string = keccak256( + const txHash: string = ethers.utils.keccak256( serializeTransaction(await resolveProperties(tx)) ) diff --git a/packages/rollup-core/src/app/data/producers/log-handlers.ts b/packages/rollup-core/src/app/data/producers/log-handlers.ts index 8462eb57eb4a3..c6d8a7df04fcd 100644 --- a/packages/rollup-core/src/app/data/producers/log-handlers.ts +++ b/packages/rollup-core/src/app/data/producers/log-handlers.ts @@ -22,11 +22,6 @@ import { RollupTransaction, } from '../../../types' import { CHAIN_ID } from '../../constants' -import { - joinSignature, - resolveProperties, - serializeTransaction, -} from 'ethers/utils' const abi = new ethers.utils.AbiCoder() const log = getLogger('log-handler') From ea5cd804fcd62f429abda84f4e83869697896216 Mon Sep 17 00:00:00 2001 From: Will Meister Date: Mon, 13 Jul 2020 14:01:15 -0500 Subject: [PATCH 14/14] fixing regression --- packages/core-utils/src/app/misc.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core-utils/src/app/misc.ts b/packages/core-utils/src/app/misc.ts index d12f3ec287b96..fd80c275d5509 100644 --- a/packages/core-utils/src/app/misc.ts +++ b/packages/core-utils/src/app/misc.ts @@ -80,7 +80,7 @@ export const sleep = (ms: number): Promise => { * @returns the string without "0x". */ export const remove0x = (str: string): string => { - if (!str) { + if (str === undefined) { return str } return str.startsWith('0x') ? str.slice(2) : str @@ -92,7 +92,7 @@ export const remove0x = (str: string): string => { * @returns the string with "0x". */ export const add0x = (str: string): string => { - if (!str) { + if (str === undefined) { return str } return str.startsWith('0x') ? str : '0x' + str