作者:b10c
來源:https://b10c.me/observations/16-slow-block-propagation-validation-signet/
原文出版於 2026 年 4 月。
在上週的 “Signet 繁難區塊演示” 活動期間,我運行了一個定製的 P2P 客戶端,連接到了運行在 IPv4 和 Tor 上的全部(大約 190 個)Signet 節點。我的目的是度量區塊在網絡中傳播的速度,以及各節點驗證區塊的速度。
本文最初出版於 bnoc.xyz 。
度量結果表明,區塊傳播速度受到了很大的影響,因為節點需要等待先收到區塊的對等節點先驗證完區塊,才能收到 blocktxn 響應消息以及本地缺失的交易,進而開始重新構造區塊、驗證和轉發。面對這些遠遠不算最壞情況的繁難區塊時,在可以觀察到的中值對等節點上,驗證速度降低了大約 160 倍。這個中值節點花費了 20 秒(s)來驗證一個繁難區塊,而驗證 Signet 上的普通區塊只需花費 176 毫秒(ms)。
(譯者注:“Signet” 為一個使用專屬出塊者簽名出塊的比特幣測試網絡。“繁難區塊” 為驗證起來格外緩慢的區塊。)
方法論
為了度量區塊傳播速度和驗證速度,來自對等節點的兩種宣告事件格外重要:
其一,BIP-152 高帶寬緻密區塊宣告。在我們使用 sendcmpct(1) 消息向對等節點請求高帶寬緻密區塊之後,對等節點們將在他們構造好區塊之後、驗證區塊之前,儘快給我們發送一條 cmpctblock 信息。當我們的客戶端收到來自一個對等節點的 cmpctblock 消息之後,它會通過發送 getblocktxn 信息來請求交易。一旦這個對等節點驗證完了區塊,他會給我們響應一條 blocktxn 信息。通過記錄 cmpctblock 消息和 blocktxn 消息到達本地的時間戳,我們就能推斷驗證時長。cmpctblock的時間戳也向我們表明了區塊傳播的速度(不摻雜驗證時間)。
其次, 低帶寬緻密區塊 INV(或者類似的 BIP-130 區塊頭)宣告,也包含了信息。它們發生在節點已經驗證了區塊之後。
我們定製的 P2P 客戶端也會記錄各節點的比特幣協議的 ping pong 往返通信延遲(RTT),從而能夠為網絡和應用層時延調整宣告消息的時間戳。
區塊傳播的時長,是宣告信息時間戳和我們收到的該區塊的第一條宣告的時間戳的差值。兩個時間戳都會用 RTT 來調整,調整項是 $\frac{1}{2}$ RTT :$\textit{ts_adjusted} = \textit{ts_raw} - \frac{1}{2} \textit{RTT}$ 。我們假設 RTT 是對稱的 1 。
區塊驗證的時長,則是一個對等節點給我們發送高帶寬緻密區塊宣告、與其發送對應 blocktxn 響應信息的時間差 2。我們不知道對等節點給我們發送一條信息的時間戳,但可以從收到信息的時間戳以及 RTT 中計算出來。整個過程的時間順序是這樣的:

- 這個事件序列圖展示了我們測量的時間段 -
- $t_0$ :對等節點完成區塊重構,並向我們發送一條高帶寬緻密區塊宣告
- $t_1$ :我們收到一條高帶寬緻密區塊宣告($t_0$ 之後經過 $\frac{1}{2} RTT$ )
- $t_2$ :我們發送
getblocktxn請求(假設是瞬時的) - $t_3$ :對等節點收到
getblocktxn請求($t_2$ 之後經過 $\frac{1}{2} RTT$ ) - $t_4$ :對等節點完成區塊驗證,發送
blocktxn(時間不確定) - $t_5$ :我們收到
blocktxn($t_4$ 之後經過 $\frac{1}{2} RTT$ )
我們要度量的是 $t_0$ 與 $t_4$ 兩個時間點之間的時長($= d$)。面對一個對等節點,我們有 $t_1$(和 $t_2$)、$t_5$ 和 RTT 。不過,因為在$t_0$ 與 $t_4$ 之間,我們必須完成一次完整的消息往返,當區塊驗證時間比 RTT 還要短時,我們將只能得到驗證時長的上界。在這種情況下,$d ≈ RTT$ 。
$$
\begin{align}
t_0 &= t_1 - \frac{1}{2} \textit{RTT} \\
t_2 &= t_1 \\
t_3 &= t_1 + \frac{1}{2} \textit{RTT} \\
t_4 &= t_5 - \frac{1}{2} \textit{RTT} \\
d &= t_4 - t_0 \\
\textit{longer-than-rtt} &= d > \textit{RTT}
\end{align}
$$
至於 RTT 本身,我們使用在連接活躍期間記錄下的 RTT 的中值。
結果
區塊傳播
關於區塊傳播,我們分別觀察了對等節點以高帶寬緻密區塊宣佈一個區塊的時間,和通過 INV 消息宣佈這個區塊的時間。我們也區分了普通區塊和專門構造的繁難區塊。

- 普通區塊和專門構造的繁難區塊的 INV 傳播速度和緻密區塊傳播速度(ECDF) -
在兩種區塊 —— 普通區塊和專門構造的繁難區塊 —— 上,高帶寬緻密區塊宣告的傳播速度都比 INV 宣告的傳播速度要快。這在意料之中,因為高帶寬緻密區塊宣告是在驗證區塊之前發送的,而 INV 宣告僅在區塊得到驗證之後才會發送。
普通區塊在 Signet 上的緻密區塊傳播時間(綠線)和 INV 傳播時間(藍線)是接近的。Signet 上的普通區塊通道不會包含很多交易,並且可以快速驗證。處在(前)25 分位的節點會在 150ms 之後向我們宣佈區塊;中值節點稍慢,稍微超過 200ms;75 分位節點在 300ms 以後;經過 600ms,90% 的對等節點都已經驗證完了區塊,並向我們發送了宣告。
-(僅限繁難區塊)區塊傳播 ECDF-
繁難區塊的傳播則有很大不同。雖然高帶寬緻密區塊宣告消息(黃線)都會在 INV 宣告消息之前到達,25 分位的節點在接近 14 秒之後才會向我們發送一條高帶寬緻密區塊宣告;而這些節點的 INV 消息會在 27.5 秒之後到達。中值節點在大約 26 秒之後向我們發送緻密區塊宣告,31.7 秒後發送 INV 信息。至於 75 分位的節點,是 31.7 秒後發送緻密區塊,55 秒後發送 INV 。90 分位對等節點會在 58 秒後以致密區塊宣佈繁難區塊,114 秒後通過 INV 消息宣佈。
這表明,區塊傳播的速度依賴於驗證性能。驗證起來較快的區塊,傳播起來也較快;難以驗證的區塊,傳播起來也較慢。這似乎出是可以理解的,因為節點要驗證完一個區塊之後才傳播它。但是,請主要,這種降速僅在節點需要用 getblocktxn 請求交易時才會發生。如果節點不需要請求交易,比如,因為恰好其交易池中包含了區塊內的所有交易,那麼它可以在收到 cmpctblock 消息之後就立即重構區塊並開始驗證它。慢速區塊中的繁難交易是非標準的,需要用 getblocktxn 來請求。如果實現了 “緻密區塊預填充” 技術,就像這個帖子說的那樣,那麼繁難區塊可能也會更快傳播。在這次演示中,這些繁難區塊都包含了僅僅一筆體積我 999kvB 的繁難交易。預填充這筆交易可以不會有什麼幫助,因為它是在是太大了。
區塊驗證
為了度量區塊驗證時長,我們只觀察高帶寬緻密區塊宣告和 blocktxn 響應。具體來說,我們的時長 d 定義為 d = t4 - t0(如前所述)。當實際驗證時長低於 RTT 時,我們度量出來的數值將是真實驗證時長的上限。這意味著,實際驗證時長可能比我們度量出來的數值要短。

- 普通區塊和繁難區塊的驗證時長分佈 -
上圖展示了常規區塊和專門構造的繁難區塊的驗證時長。在一些普通區塊上,我們只度量除了上界,實際驗證可能會更寬。
度量出來的 Signet 上的普通區塊的驗證時間從 10ms 到 2s 不等。而犯難區塊的驗證時間則從 2.5s 到 5 分鐘不等。

- 區塊驗證時長的 ECDF -
關於普通區塊(包括只度量出驗證時間上界的哪些),區塊驗證時長的 25 分位值為 37ms;50 分位值為 176ms;75 分位值為 447ms;90 分位值是 740ms 。Signet 網絡上的絕大部分監聽節點都能在 1 秒內驗證完普通區塊。
至於繁難區塊,25 分位值為 8.2s;50 分位值為 19.7s;75 分位值為 32s,90 分位值為 78.3s 。
為了對這個驗證速度獲得一個直觀印象,我們可以拿各個對等節點的普通區塊的驗證時間中值作為基準。然後,拿它與該節點的繁難區塊的驗證時間中值相比較、計算出一個乘數。

- 繁難區塊的驗證時間降速 ECDF -
在我的對等節點中,10 分位節點變慢了 13.5 倍;25 分位值為 31 倍;50 分位值為 159.4 倍;75 分位值為 793 倍;90 分位節點變慢了 1156 倍。Signet 網絡上的監聽節點在驗證這些繁難區塊時,性能似乎有很大差別。
在解讀這些數據時,我們必須緊記,這些只是 Antoine 選擇披露的繁難區塊,遠遠不是可能構造出來的最難驗證區塊。
本度量方法的準確性
為了驗證我們的度量方法是準確的,我們可以查看 -debug=bench -debug=validation -debug=net -logtimemicros=1 啟動標籤下的Bitcoin Core 的 debug.log(調試日誌),然後比較真實的驗證時間和度量出來的驗證時間。
下面的 debug.log 截圖展示了我的觀察節點經歷的第一輪演示中的第一個繁難區塊(詳見 delvingbitcoin.org)。我還添加了四個標記([m1] [m2] [m3] [m4])。
2026-04-08T14:05:12.292703Z [msghand] [cmpctblock] Successfully reconstructed block 0000000eb552c9f26e712d546c71297fd0623890299b40e7ada81d2dc32f5d0b with 1 txn prefilled, 0 txn from mempool (incl at least 0 from extra pool) and 1 txn (999557 bytes) requested2026-04-08T14:05:12.292795Z [msghand] [cmpctblock] Reconstructed block 0000000eb552c9f26e712d546c71297fd0623890299b40e7ada81d2dc32f5d0b required tx 1b44e7f59d39e4d53b4c4a77a650561de1871fe962fe6a17d1a302b877b2cb48[m1] 2026-04-08T14:05:12.422939Z [msghand] [validation] NewPoWValidBlock: block hash=0000000eb552c9f26e712d546c71297fd0623890299b40e7ada81d2dc32f5d0b2026-04-08T14:05:12.423680Z [msghand] [net] PeerManager::NewPoWValidBlock sending header-and-ids 0000000eb552c9f26e712d546c71297fd0623890299b40e7ada81d2dc32f5d0b to peer=52026-04-08T14:05:12.423794Z [msghand] [net] sending cmpctblock (358 bytes) peer=52026-04-08T14:05:12.543864Z [msghand] [bench] - Using cached block2026-04-08T14:05:12.543926Z [msghand] [bench] - Load block from disk: 0.07ms2026-04-08T14:05:12.543962Z [msghand] [bench] - Sanity checks: 0.01ms [0.00s (0.01ms/blk)]2026-04-08T14:05:14.166485Z [msghand] [bench] - Fork checks: 1622.48ms [1.96s (93.29ms/blk)]2026-04-08T14:05:14.643331Z [msghand] [bench] - Connect 2 transactions: 476.84ms (238.419ms/tx, 1.189ms/txin) [0.69s (32.64ms/blk)]2026-04-08T14:06:33.383188Z [msghand] [bench] - Verify 401 txins: 79216.70ms (197.548ms/txin) [79.43s (3782.32ms/blk)]2026-04-08T14:06:33.385563Z [msghand] [bench] - Write undo data: 2.39ms [0.07s (3.19ms/blk)]2026-04-08T14:06:33.385643Z [msghand] [bench] - Index writing: 0.11ms [0.00s (0.08ms/blk)]2026-04-08T14:06:33.385754Z [msghand] [validation] BlockChecked: block hash=0000000eb552c9f26e712d546c71297fd0623890299b40e7ada81d2dc32f5d0b state=Valid2026-04-08T14:06:33.385862Z [msghand] [bench] - Connect total: 80841.94ms [81.46s (3879.22ms/blk)]2026-04-08T14:06:33.832999Z [msghand] [bench] - Flush: 447.07ms [0.51s (24.52ms/blk)]2026-04-08T14:06:33.833198Z [msghand] [bench] - Writing chainstate: 0.27ms [0.00s (0.19ms/blk)]2026-04-08T14:06:33.833668Z [msghand] [validation] Enqueuing MempoolTransactionsRemovedForBlock: block height=299177 txs removed=02026-04-08T14:06:33.833839Z [msghand] UpdateTip: new best=0000000eb552c9f26e712d546c71297fd0623890299b40e7ada81d2dc32f5d0b height=299177 version=0x20000000 log2_work=43.630312 tx=29147716 date='2026-04-08T14:03:39Z' progress=1.000000 cache=15.3MiB(114240txo)2026-04-08T14:06:33.833856Z [msghand] [bench] - Connect postprocess: 0.66ms [1.57s (74.55ms/blk)][m2] 2026-04-08T14:06:33.833868Z [msghand] [bench] - Connect block: 81290.01ms [83.55s (3978.55ms/blk)]2026-04-08T14:06:33.833926Z [msghand] [validation] Enqueuing BlockConnected: block hash=0000000eb552c9f26e712d546c71297fd0623890299b40e7ada81d2dc32f5d0b block height=2991772026-04-08T14:06:33.833962Z [msghand] [validation] Enqueuing UpdatedBlockTip: new block hash=0000000eb552c9f26e712d546c71297fd0623890299b40e7ada81d2dc32f5d0b fork block hash=0000000bc5d91380fa9188acfabd6a59244e8e6e744b0a0ef07064968027e256 (in IBD=false)2026-04-08T14:06:33.834043Z [msghand] [validation] ActiveTipChange: new block hash=0000000eb552c9f26e712d546c71297fd0623890299b40e7ada81d2dc32f5d0b block height=299177[m3] 2026-04-08T14:06:33.834887Z [msghand] [net] received: headers (82 bytes) peer=102026-04-08T14:06:33.835052Z [msghand] [net] sending ping (8 bytes) peer=102026-04-08T14:06:34.062891Z [scheduler] [validation] MempoolTransactionsRemovedForBlock: block height=299177 txs removed=02026-04-08T14:06:34.063548Z [scheduler] [validation] BlockConnected: block hash=0000000eb552c9f26e712d546c71297fd0623890299b40e7ada81d2dc32f5d0b block height=2991772026-04-08T14:06:34.063728Z [scheduler] [validation] UpdatedBlockTip: new block hash=0000000eb552c9f26e712d546c71297fd0623890299b40e7ada81d2dc32f5d0b fork block hash=0000000bc5d91380fa9188acfabd6a59244e8e6e744b0a0ef07064968027e256 (in IBD=false)[m4] 2026-04-08T14:06:34.461329Z [msghand] [net] received: headers (82 bytes) peer=42026-04-08T14:06:34.461786Z [msghand] [net] sending ping (8 bytes) peer=11... -> p2p communication continues[m1]:在2026-04-08T14:05:12.422939,NewPoWValidBlock標誌著開始驗證這個區塊[m2]:在2026-04-08T14:06:33.833868,分支日誌顯示Connect block: 81290.01ms[m3]:在2026-04-08T14:06:33.834887,P2P 通信基本恢復[m4]:在2026-04-08T14:06:34.461329,我們以UpdatedBlockTip(更新區塊鏈頂端)結束,P2P 通信完全恢復
[m1] 與 [m4] 之間的差值為 82038ms ,而我們度量出來的數值為 82049ms(只差 11ms)。回顧第一輪的情形,兩者的差值也並不總是這麼小。
| 度量得到的值 | 實際值(來自日誌) | 相差 | 偏差率 | 區塊 |
|---|---|---|---|---|
| 82049ms | 82038ms | 11ms | 0.013% | 0000000eb552c9f26e712d546c71297f.. |
| 82478ms | 81814ms | 664ms | 0.81% | 000000002b3a132836666c18f5e1a9d9.. |
| 76937ms | 76368ms | 569ms | 0.74% | 00000006d34037534a517f9e5809a347.. |
| 79382ms | 78667ms | 715ms | 0.90% | 00000014a4cae4501f98539b45c76059.. |
| 79894ms | 78571ms | 1323ms | 1.68% | 00000003220437cb8b5a2edef6be828c.. |
| 93556ms | 93541ms | 15ms | 0.016% | 000000143c97bf0134c5cf0881dfd4ef.. |
較高的度量誤差出現在中間的區塊,可能是因為對等節點擠壓了大量 P2P 消息需要先處理,然後才會響應我們的 getblocktxn 消息,或是宣佈下一個緻密區塊。在這裡,1% 的偏差率是可以接受的。還請注意,這並不表明我們對所有對等節點的度量都是準確的。
結論
我們成功在 Signet 網絡上度量了監聽節點之間的高帶寬緻密區塊傳播速度和 INV 宣告的傳播速度。在繁難區塊出現時,因為它們在同一時間廣播,區塊傳播速度大大降低。
我們也度量了區塊驗證時長。在 Signet 的普通網絡條件下,區塊驗證起來有時會比節點之間的通信往返還要快。在這種時候,我們只能度量出驗證時間的上限。對於繁難區塊,我們度量得出,50 分位節點的驗證時間降速是 160 倍(使用本次演示中曝光的繁難區塊)。繁難區塊需要超過 20 秒來驗證。
未來的工作可以在主網上探索區塊傳播和驗證時間。
- - -
我已經在這裡發佈了我用來創建這些圖表的 Jupyter notebook 。歡迎修改和延申本研究。
- - -
1. 在 Tor 上這一點可能不成立。 ↩
2. 有時候,尤其在多個繁難區塊的致命區塊到達對等節點、排隊等待驗證的時候,我們只有等到所有排隊的區塊都驗證完成之後才能收到返回的 getblocktxn 信息。這時候,我就使用下一次緻密區塊宣告的時間。它們會在下一次區塊驗證開始之前發送。 ↩
(完)

