Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sync Service Tx Batch Subscription, Parsing, and L2 Submission #155

Merged
merged 9 commits into from
Jun 18, 2020
10 changes: 9 additions & 1 deletion packages/core-db/src/app/ethereum/ethereum-event-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ export class EthereumEventProcessor {
const logs: Log[] = await contract.provider.getLogs(filter)
const events: EthereumEvent[] = logs.map((l) => {
const logDesc: LogDescription = contract.interface.parseLog(l)
return EthereumEventProcessor.createEventFromLogDesc(logDesc, eventId)
return EthereumEventProcessor.createEventFromLogDesc(l, logDesc, eventId)
})

for (const event of events) {
Expand Down Expand Up @@ -222,11 +222,13 @@ export class EthereumEventProcessor {
/**
* Creates a local EthereumEvent from the provided Ethers LogDesc.
*
* @param logObj The Log in question
* @param logDesc The LogDesc in question
* @param eventID The local event ID
* @returns The local EthereumEvent
*/
private static createEventFromLogDesc(
logObj: Log,
logDesc: LogDescription,
eventID: string
): EthereumEvent {
Expand All @@ -236,6 +238,9 @@ export class EthereumEventProcessor {
name: logDesc.name,
signature: logDesc.signature,
values,
blockHash: logObj.blockHash,
blockNumber: logObj.blockNumber,
transactionHash: logObj.transactionHash,
}
}

Expand All @@ -252,6 +257,9 @@ export class EthereumEventProcessor {
name: event.event,
signature: event.eventSignature,
values,
blockHash: event.blockHash,
blockNumber: event.blockNumber,
transactionHash: event.transactionHash,
}
}

Expand Down
3 changes: 3 additions & 0 deletions packages/core-db/src/types/ethereum/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@ export interface EthereumEvent {
name: string
signature: string
values: {}
blockHash: string
blockNumber: number
transactionHash: string
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/* External Imports */
import {
add0x,
getLogger,
isValidHexAddress,
logError,
remove0x,
} from '@eth-optimism/core-utils'

import { JsonRpcProvider } from 'ethers/providers'
import { Wallet } from 'ethers'

/* Internal Imports */
import {
Address,
L1ToL2TransactionBatch,
L1ToL2TransactionBatchListener,
} from '../types'
import { CHAIN_ID, GAS_LIMIT } from './constants'

const log = getLogger('l1-to-l2-tx-batch-listener-submitter')

/**
* Handles L1 To L2 TransactionBatches, submitting them to the configured L2 node.
*/
export class L1ToL2TransactionBatchListenerSubmitter
implements L1ToL2TransactionBatchListener {
private readonly submissionToAddress: Address
private readonly submissionMethodId: string

constructor(
private readonly wallet: Wallet,
private readonly provider: JsonRpcProvider,
submissionToAddress: Address,
submissionMethodId: string
) {
if (!submissionMethodId || remove0x(submissionMethodId).length !== 8) {
throw Error(
`Invalid Transaction Batch submission method ID: ${remove0x(
submissionMethodId
)}. Expected 4 bytes (8 hex chars).`
)
}
if (!isValidHexAddress(submissionToAddress)) {
throw Error(
`Invalid Transaction Batch submission to address: ${remove0x(
submissionToAddress
)}. Expected 20 bytes (40 hex chars).`
)
}
this.submissionToAddress = add0x(submissionToAddress)
this.submissionMethodId = add0x(submissionMethodId)
}

public async handleTransactionBatch(
transactionBatch: L1ToL2TransactionBatch
): Promise<void> {
log.debug(
`Received L1 to L2 Transaction Batch ${JSON.stringify(transactionBatch)}`
)

const signedTx = await this.getSignedBatchTransaction(transactionBatch)

log.debug(
`sending signed tx for batch. Tx: ${JSON.stringify(
transactionBatch
)}. Signed: ${add0x(signedTx)}`
)
const receipt = await this.provider.sendTransaction(signedTx)
willmeister marked this conversation as resolved.
Show resolved Hide resolved

log.debug(
`L1 to L2 Transaction Batch Tx submitted. Tx hash: ${
receipt.hash
}. Tx batch: ${JSON.stringify(transactionBatch)}`
)
try {
const txReceipt = await this.provider.waitForTransaction(receipt.hash)
if (!txReceipt || !txReceipt.status) {
const msg = `Error processing L1 to L2 Transaction Batch. Tx batch: ${JSON.stringify(
transactionBatch
)}, Receipt: ${JSON.stringify(receipt)}`
log.error(msg)
throw new Error(msg)
}
} catch (e) {
logError(
log,
`Error submitting L1 to L2 Transaction Batch to L2 node. Tx Hash: ${
receipt.hash
}, Tx: ${JSON.stringify(transactionBatch)}`,
e
)
throw e
}
log.debug(`L1 to L2 Transaction applied to L2. Tx hash: ${receipt.hash}`)
}

private async getSignedBatchTransaction(
transactionBatch: L1ToL2TransactionBatch
): Promise<string> {
const tx = {
nonce: transactionBatch.nonce,
gasPrice: 0,
gasLimit: GAS_LIMIT,
to: this.submissionToAddress,
value: 0,
data: await this.getL2TransactionBatchCalldata(transactionBatch.calldata),
chainId: CHAIN_ID,
}

return this.wallet.sign(tx)
}

private async getL2TransactionBatchCalldata(
l1Calldata: string
): Promise<string> {
const l1CalldataParams =
!l1Calldata || remove0x(l1Calldata).length < 8
? 0
: remove0x(l1Calldata).substr(8)
return `${this.submissionMethodId}${l1CalldataParams}`
}
}
160 changes: 160 additions & 0 deletions packages/rollup-core/src/app/l1-to-l2-transaction-batch-processor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/* External Imports */
import {
BaseQueuedPersistedProcessor,
DB,
EthereumEvent,
EthereumListener,
} from '@eth-optimism/core-db'
import { getLogger, logError, Logger } from '@eth-optimism/core-utils'

/* Internal Imports */
import {
L1ToL2Transaction,
L1ToL2TransactionBatch,
L1ToL2TransactionBatchListener,
} from '../types'
import { Provider, TransactionResponse } from 'ethers/providers'

const log: Logger = getLogger('l1-to-l2-transition-batch-processor')

export class L1ToL2TransactionBatchProcessor
extends BaseQueuedPersistedProcessor<L1ToL2TransactionBatch>
implements EthereumListener<EthereumEvent> {
public static readonly persistenceKey = 'L1ToL2TransitionBatchProcessor'

private readonly provider: Provider

public static async create(
db: DB,
l1ToL2EventId: string,
listeners: L1ToL2TransactionBatchListener[],
persistenceKey: string = L1ToL2TransactionBatchProcessor.persistenceKey
): Promise<L1ToL2TransactionBatchProcessor> {
const processor = new L1ToL2TransactionBatchProcessor(
db,
l1ToL2EventId,
listeners,
persistenceKey
)
await processor.init()
return processor
}

private constructor(
db: DB,
private readonly l1ToL2EventId: string,
private readonly listeners: L1ToL2TransactionBatchListener[],
persistenceKey: string = L1ToL2TransactionBatchProcessor.persistenceKey
) {
super(db, persistenceKey)
}

/**
* @inheritDoc
*/
public async handle(event: EthereumEvent): Promise<void> {
if (event.eventID !== this.l1ToL2EventId || !event.values) {
log.debug(
`Received event of wrong ID or with incorrect values. Ignoring event: [${JSON.stringify(
event
)}]`
)
return
}

const calldata = await this.fetchCalldata(event.transactionHash)

let transactions: L1ToL2Transaction[] = []
try {
transactions = await this.parseTransactions(calldata)
} catch (e) {
// TODO: What do we do here?
logError(
log,
`Error parsing calldata for event ${JSON.stringify(
event
)}. Assuming this tx batch was malicious / invalid. Moving on.`,
e
)
}

const transactionBatch: L1ToL2TransactionBatch = {
nonce: event.values['_nonce'].toNumber(),
timestamp: event.values['_timestamp'].toNumber(),
transactions,
calldata,
}

this.add(transactionBatch.nonce, transactionBatch)
}

/**
* @inheritDoc
*/
public async onSyncCompleted(syncIdentifier?: string): Promise<void> {
// no-op
}

/**
* @inheritDoc
*/
protected async handleNextItem(
index: number,
item: L1ToL2TransactionBatch
): Promise<void> {
try {
await Promise.all(
this.listeners.map((x) => x.handleTransactionBatch(item))
)
await this.markProcessed(index)
} catch (e) {
this.logError(
`Error processing L1ToL2Transaction in at least one handler. Tx: ${JSON.stringify(
item
)}`,
e
)
// All errors should be caught in the listeners, so this is fatal.
process.exit(1)
}
}

/**
* @inheritDoc
*/
protected async serializeItem(item: L1ToL2TransactionBatch): Promise<Buffer> {
return Buffer.from(JSON.stringify(item), 'utf-8')
}

/**
* @inheritDoc
*/
protected async deserializeItem(
itemBuffer: Buffer
): Promise<L1ToL2TransactionBatch> {
return JSON.parse(itemBuffer.toString('utf-8'))
}

private async fetchCalldata(txHash: string): Promise<string> {
let tx: TransactionResponse
try {
tx = await this.provider.getTransaction(txHash)
} catch (e) {
logError(
log,
`Error fetching tx hash ${txHash}. This should not ever happen.`,
e
)
process.exit(1)
}

return tx.data
}

// TODO: This when a format is solidified
private async parseTransactions(
calldata: string
): Promise<L1ToL2Transaction[]> {
return []
}
}
2 changes: 1 addition & 1 deletion packages/rollup-core/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export * from './errors'
export * from './l1-to-l2-transaction-listener'
export * from './l1-to-l2-listener'
export * from './node-context'
export * from './opcodes'
export * from './state-machine'
Expand Down
20 changes: 20 additions & 0 deletions packages/rollup-core/src/types/l1-to-l2-listener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {
L1ToL2StateCommitmentBatch,
L1ToL2Transaction,
L1ToL2TransactionBatch,
} from './types'

/**
* Defines the event handler interface for L1-to-L2 Transactions.
*/
export interface L1ToL2TransactionListener {
handleL1ToL2Transaction(transaction: L1ToL2Transaction): Promise<void>
}

export interface L1ToL2TransactionBatchListener {
handleTransactionBatch(batch: L1ToL2TransactionBatch): Promise<void>
}

export interface L1ToL2StateCommitmentBatchHandler {
handleStateCommitmentBatch(batch: L1ToL2StateCommitmentBatch): Promise<void>
}

This file was deleted.

18 changes: 18 additions & 0 deletions packages/rollup-core/src/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,24 @@ export interface L1ToL2Transaction {
callData: string
}

// TODO: Update when the format is known
export type StateCommitment = string
export interface RollupTransition {
nonce: number
transaction: L1ToL2Transaction
stateCommitment: StateCommitment
}
export interface L1ToL2TransactionBatch {
nonce: number
timestamp: number
transactions: L1ToL2Transaction[]
calldata: string
}
export interface L1ToL2StateCommitmentBatch {
nonce: number
stateCommitments: StateCommitment[]
}

/* Types */
export type Address = string
export type StorageSlot = string
Expand Down
Loading