本日程式碼:repo tag day-23
目前系統今天認識幾條鏈?這取決於你問的對象是誰。finality 的預設規則認識四條,bulk 的交易上限只認識兩條,txseq 把鏈分成「自己發號」與「不用發號」兩類,txfee 只服務前一類。四份答案各自拿 evm、solana 這種字串當 key,而沒有任何地方保證它們講的是同一批鏈。TON 跟 SUI 就是現成的例子,兩條鏈在 finality 裡有不可逆規則、在 bulk 裡查不到交易上限,接了四分之一就停在那裡,而目前唯一會講出這件事的是一筆真的走到切批的付款。它拿回的是 ErrNoRules,而那可能是半夜。今天來把「一條鏈要回答哪些問題」收成一個介面。
今天要蓋的是一個小 package:chain。一個 Adapter 介面放必答題,四題:你叫什麼(協定名)、交易要不要排隊拿號、什麼時候不可逆、一筆交易裝得下多少;第五個問題「卡住的交易怎麼替換」刻意不在裡面,它是選答題,為什麼,後面講。一個 Registry 把 intent 身上的 evm:31337 對回 adapter,再加 EVM 與 Solana 兩個真的 adapter。規則本身一條都不搬家,不可逆規則還是住在 finality、上限還是住在 bulk,adapter 只當索引。搬進來的話,同一個數字就有兩個家,遲早不同步。
這一題可以參考的東西,一句是 Go 自家的箴言、一個是標準函式庫的先例、一份是跨鏈的命名規格:
namespace + ":" + reference:我們的 intent.Chain 從一開始就寫成 evm:31337,剛好是同一個形狀,所以 Registry 認冒號前面那一段就好做完之後 repo 中 internal/chain 的 Example_askTheRegistry 會拿三筆 intent 的鏈名去查 adapter,再把查得到的兩個各問一輪,輸出長這樣:
registry evm, solana
evm:31337 -> evm
solana:mainnet-beta -> solana
ton:mainnet -> chain: no adapter registered for this protocol: "ton"
evm slot 0x0A11…0001 #0
evm replace cap 30.000 gwei tip 2.000 gwei, bump 10%, ceiling 45.000 gwei, at most 3 broadcasts
evm final final when finalized; lost after 5m0s
evm batch gas cap 30,000,000
solana slot no slot needed
solana replace resend the same signed bytes
solana final final when finalized; lost after 2m0s
solana batch bytes cap 1,232, accounts cap 64
evm:31337 跟 evm:1 拿到同一個;ton 那行是一條沒接的鏈在這個系統裡的長相,查不到就回錯誤,沒有預設值evm:31337 對得回 evm,靠的就是它);replace 那行是那題選答。答案沒有一個是新的:取號那行是 txseq 的、替換那行是 txfee 的、不可逆與上限各是 finality 與 bulk 的,今天只是讓它們第一次出現在同一個地方Adapter 只有四個方法,一題一個:名字、發號、不可逆、上限,四題都是鏈下元件真的會問一條鏈的問題。要注意的是發送交易的方法不在這裡面。
type Adapter interface {
// Protocol 回報這個 adapter 服務的協定名(evm、solana),對應 intent.Chain 冒號前面那一段。
Protocol() string
// Sequencer 回報這條鏈發號的做法。同一個 adapter 永遠回同一個 sequencer:
// 發號的狀態(下一個號、有沒有空缺)只能有一份,兩份就是兩條互相撞號的線。
// 不用發號的鏈回 txseq.Unordered,呼叫端照常取號、收尾,不用寫 if(見 txseq 的 package 註解)。
Sequencer() txseq.Sequencer
// Finality 回報這條鏈的不可逆規則,listener 拿它判 confirming 的 intent。
Finality() finality.Policy
// BatchLimits 回報這條鏈一筆交易裝得下多少項付款,bulk.Pack 拿它切撥款名單。
BatchLimits() bulk.Limits
}
名單上明顯缺席的是送交易與讀鏈這兩個動作。它們早就有介面了:relayer 的 Sender 與 listener 的 Watcher,各自定義在使用它們的那個 package 裡,Sender 的註解從第一天就寫著「接真的鏈時,chain adapter 實作這個介面」。之後接上 RPC,是 adapter 去實作那兩個既有介面,而今天這個介面只收「不碰 RPC 就答得出來」的問題。payer 的授權長什麼樣也刻意不在名單上。EVM 上是一份 allowance,Solana 上是簽進 run 的那個 root,同一題連答案的型別都不一樣,硬收進介面只會得到一個誰都要繞過的欄位。
四題裡只有發號那題的答案是有狀態的,而這正是 Registry 規定「一個協定只准註冊一個 adapter」的理由。多一個 adapter 就多一條發號線,同一個錢包送出的交易會在兩條線上各拿各的號。
四條鏈裡只有 EVM 每題都有完整解答。Solana 對「怎麼發號」的答案是「不需要」,對「怎麼替換」則根本沒有答案。兩題長得很像,處理方式卻刻意不同。分界線是這個問題,答不出來的鏈照常被呼叫,會不會出事?
差別全在那個「會不會出事」。txseq.Unordered 被呼叫,取號永遠放行、收尾什麼都不做,relayer 白走一趟流程而已。替換的空實作被呼叫,送出去的是一筆可能重複付款的交易,因為 Solana 的交易由簽名識別,改了出價就是另一份簽名、另一筆交易,兩筆都可能上鏈。所以替換不進 Adapter,另開一個 Replacer 介面,用型別斷言問:
// Replacement 問一個 adapter 能不能替換。第二個回傳值是 false 時,卡住的交易只有一條路:
// 原封不動重送同一筆簽好的交易,而那件事不需要 relayer 介入。
func Replacement(a Adapter) (txfee.Policy, bool) {
r, ok := a.(Replacer)
if !ok {
return txfee.Policy{}, false
}
return r.ReplacementPolicy(), true
}
這一招在 repo 裡已經出現過兩次,OrderedSender 與 ReplacingSender 都是同一款,http.Flusher 那句「test for this ability at runtime」講的也是它。今天只是把這個模式從 relayer 內部推到鏈的邊界上,給它一個名字。
Registry 的查詢很薄,切出冒號前面的協定名、查表,查不到回 ErrUnknownChain。厚的是註冊。Register 逐題檢查,答不齊的 adapter 直接拒絕,協定名空著、不可逆規則什麼都不等、上限少一條出處,都過不了關。
vet 有十二個拒絕的理由,最值得講的兩個都在協定名上。一條擋帶網路後綴的名字,evm:31337 註冊不進去,因為同一個協定的每個網路共用一個 adapter,拿完整鏈名去註冊等於把一條鏈裂成兩條,evm:1 從此查不到。另一條擋大小寫,EVM 也註冊不進去,而這條本來可以擺在另一邊,For 查詢的時候順手折疊大小寫,兩種寫法就都查得到。For 選擇不折疊,寬容於是從查詢那一側收回來,變成註冊時的一次拒絕。反過來做也成立,改起來兩行的事,代價是每一筆 intent 分派的時候都多一次字串轉換。
這個檢查防的是「接了一半的鏈」拖到最壞的時刻才暴露:
同一個缺口,差別在誰面對它、修起來多貴:接線時炸開,修的是一行設定;半夜才炸開,值班的人得先從一份切不出批的名單一路查回「原來這條鏈根本沒接完」。錯誤越早出現越便宜,這條原則之前在 API 層擋重試、在 intake 擋壞行的時候各用過一次,今天用在鏈的接線上。
要誠實交代的是這個檢查的邊界。它驗的是答案齊不齊,驗不了答案對不對。一條把上限抄錯一個零的鏈照樣註冊得進去,釘住數字本身的還是各 package 自己的測試與出處欄位。Registry 保證的只有一件事:你不會問到一半,才發現對面根本沒有人。
這個介面賭的是「五題就是接一條鏈的全部」,而賭注的有效期限到下一條鏈為止。TON 與 SUI 真的接進來的那天,要嘛乖乖答完四題,要嘛帶來第六題,而第六題不只是多一個方法,是每個既有的 adapter 都得重新回答一次。
如果你的系統從頭到尾只跑一條鏈,這個 package 是純開銷,把 finality.Defaults()["evm"] 寫在呼叫端更短也更好讀。它開始划算的時間點不是第二條鏈接進來,是第二條鏈的答案跟第一條開始互相矛盾的時候。
明天來討論抽象的另一邊:這個 adapter 什麼都答得出來,唯獨「把一批付款變成一筆交易」這件事連介面都做不出來。什麼該讓每條鏈共用、什麼從一開始就不該,明天把兩條鏈的交易內容實際組出來對照。
明天見。