Skip to content
Open
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