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

Add OP Stack support to tx batch decompress script #1

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
31 changes: 31 additions & 0 deletions packages/optimism-decoder/src/analyze.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,48 @@ const ctcMapping: Record<string, string | undefined> = {
'0x56a76bcC92361f6DF8D75476feD8843EdC70e1C9': 'Metis',
'0x6A1DB7d799FBA381F2a518cA859ED30cB8E1d41a': 'Metis 2.0',
'0xfBd2541e316948B259264c02f370eD088E04c3Db': 'Boba Network',
'0x5f7f7f6DB967F0ef10BdA0678964DBA185d16c50': 'Lyra',
'0xFf00000000000000000000000000000000008453': 'Base',
'0x6F54Ca6F6EdE96662024Ffd61BFd18f3f4e34DFf': 'Zora',
'0xC1B90E1e459aBBDcEc4DCF90dA45ba077d83BFc5': 'PGN',
'0xFF00000000000000000000000000000000000010': 'OPMainnet',
'0x253887577420Cb7e7418cD4d50147743c8041b28': 'Aevo',
'0x1c479675ad559DC151F6Ec7ed3FbF8ceE79582B6': 'Arbitrum',
}

const typeMapping: Record<string, string | undefined> = {
Arbitrum: 'Arbitrum',
Lyra: 'OpStack',
Base: 'OpStack',
Zora: 'OpStack',
PGN: 'OpStack',
OPMainnet: 'OpStack',
Aevo: 'OpStack',
'Boba Network': 'OVM 2.0',
'Optimism OVM 1.0': 'OVM 1.0',
}

export async function analyzeTransaction(
provider: providers.Provider,
txHash: string,
) {
const tx = await provider.getTransaction(txHash)
const block = await provider.getBlock(tx.blockNumber)
const project = ctcMapping[tx.to ?? ''] ?? 'Unknown'
const kind = typeMapping[project ?? ''] ?? 'Unknown'
console.log(
'Tx submits data to',
tx.to,
'hence it is',
project,
'of kind',
kind,
)

return {
data: tx.data,
timestamp: block.timestamp,
project,
kind,
}
}
312 changes: 302 additions & 10 deletions packages/optimism-decoder/src/decode.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import assert from 'assert'
import { BufferReader } from 'bufio'
import { ethers } from 'ethers'
import zlib from 'zlib'

import { FourBytesApi } from './FourBytesApi'
import { add0x, trimLong } from './utils'
import { decode } from 'punycode'
import { parse } from 'path'
import { mnemonicToEntropy } from 'ethers/lib/utils'
import { exit } from 'process'

interface BatchContext {
sequencerTxCount: number
Expand All @@ -18,13 +27,296 @@ interface AppendSequencerBatchParams {
contexts: BatchContext[] // total_elements[fixed_size[]]
transactions: string[] // total_size_bytes[], total_size_bytes[]
}
/*

//4d73adb72bc3dd368966edd0f0b2148401a178e2

86 038465ab8606
86 04840122a208
b9 0b77 0003000000000000027404f9027083597a5d8407270e00835ca96d94a0cc33dd6f4819d473226257792afe230ec3c67f80b902046c459a28
000000000000000000000000
4d73adb72bc3dd368966edd0f0b2148401a178e2
00000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000065abda65
*/

/* L1MessageType_L2Message = 3
L1MessageType_EndOfBlock = 6
L1MessageType_L2FundedByL1 = 7
L1MessageType_RollupEvent = 8
L1MessageType_SubmitRetryable = 9
L1MessageType_BatchForGasEstimation = 10 // probably won't use this in practice
L1MessageType_Initialize = 11
L1MessageType_EthDeposit = 12
L1MessageType_BatchPostingReport = 13
L1MessageType_Invalid = 0xFF
/*

/*
ARBITRUM SEGMENT TYPES:

const BatchSegmentKindL2Message uint8 = 0
const BatchSegmentKindL2MessageBrotli uint8 = 1
const BatchSegmentKindDelayedMessages uint8 = 2
const BatchSegmentKindAdvanceTimestamp uint8 = 3
const BatchSegmentKindAdvanceL1BlockNumber uint8 = 4
*/

/* L2MessageKind_UnsignedUserTx = 0
L2MessageKind_ContractTx = 1
L2MessageKind_NonmutatingCall = 2
L2MessageKind_Batch = 3
L2MessageKind_SignedTx = 4
// 5 is reserved
L2MessageKind_Heartbeat = 6 // deprecated
L2MessageKind_SignedCompressedTx = 7
// 8 is reserved for BLS signed batch
) */

export function decodeArbitrumL2Message(
tx: string,
fourBytesApi: FourBytesApi,
) {
const type = tx.slice(0, 2)
//console.log(' Type:', type)
const rawTx = add0x(tx.slice(2))
const parsed = ethers.utils.parseTransaction(rawTx)
const methodHash = parsed.data.slice(0, 10)
// const methodSignature = await fourBytesApi.getMethodSignature(methodHash)
const methodSignature = '???'
//console.log(
// ' ',
// trimLong(tx),
// methodHash,
// methodSignature,
// parsed.from,
// parsed.to,
// )
//console.log(parsed.from, parsed.to)
}

export function decodeArbitrumL2MessageBatch(
l2Message: string,
fourBytesApi: FourBytesApi,
) {
//console.log('decoding L2Message:')
//console.log(l2Message)
//console.log()
let totalRead = 0
for (let i = 0; ; i++) {
const length = parseInt(l2Message.slice(totalRead, totalRead + 16), 16) * 2
//console.log(' TxChunkLength:', i, +length)
const tx = l2Message.slice(totalRead + 16, totalRead + 16 + length)
//console.log(tx, tx.length)
//decodeArbitrumL2Message(tx, fourBytesApi)
totalRead += length + 16
//console.log('TotalRead: ', totalRead)
if (totalRead >= l2Message.length) break
}
}

export function decodeArbitrumSegment(
segment: string,
fourBytesApi: FourBytesApi,
): string {
const segmentContentType = segment.slice(0, 2)
let timestamp = '0x00'
//console.log('SegmentContentType: ', segmentContentType)
switch (segmentContentType) {
case '00': // Batch of signed transactions
if (segment.slice(2, 4) === '03') {
decodeArbitrumL2MessageBatch(segment.slice(4), fourBytesApi)
} else {
const tx = segment.slice(4)
decodeArbitrumL2Message(add0x(tx), fourBytesApi)
}
break
case '03': // AdvanceTimestamp + 4 bytes
timestamp = ethers.utils.RLP.decode(add0x(segment.slice(2)))
//console.log(' AdvanceTimestamp:', timestamp, parseInt(timestamp, 16))
break
case '04': // AdvanceL1BlockNumber + 4 bytes
const l1block = ethers.utils.RLP.decode(add0x(segment.slice(2)))
//console.log(' AdvanceL1BlockNumber:', l1block, parseInt(l1block, 16))
break
default:
console.log(
'Unknown segment type',
segmentContentType,
segment.slice(4),
parseInt(segment.slice(4), 16),
)
}
return timestamp
}

export function decodeArbitrumBatch(
kind: string,
data: string,
submissionTimestamp: number,
fourBytesApi: FourBytesApi,
) {
let minTimestamp, maxTimestamp
let firstTimestamp = true

console.log('Decoding Arbitrum...')
const abi = [
'function addSequencerL2BatchFromOrigin(uint256 sequenceNumber,bytes data,uint256 afterDelayedMessagesRead,address gasRefunder,uint256 prevMessageCount,uint256 newMessageCount)',
]
const iface = new ethers.utils.Interface(abi)
const decodedArgs = iface.decodeFunctionData(data.slice(0, 10), data)
console.log(decodedArgs.data.slice(2, 4)) // removing 0x, next byte is type of compressed data
let brotliCompressedData = Buffer.from(decodedArgs.data.slice(4), 'hex')
try {
let decompressedData = zlib.brotliDecompressSync(brotliCompressedData, {
//TODO: No idea what are the correct params.
params: {
[zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_GENERIC,
[zlib.constants.BROTLI_PARAM_QUALITY]:
zlib.constants.BROTLI_MAX_QUALITY,
},
})
//console.log('Decompressed data:', decompressedData)
let reader = new BufferReader(decompressedData)

const decompressedBytes = reader.readBytes(reader.left())
const totalLength = decompressedBytes.toString('hex').length / 2 // we do /2 because we are counting bytes
const lengthBytes = ethers.utils.hexlify(totalLength).slice(2)
const lengthBytesLength = lengthBytes.length / 2
const lengthByte = 0xf7 + lengthBytesLength
const lengthByteHex = ethers.utils.hexlify(lengthByte)
const concatenatedWithLength =
lengthByteHex +
lengthBytes +
(decompressedBytes.toString('hex') as string)
const decoded = ethers.utils.RLP.decode(concatenatedWithLength)
console.log('Decoded:', decoded.length)
for (const [index, value] of decoded.entries()) {
const timestamp = decodeArbitrumSegment(value.slice(2), fourBytesApi)
if (firstTimestamp) {
minTimestamp = parseInt(timestamp, 16)
maxTimestamp = parseInt(timestamp, 16)
firstTimestamp = false
} else {
maxTimestamp += parseInt(timestamp, 16)
}
}
console.log('Submission timestamp:', submissionTimestamp)
console.log('Min L2 timestamp in submission:', minTimestamp)
console.log('Max L2 timestamp in submission:', maxTimestamp)
const minT = submissionTimestamp - minTimestamp
const maxT = submissionTimestamp - maxTimestamp
console.log(
'Finality delay between',
minT,
'and',
maxT,
'seconds (',
parseFloat((minT / 60).toFixed(2)),
'and',
parseFloat((maxT / 60).toFixed(2)),
'minutes)',
)
} catch (err) {
console.error('An error occurred:', err)
}
}

export async function decodeOpStackSequencerBatch(
kind: string,
data: string,
submissionTimestamp: number,
fourBytesApi: FourBytesApi,
) {
console.log('Decoding', kind, 'L1 Sequencer transaction batch ...')
let reader = new BufferReader(Buffer.from(data.slice(2), 'hex'))

const version = reader.readBytes(1).toString('hex')
console.log('Version:', version)
const channelId = reader.readBytes(16).toString('hex')
console.log('ChannelId:', channelId)
const frame_number = reader.readU16BE()
console.log('Frame Number:', frame_number)
if (frame_number !== 0) {
console.log(
"This is not a first frame, I won't be able to decompress this, exiting...",
)
return
}
const frame_data_length = reader.readU32BE()
console.log('Frame Data Length:', frame_data_length)
// console.log(reader.left())
const bytes = reader.readBytes(frame_data_length)
const is_last = reader.readBytes(1).toString('hex')
assert(is_last === '01' || is_last === '00')
console.log('Is Last:', is_last === '01')
if (is_last === '00') {
console.log(
"This is not a last frame, I won't be able to decompress this, exiting...",
)
return
}

const inflated = zlib.inflateSync(bytes)

// ----- reading decompressed data ----- This is RLP list w/out the header, so we need to add header

reader = new BufferReader(inflated)
const decompressedBytes = reader.readBytes(reader.left())
const totalLength = decompressedBytes.toString('hex').length / 2 // we do /2 because we are counting bytes
const lengthBytes = ethers.utils.hexlify(totalLength).slice(2)
const lengthBytesLength = lengthBytes.length / 2
const lengthByte = 0xf7 + lengthBytesLength
const lengthByteHex = ethers.utils.hexlify(lengthByte)
const concatenatedWithLength =
lengthByteHex + lengthBytes + (decompressedBytes.toString('hex') as string)
const decoded = ethers.utils.RLP.decode(concatenatedWithLength)

let numEmptyBatches = 0
console.log('Decoding', decoded.length, 'batches')

const timestamps = []
for (const [index, batch] of decoded.entries()) {
// batch: batch_version ++ rlp (parent_hash, epoch_number, epoch_hash, timestamp, transaction_list)
const decodedBatch = ethers.utils.RLP.decode(add0x(batch.slice(4)))
const numTxs = decodedBatch[decodedBatch.length - 1].length
if (numTxs !== 0) {
// transaction list is not empty
console.log()
console.log('Batch #', index, 'with', numTxs, 'transactions')
console.log('ParentHash', decodedBatch[0])
console.log('EpochNumber', parseInt(decodedBatch[1], 16))
console.log('EpochHash', decodedBatch[2])
const timestamp = parseInt(decodedBatch[3], 16)
console.log('Timestamp', timestamp)
timestamps.push(timestamp)

for (const tx of decodedBatch[decodedBatch.length - 1]) {
//console.log('tx:', tx)
const parsed = ethers.utils.parseTransaction(tx)
const methodHash = parsed.data.slice(0, 10)
const methodSignature = await fourBytesApi.getMethodSignature(
methodHash,
)
console.log(' ', trimLong(tx), methodHash, methodSignature)
}
} else numEmptyBatches++
}
console.log('Num of empty batches', numEmptyBatches)
console.log(
'Finality delay between',
submissionTimestamp - Math.min(...timestamps),
'and',
submissionTimestamp - Math.max(...timestamps),
'seconds',
)
}

export async function decodeSequencerBatch(
kind: string,
data: string,
fourBytesApi: FourBytesApi,
): Promise<AppendSequencerBatchParams> {
console.log('Decoding', kind, 'L1 Sequencer transaction batch...')
): Promise<AppendSequencerBatchParams | undefined> {
console.log('Decoding', kind, 'L1 Sequencer transaction batch ...')
let reader = new BufferReader(Buffer.from(data.slice(2), 'hex'))

const methodName = reader.readBytes(4).toString('hex')
Expand Down Expand Up @@ -79,15 +371,15 @@ export async function decodeSequencerBatch(
transactions.push(add0x(raw))
console.log(' ', trimLong(add0x(raw)), methodHash, methodSignature)
}
}

console.log('Decoded', transactions.length, 'transactions')
console.log('Done decoding...')
console.log('Decoded', transactions.length, 'transactions')
console.log('Done decoding...')

return {
shouldStartAtElement,
totalElementsToAppend,
contexts,
transactions,
return {
shouldStartAtElement,
totalElementsToAppend,
contexts,
transactions,
}
}
}
Loading
Loading