iT邦幫忙

2026 iThome 鐵人賽

DAY 10
0
Software Development

Web3 支付工程筆記:企業級穩定幣結算系統與多鏈架構實戰系列 第 10

Day 10 | 交易序列化的四鏈對照:Nonce、Seqno、Blockhash、Object Version

  • 分享至 

  • xImage
  •  

本日程式碼:repo tag day-10

昨天 pool 建起來之後,現在 N 個 worker 各領各的 job,誰先誰後都無所謂;但那 N 筆交易要上鏈的時候都是從同一個錢包出去的,而每條鏈都需要用某種 unique 的數字,把同一個帳戶送出的交易排序或獨立開來:沒錯,相信大家老熟了,在 EVM 上那個數字叫 nonce,兩筆以上的交易帶著同一個 nonce 的話只有一筆會留在鏈上。今天要處理的是這些交易順序由誰來負責安排,還有有些交易得到號碼了、卻沒發出去會怎樣。

今天的目標

我們今天要做一個發號的元件,讓 worker 在開始動作之前先拿到「這筆交易拿到了哪個號碼」,再把「拿了號碼卻沒送出去」定義清楚。

四條鏈排隊的方式差很多,所以我們要先把各自的官方文件讀一遍(為了方便,後續我們都用 nonce 統稱那個號碼):

  1. EVM 的 nonce 是一個依序遞增的計數器,鏈上只回報這個帳戶送出過幾筆(eth_getTransactionCount
  2. Solana 沒有帳戶層級的計數器,交易只會帶一個 recent blockhash,而且只在最近 151 個 block 內有效,驗證節點在這個窗口內記住處理過的簽名來去重
  3. TON 的 seqno 存在錢包合約裡,合約只收剛好等於當前值的那一個,收下就加一
  4. SUI 排的是 object 的版本,同一組 (ObjectId, SequenceNumber) 被兩筆還沒 finalize 的交易同時用到就是 equivocation,那個 object 會被鎖到這個 epoch 結束

repo 中 internal/relayerExample_nonceGap 用一個發送錢包送五筆付款,中間安排一筆確定沒出門、一筆不知道有沒有出門。前半段輸出長這樣:

sync    0x90F7…b906  next 7    in-flight -    gap -
worker  nonce 7    pi_0001/settle   #1  sent         tx 0x0001
worker  nonce 8    pi_0002/settle   #1  retry        (send: relayer: transaction was not sent: signing failed)
worker  nonce 8    pi_0003/settle   #1  sent         tx 0x0003
worker  nonce 9    pi_0004/settle   #1  retry        (send: rpc: timeout)
worker  nonce -    pi_0005/settle   #1  retry        (no slot: txseq: account has an unfilled gap at 9)
count   0x90F7…b906  next 10   in-flight -    gap 9
  • 跟鏈同步時,第一件事是問鏈上這個錢包的 nonce 用到哪了(上面範例是 7)
  • pi_0002 簽名失敗,可以透過 Sender 確定沒有送出這筆交易,所以 8 退回去,然後 pi_0003 拿到同一個 nonce 8
  • pi_0004 送出時 RPC 逾時,沒有人知道那筆交易有沒有進節點,所以 9 只好當成用掉了(但其實還沒)
  • pi_0005 因為實際上 nonce 有 gap(可能根本沒送出),所以連 nonce 都拿不到,原封不動返回 queue(intent 還在 authorized、帳上沒有 hold)

四條鏈排隊的方式

作為所謂 nonce 的數字 誰算出來的 填錯的下場 同帳戶能不能同時送
EVM nonce,每個帳戶一個遞增整數 發送方自己算 太小直接被拒;太大排進節點的 queued 區,等前面的空位被填掉 可以,但跳號的都要等待
Solana recent blockhash,不是序號 從鏈上讀最近的 block 過期就失效,不會卡到別人 可以,天生不用排
TON seqno,錢包合約裡的計數器 從合約讀 不等於當前值就不收,不會卡住後面 不行,一個錢包一次一筆
SUI owned object 的版本 從鏈上讀那個 object 同一版本被兩筆同時用是 equivocation,object 鎖到 epoch 結束 看你把 gas coin 切成幾個 object

總歸來說,我們只需要考慮 nonce 是誰算出來的:

  1. EVM 的 nonce 與 TON 的 seqno 是發送方自己往上加的,所以要有人集中發號
  2. Solana 的 blockhash 與 SUI 的 object version 是送出當下從鏈上讀的,讀到什麼填什麼,同一個帳戶要同時送幾筆都可以

故我們介面上應分成兩種 Sender

  1. 實作 OrderedSender 的鏈,worker 會先幫它取號再呼叫 SendAt
  2. 沒實作的走原本的 Send,relayer 完全不介入

為什麼不讓 Sender 自己每次去鏈上查詢

最省事的做法是每次送之前問一次 eth_getTransactionCount(addr, "pending"),鏈上回什麼就填什麼。單執行緒下這樣沒問題,但我們的 worker 有 N 個。

在同一個時間點,兩個 worker 在鏈上得到的會是同一個數字,所以當兩筆不同的交易帶著同一個 nonce 來到同個節點的話,節點最多只留下其中一筆,而被擠掉的那一筆不會回任何錯誤、它會直接消失不見;但這時我們鏈下的 intent 已經是 confirming、而且帶著一個永遠不會上鏈的 tx hash。故 nonce 只能有一個發放者,而那個發放者只能是 relayer,因為只有它知道現在有幾個 worker 在操作同一個錢包。

此外,計數器的即時數值在鏈上不在鏈下,所以一旦後端重啟、或有別人用這錢包在其他地方發送交易,我們記的數字就會是錯誤的。因此,Counter 要實作一個 Sync 來拿鏈上即時的值蓋掉鏈下記錄的值。

三種發送結果與跳號問題

nonce 真正被拿走之前都好說,但被某筆交易拿到之後就不一樣了:名額有 defer 就還得掉,但 nonce 能不能還就要看那筆交易到底有沒有出門。

const (
	// SentYes:節點收下了。nonce 用掉,計數器往前。
	SentYes Sent = "sent"
	// SentNo:確定沒發送出去(簽名失敗、參數組不出來、連線根本沒建立)。nonce 退回去給下一筆用。
	SentNo Sent = "not-sent"
	// SentUnknown:不知道。nonce 當成用掉了,序列上留一個洞,這個帳戶暫停發號等人來對帳。
	// 退回去重用會撞到那筆可能正躺在 mempool 裡的交易。
	SentUnknown Sent = "unknown"
)

發號器與兩個實作都在 internal/txseq,完整程式碼在 repo 的 day-10 tag。三種發送結果各自對計數器做一件事:

預設是「不知道」,要 Sender 主動包一個 ErrNotSent 才算「確定沒出門」。預設偏保守是因為兩邊猜錯的代價不對稱:把沒出門的當成不知道,最壞是留一個洞、這個帳戶停下來等人來看;但把出門了的當成沒出門,下一筆會帶著同一個號,去撞自己那筆還躺在 mempool 的交易(保證其中一筆失敗)。

此外,跳號問題每條鏈的成本也不同:TON 的錢包合約不收就算了,跳一個 seqno 沒有什麼成本;但 EVM 跳號會把這個帳戶後面所有交易都掛在 queued 區,一筆都進不了區塊。所以 Counter 一遇到洞就把整個帳戶停下來,後面的 job 在取號那一關就被擋掉,原封不動回 queue。這是刻意的:擋在副作用之前,那批 intent 就停在 authorized,不會一路變成 settling、加了 hold,最後全部送審。

於是一筆下落不明的交易會在鏈下卡一筆 settling 的 intent、在鏈上躺一個沒人認領的交易。但我們現在有辦法解決了:鏈下的等到 StuckAfter 就送審,然後鏈上的只能等那筆交易上鏈直到 Sync 看到。

應該有資深的區塊鏈開發者會問:「鏈上那筆卡住的,拿同一個 nonce 再送一筆交易把它蓋掉不就好了?」對,但這部分我們應該明天才會細說。

序列化的範圍是帳戶

Counter 現在一個帳戶只發一個 nonce,交易終結後才發下一個。但實務上的 relayer 會同時操作好幾個 nonce、吞吐高很多,只要中間有一個沒發送成功,後面那些就全部在等它,而把洞填掉需要另一套手段。

所以提高吞吐不應該是「提升單個錢包的非同步交易數量」,而是「多開幾個發送錢包」。

圖上有三個數字,是三個不同的限制:worker 數是同時有幾份 job 在手上、名額數是同時有幾筆在往 RPC 送、錢包數是同時有幾筆能真的並行上鏈。直覺上這三個應該是同一個數字,但實際上「同時有幾筆能真的並行上鏈」常常是最小的那個(到了 Solana 與 SUI 這個數字甚至直接消失:Solana 的 blockhash 每筆各自讀,SUI 只要 gas coin 切得開,同一個地址要並行幾筆都可以)。

小結

今天在研究錢包如何併發送出交易:nonce 只有 relayer 這個發放點;拿不到 nonce 就原封不動回 queue;發送結果分成「已送出」、「確定沒發送」、「不知道」三種,只有第二種退得回去,第三種會跳號、整個帳戶停止發號。四條鏈裡只有 EVM 與 TON 需要上述這套東西,因為 Solana 與 SUI 把這個數字換成了「送出當下從鏈上讀一個值」。

明天我打算讓 relayer 有辦法處理卡住的交易。

明天見。


上一篇
Day 9 | Go Worker Pool 實戰:優雅處理高併發的 Tx 任務
下一篇
Day 11 | Transaction Replacement:卡鏈救援與手續費加速機制
系列文
Web3 支付工程筆記:企業級穩定幣結算系統與多鏈架構實戰11
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言