如何解析 Arbitrum 傳到 L1 的資料

Paul (Tzu Chun) Yu
Taipei Ethereum Meetup
13 min readJan 9, 2024

大家有好奇過 Arbitrum 收了交易 (tx) 後到底上傳了什麼資料到 L1 嗎?本文介紹如何使用工具解析 Arbitrum 傳到 L1 的資料。

tx & batch

首先我們來看 arbiscan 裏的這筆交易:0xe23f413e5f6e3e2e45a8297b4bf67de721981932ae5797b710f60ea18f2cbfa1

這個交易有一坨 input ,用 utf-8 解析之後會看到以下文字

這段文字是近期很紅的銘文其中一種,銘文在 Ethereum 上的做法是在 Calldata 存放獨特的參考標識符,就像上圖有個 mint、有 amt。這段文字會被存到 L1 嗎?要解答這問題要知道 Arbitrum 交易是怎麼上傳到 L1 的。

交易發給排序器(Arbitrum sequencer)後,上面發出銘文的這筆交易的該區塊,會與其他數個區塊,變成一個批次(batch)上傳到 L1,像上面這筆交易他屬於的批次是 446848,我們點入 arbiscan 的 Advanced TxInfo 這頁籤可以看到以下內容

點選 Submission Tx Hash 能在 etherscan找到對應的以下這筆 L1 交易

0x47dafdf9862f3564df15aecaf1c83bdac5da44097504fe362fd685896bc533a9

0x47dafdf9862f3564df15aecaf1c83bdac5da44097504fe362fd685896bc533a9 on Ethereum

這筆交易 #0 對應批次號碼,我們關注的重點是 #1 data,其餘無關的我們在此忽略。如果 #1 往右滑可以看到非常非常長的一筆 data,而且多到很難滑到底。這是被經過壓縮後的批次交易資料 (tx) ,交易包含的資料,如 hash, calldata, etc 都會被一起壓縮。

Brotli 壓縮

可以看到官方文件裡面寫使用一種叫做 brotli 的壓縮方式壓縮

The Sequencer’s batches are compressed using a general-purpose data compression algorithm called “brotli”, on its highest-compression setting.

brotli 是 Google 開發的一個無損壓縮技術,無損是可以完整還原回來,並且不會丟失資訊的意思。我們可以利用一些線上工具試著壓縮看看

將 brotli value 還原回原文

壓縮比例是這類技術一個重要的指標之一,以 brotli 而言多少要看你壓縮什麼樣的資料,這最常被用來壓縮網路傳輸的資料。Arbitrum 論壇上有篇文章比較深入討論壓縮比率並做了模擬,有興趣的請見此

透過 arbitrum-cli-tools 還原

這時就可以問幾個問題

1. 是全部資料都壓縮呢?還是部分壓縮?或甚至分不同段壓縮?
只要不是非全部一次壓縮,我們需要知道究竟哪個段落是切分點

2. 壓縮的過程大概是怎樣?

3. 是否有特殊的標頭檔或是 metadata?
通常這樣的標頭檔或是 metadata 在解壓縮的時候要去掉

要了解這些問題我們要試著還原資料,但是如果直接丟到線上的工具會發現還原不出來,即使把 hex 換成上面工具吃的 base64 也會還原不回來。Arbitrum 有提供一個工具包 arbitrum-cli-tools 剛好能讓我們解析這樣的資料。我們需要工具包裡面的 l1-batch-handler 工具。

注意因為 arbitrum-cli-tools 有少部分功能還沒實作完 (他們的 TODO 還有: Add `startBlock` and delayed transaction handler, 2024/1/4 updated),在我本地端是沒有辦法直接跑的,我有註解掉一些段落,這部分我放在文末 “使用 arbitrum-cli-tools 該注意的細節”

# readme 提供的
$ batch-tx-handler git:(main) ✗ yarn L1BatchHandler --l1TxHash {SEQUENCER_SUBMISSION_TX} --outputFile {FILE_TO_RECORD_TXNS} --l2NetworkId {L2_NETWORK_ID}
# 我跑的
$ batch-tx-handler git:(main) ✗ yarn L1BatchHandler --l1TxHash 0x47dafdf9862f3564df15aecaf1c83bdac5da44097504fe362fd685896bc533a9 --outputFile batchdecode --l2NetworkId 42161

此時如果跑這個指令,應該會得到一整個檔案都是 txhash 但是沒有其他資訊,我們需要在 util 裡面的 decodeL2Msg 函數 裏面多加你想看的資料,舉例我多印了 calldata 的部分

// /arbitrum-cli-tools/packages/batch-tx-handler/src/l1-batch-handler/utils.ts
export const decodeL2Msgs = (l2Msgs: Uint8Array): string[] => {
const txHash: string[] = [];

const kind = l2Msgs[0];
console.log('kind', kind);
if (kind === L2MessageKind_SignedTx) {
const serializedTransaction = l2Msgs.subarray(1); // remove kind tag
const tx = ethers.utils.parseTransaction(serializedTransaction);
const currentHash = tx.hash!; // calculate tx hash
txHash.push(currentHash);
console.log('txhash', tx.hash); // **** 在這行加入 txhash
console.log('calldata', tx.data); // **** 在這行加入 calldata

此時就可以看到我們的資料被還原出來了

這個 calldata 一樣用 utf-8 解析就會得到前面我們看到的這串字(這是現在很紅的銘文其中之一)

data:,{"p":"fair-20","op":"mint","tick":"fair","amt":"1000"}

解壓縮的細節

L1BatchHandler 總共透過三步解壓縮資料:首先透過 getRawData 用 txhash 抓到 rawData,經過 processRawData 一番處理會變成 compressedData,接著經過 decompressAndDecode 變成 l2segment。如以下程式碼所示:

// /arbitrum-cli-tools/packages/batch-tx-handler/src/l1-batch-handler/exec.ts
export const startL1BatchHandler = async (
sequencerTx: string,
provider: providers.JsonRpcProvider,
) => {
// ..... 省略中間的程式碼
const [rawData, deleyedCount] = await getRawData(sequencerTx);
const compressedData = processRawData(rawData);
const l2segments = decompressAndDecode(compressedData);

以下更詳細的介紹這三步:

1. getRawData
將 txhash 傳給 L1provider 抓到那筆交易資料,並且轉成 Uint8Array。

// /arbitrum-cli-tools/packages/batch-tx-handler/src/l1-batch-handler/exec.ts
export const getRawData = async (sequencerTx: string): Promise<[Uint8Array, BigNumber]> => {
const contractInterface = new Interface(seqFunctionAbi);
const l2Network = await getL2Network(l2NetworkId);
const txReceipt = await l1Provider.getTransactionReceipt(sequencerTx);
const tx = await l1Provider.getTransaction(sequencerTx);

// ... 省略中間的 error handling

const funcData = contractInterface.decodeFunctionData('addSequencerL2BatchFromOrigin', tx.data);
const seqData = funcData['data'].substring(2); //remove '0x'
const deleyedCount = funcData['afterDelayedMessagesRead'] as BigNumber;
let rawData = Uint8Array.from(Buffer.from(seqData, 'hex'));

raw data 是 Uint8Array,也就是長得像下面這樣:

Uint8Array(97511) [
0, 91, 75, 217, 35, 138, 138, 86, 15, 35, 42, 106,
61, 4,

2. processRawData
會利用標頭確認是不是 Arbitrum Nitro Rollups 類型的資料(註:Arbitrum Nitro 是 Arbitrum One 的進化版,跟 Nova 是使用 Anytrust DAC 當作 DA 的)。

export const processRawData = (rawData: Uint8Array): Uint8Array => {
// This is to make sure this message is Nitro Rollups type. (For example: Anytrust use 0x80 here)
if (rawData[0] !== BrotliMessageHeaderByte) {
throw Error('Can only process brotli compressed data.');
}
//remove type tag of this message
const compressedData = rawData.subarray(1);

if (compressedData.length === 0) {
throw new Error('Empty sequencer message');
}
return compressedData;
};

3. decompressAndDecode
會把 Uint8Array 丟到 Brotli 解壓縮然後變成 hex,再利用 RLP (Recursive Length Prefix, 以太坊用來序列化資料的編碼方式) decode,這裡可以看到 decode 是會一段一段 decode 直到沒有 remainder。

// Use brotli to decompress the compressed data and use rlp to decode to l2 message segments
export const decompressAndDecode = (compressedData: Uint8Array): Uint8Array[] => {
//decompress data
const decompressedData = brotli.decompress(Buffer.from(compressedData));
const hexData = ethers.utils.hexlify(decompressedData);

//use rlp to decode stream type
let res = rlp.decode(hexData, true) as Decoded;
const l2Segments: Uint8Array[] = [];
while (res.remainder !== undefined) {
l2Segments.push(bufArrToArr(res.data as Buffer));
res = rlp.decode(res.remainder as Input, true) as Decoded;
}
return l2Segments;
};

使用 arbitrum-cli-tools 該注意的細節

  1. 這段 kind 的判斷要註解掉才能跑
    我判斷原因是這個跟還沒做完的 delayed transaction handler 有關,但我沒有更深入研究
if (kind === BatchSegmentKindDelayedMessages) {
//MessageDelivered
l2Msgs.push(await getDelayedTx(currentDelayedMessageIndex));
currentDelayedMessageIndex -= 1;
}

2. l2NetworkId 是 42161,這是 Arbitrum Nitro 的網路代碼,你可以在 chainlist 上找到,42170 是 Arbitrum Nova

3. 如果執行有遇到 ERR_UNKNOWN_FILE_EXTENSION 的問題可以試著把 tsconfig 這段刪掉

"ts-node": {
"esm": true,
"compilerOptions": {
"module": "nodenext"
}
}

資料來源

Special thanks to NIC Lin, and Cyan Ho for reviewing and improving this post

TEM Medium 2023 有獎徵稿 TEM Medium 目前正在進行有獎徵稿!詳情請參考: https://medium.com/taipei-ethereum-meetup/tem-medium-call-for-papers-with-prize-2023-q1-f384828f902f

--

--

Paul (Tzu Chun) Yu
Taipei Ethereum Meetup

NTU CSIE, ECON; learning crypto and zero knowledge proof; I envision a hyperstructure; https://github.com/NOOMA-42