看完了區塊,兩人開始檢視第 23231122 號區塊底下的一些交易。
以太坊的交易可以非常複雜,小雨先挑了一筆最單純的 交易 ,只有做發送以太幣這件事。
Transaction hash 0x357ad7fb6b2c2ada106471746ee3c6561e08573a2137cd18317b2a8248fc36e8
Status and method Success
Block 23231122
Timestamp Aug 27 2025 15:59:47 PM (+08:00 UTC) | Confirmed within <= 12 secs
-----------------
From Kraken: Hot Wallet
To 0xCF2dcF8375373BF9a2Dee424358B9de34d158Edf
-----------------
Value 0.0001565976 ETH($0.687875315688)
Transaction fee 0.000050469443304 ETH ($0.22169359074044952)
Gas price 0.000000002403306824 ETH (2.403306824 Gwei)
Gas usage & limit by txn 21,000 | 21,000 100%
Gas fees (Gwei) Base: 0.403306824 | Max: 2.845306312 | Max priority: 2
Burnt fees 0.000008469443304 ETH ($0.03720313074044952)
「什麼是 交易(Transaction) 呢?」特務 K 問
「交易本身是使用者發出的一筆資料,請求電腦執行某種任務」小雨說「像是轉帳之類」
「我們在畫面看到好多欄位,應該怎麼開始呢?」
「如果把交易視為轉帳的話,我會說首先想到的欄位就是:誰?、給誰?、多少錢?」小雨說「相對應的是 發送方(From) 、 收受方(To) 、 轉帳金額(Value) 」
特務 K 在畫面中照到相應的欄位。「轉帳金額的單位是以太幣 ETH 對吧」
「沒錯,然後發送方和收受方是以太坊的 20 位元組地址格式」小雨說「但瀏覽器網站有時會把地址代換為他們已經確認的身份,會以英文表示。」
「我看到上面有 手續費(Transaction fee) 這個欄位,這是上次講的系統費和小費加總對吧」
「是的。」
特務 K 把燃氣單價(Gas Price)的 0.000000002403306824 ETH 乘上燃氣用量(Gas usage)的 21000 ,得到了 0.000050469443304 ETH ,和最終的手續費相同。
「但我看到組成手續費的欄位好多,不是只有燃氣用量、系統費、和小費嗎?」
「因為使用者在發送交易的時候,並不會知道交易打包到區塊後,最後燃氣會用多少、系統費會飄到哪」小雨說「使用者可以給他們一些限制以避免手續費變成天價」
特務 K 試著用實際的數字拼湊小雨的說明。交易燃氣上限是 21000 單位,實際使用的燃氣也是 21000 單位。
「使用者很確定這筆交易就是花費 21000 燃氣嗎?」
「純以太幣的交易是最基礎的交易,因此 21,000 是一個確定的數字」小雨說「據說 21,000 是致敬比特幣的最終發行量 2100 萬比特幣,但在以太坊這個數字背後主要是反映要驗證數位簽章的負擔」
特務 K 又把燃氣單價上限(Max)的 2.845306312 Gwei ,減掉系統決定的燃氣單價(Base) 0.403306824 Gwei ,得到 2.441999488 Gwei 。算出來的數字代表付完系統費之後,還剩下的燃氣單價。這個部分能分給驗證者作為優先費。
然而使用者設定了優先費上限 2 Gwei ,代表 2.441999488 Gwei 裡面,只有 2 Gwei 會給驗證者,但使用者會保留剩下的 0.441999488 Gwei。
因此最後的總燃氣單價是系統費 0.403306824 Gwei 加上優先費 2 Gwei ,得到 2.403306824 Gwei 。這個數字和 Gas price 那個欄位的一樣。
特務 K 決定把 Burnt fees 那個欄位怎麼算出來的任務留給讀者。
「從瀏覽器看交易的好處是方便」小雨說「壞處是加了一些有的沒有的欄位,像廣告之類的,有礙我們理解」
「你建議怎麼做呢?」
「我們從全節點看交易」小雨捏了個鍵訣,掐出了終端機,一句 geth attach
喚出了執行客戶端 geth 的主控台。
「我們怎麼讓客戶端知道要找哪筆交易呢?」特務 K 問。
「 交易的雜湊值(Transaction hash) 就像是那筆交易的身分證一樣」小雨說「可以用它來指稱那筆交易」
「我們找一天得好好談一下這個雜湊值,已經看到好多十六進位值了,看得我身心不適」特務 K 說。
「我們會談到的」
geth 的主控台讓使用者可以用 JavaScript 進行操作。找到交易的方式如下:
> eth.getTransaction("0x357ad7fb6b2c2ada106471746ee3c6561e08573a2137cd18317b2a8248fc36e8")
{
accessList: [],
blockHash: "0xb0f2f8b10e60f69e1d7f96c1646ac4fe4dd7af6cca5c2d5e0f2e559c37426fbc",
blockNumber: 23231122,
chainId: "0x1",
from: "0x267be1c1d684f78cb4f6a176c4911b741e4ffdc0",
gas: 21000,
gasPrice: 2403306824,
hash: "0x357ad7fb6b2c2ada106471746ee3c6561e08573a2137cd18317b2a8248fc36e8",
input: "0x",
maxFeePerGas: 2845306312,
maxPriorityFeePerGas: 2000000000,
nonce: 5185048,
r: "0xac970f5cdec725a09c2b11f39530a65191fac6ed163bd800be365cd6bfc52a47",
s: "0x1909d4e4e4ff68cd60c08741456fbf4232394eef46bb2717ff6c8b245b4d3f12",
to: "0xcf2dcf8375373bf9a2dee424358b9de34d158edf",
transactionIndex: 66,
type: "0x2",
v: "0x1",
value: 156597600000000,
yParity: "0x1"
}
特務 K 湊近一看。因為主控台的輸出是照英文字母排序的,他花了點時間辨識出發送方(from)、 收受方(to) 、 轉帳金額(value)。
的確 from 和 to 的欄位都是地址。他數了一數,不計 0x 的話,有 40 個數字或英文字母。
「你剛剛說以太坊地址格式是 20 位元組?」特務 K 問。
「但字母和數字怎麼是 40 個對吧?」小雨知道特務 K 想問什麼,他以前也遇到同樣的問題。「因為一個位元組要兩個十六進位位數表示」
「一個十六進位的位數是 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, a, b, c, d, e, f ,共 16 個,比十進位的多了 6 個變化。一個位元組(Byte)是 8 個位元(Bit),共 2^8 = 256 種組合。要兩個十六進位位數 16 * 16 才能表達 256 種組合」小雨補充。
特務 K 注意到 value 的部分是個整數,與儀表板的差了 ... 十八個零。 maxFeePerGas、 maxPriorityFeePerGas、 gasPrice 等等也都與先前儀表板數字相差一些位數。共通點是都是整數。
「系統中確實都要用整數運算,他們單位都是 Wei」小雨說。
特務 K 也注意到了一些陌生的欄位,小雨解說如下:
特務 K 是個刀頭打滾的人,經歷多少生死關頭、看過多少黑吃黑,但網路的世界有另一種說不出的兇險,讓他感到十分脆弱。
「看起來數位簽章只代表發送方曾經簽過章,但章是不是已經用過了、無效了,則需要配合其他的設計」特務 K 評論。
小雨對節點產出的交易欄位仍然不是很滿意,還是加入太多細節了。反手敲了另一個指令,叫出了最原始,最生鮮的交易格式。
> eth.getRawTransaction("0x357ad7fb6b2c2ada106471746ee3c6561e08573a2137cd18317b2a8248fc36e8")
"0x02f87301834f1e18847735940084a997edc882520894cf2dcf8375373bf9a2dee424358b9de34d158edf868e6cb852180080c001a0ac970f5cdec725a09c2b11f39530a65191fac6ed163bd800be365cd6bfc52a47a01909d4e4e4ff68cd60c08741456fbf4232394eef46bb2717ff6c8b245b4d3f12"
特務 K 眼前一黑,那麼多的十六進位碼讓他覺得失去平衡感,腳站不太穩。
但他還是勉強想看看能不能從這團亂碼中看出些什麼。他搜尋了發送方的地址,沒有匹配。再試試收受方 cf2dcf8375373bf9a2dee424358b9de34d158edf
,咦!有匹配。
「原始的交易格式是用 RLP 編碼格式 緊湊排列而成」小雨說。「RLP 主要編碼陣列以及巢狀陣列等。要解編碼的話,要知道約定的格式是什麼,哪個欄位在陣列哪個位置」
小雨的百寶袋中,拿出 https://playground.ethers.org/ ,先複製貼上前面 eth.getRawTransaction 拿到的原始資料
> raw = "0x02f87301834f1e18847735940084a997edc882520894cf2dcf8375373bf9a2dee424358b9de34d158edf868e6cb852180080c001a0ac970f5cdec725a09c2b11f39530a65191fac6ed163bd800be365cd6bfc52a47a01909d4e4e4ff68cd60c08741456fbf4232394eef46bb2717ff6c8b245b4d3f12"
用方便函式可以解析出整筆交易
> tx = Transaction.from(raw)
Transaction {
chainId: 1n,
data: "0x",
from: "0x267be1C1D684F78cb4F6a176C4911b741E4Ffdc0",
gasLimit: 21000n,
hash: "0x357ad7fb6b2c2ada106471746ee3c6561e08573a2137cd18317b2a8248fc36e8",
nonce: 5185048,
signature: Signature {
r: "0xac970f5cdec725a09c2b11f39530a65191fac6ed163bd800be365cd6bfc52a47",
s: "0x1909d4e4e4ff68cd60c08741456fbf4232394eef46bb2717ff6c8b245b4d3f12",
v: 28
},
to: "0xCF2dcF8375373BF9a2Dee424358B9de34d158Edf",
type: 2,
value: 156597600000000n
}
「這一步做了很多事情,很多欄位是透過額外步驟還原出來的」小雨說。
特務 K 注意到,和區塊相關的資料都不見了。他們的確不該出現在原始交易資料中,一開始 eth.getTransaction
的輸出資訊有區塊,是節點自己加入的。
「我們先確認一下交易雜湊值,用 keccak256 雜湊函式從原始資料導出。」
> keccak256(raw)
"0x357ad7fb6b2c2ada106471746ee3c6561e08573a2137cd18317b2a8248fc36e8"
特務 K 確認這個值和先前看到的交易雜湊值一樣。
「我們來做個 RLP 解編碼」小雨說
> decodeRlp(raw)
"unexpected junk after rlp payload (argument=\"data\", value=\"0x02f87301834f1e18847735940084a997edc882520894cf2dcf8375373bf9a2dee424358b9de34d158edf868e6cb852180080c001a0ac970f5cdec725a09c2b11f39530a65191fac6ed163bd800be365cd6bfc52a47a01909d4e4e4ff68cd60c08741456fbf4232394eef46bb2717ff6c8b245b4d3f12\", code=INVALID_ARGUMENT, version=6.13.2)"
不過小雨很痛苦的 學到 ,以太坊的 EIP1559 升級前綴了一個不屬於 RLP 編碼的位元組標記(type 的 2),要先拿掉才解得開。
> decodeRlp("0x"+raw.slice(4))
[ "0x01", "0x4f1e18", "0x77359400", "0xa997edc8", "0x5208", "0xcf2dcf8375373bf9a2dee424358b9de34d158edf", "0x8e6cb8521800", "0x", [], "0x01", "0xac970f5cdec725a09c2b11f39530a65191fac6ed163bd800be365cd6bfc52a47", "0x1909d4e4e4ff68cd60c08741456fbf4232394eef46bb2717ff6c8b245b4d3f12" ]
稍微整理一下十六進位字串和數字之後,特務 K 可以辨識出一些先前看過的欄位:chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, value, data, accesslist, v, r, s。沒有發送方 from 這個欄位
> decodeRlp("0x"+raw.slice(4)).map(x=> (x!="0x" && x.length > 0 && x.length <20) ? getBigInt(x): x)
[ 1n, 5185048n, 2000000000n, 2845306312n, 21000n, "0xcf2dcf8375373bf9a2dee424358b9de34d158edf", 156597600000000n, "0x", [], 1n, "0xac970f5cdec725a09c2b11f39530a65191fac6ed163bd800be365cd6bfc52a47", "0x1909d4e4e4ff68cd60c08741456fbf4232394eef46bb2717ff6c8b245b4d3f12" ]
「發送方的地址呢?」特務 K 一直在等這個欄位,不知道為什麼一直沒看到。
「他其實是從簽章變出來的」小雨說「以太坊用的 ECDSA 簽章可以變出公鑰,而地址是公鑰導出來的。」
ether.js 的 Transaction 結構 可以幫我們整理好未簽署的欄位有哪些。
> tx.unsignedSerialized
"0x02f001834f1e18847735940084a997edc882520894cf2dcf8375373bf9a2dee424358b9de34d158edf868e6cb852180080c0"
解編碼
> decodeRlp("0x"+tx.unsignedSerialized.slice(4))
[ "0x01", "0x4f1e18", "0x77359400", "0xa997edc8", "0x5208", "0xcf2dcf8375373bf9a2dee424358b9de34d158edf", "0x8e6cb8521800", "0x", [] ]
再用力解。chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, value, data, accesslist。沒有簽章相關的 v, r, s,合理。
> decodeRlp("0x"+tx.unsignedSerialized.slice(4)).map(x=> (x!="0x" && x.length > 0 && x.length <20) ? getBigInt(x): x)
[ 1n, 5185048n, 2000000000n, 2845306312n, 21000n, "0xcf2dcf8375373bf9a2dee424358b9de34d158edf", 156597600000000n, "0x", [] ]
簽章
> tx.signature
Signature {
r: "0xac970f5cdec725a09c2b11f39530a65191fac6ed163bd800be365cd6bfc52a47",
s: "0x1909d4e4e4ff68cd60c08741456fbf4232394eef46bb2717ff6c8b245b4d3f12",
v: 28
}
「所以簽章實際上是簽署什麼呢?」特務 K 問
「簽章對雜湊值簽署」小雨說
「可是我們前面交易雜湊值,是從包含簽章的交易資料中導出的,必須要先有簽章才有交易雜湊值對吧?」
「對!所以簽章簽署的是未簽署的欄位導出的雜湊值,像下面這樣」
> keccak256(tx.unsignedSerialized)
"0x6e4d7291fe1da9e44befece4d5dd07e1706b86e0f55e31554ad846eef0105f1d"
「我們最後用 ether.js 裡面這個 recoverAddress 函式,進行最後一個步驟」小雨說
> recoverAddress(keccak256(tx.unsignedSerialized), tx.signature)
"0x267be1C1D684F78cb4F6a176C4911b741E4Ffdc0"
「是發送方的地址!!」特務 K 驚呼「從簽章和被簽署的資料導出了」
特務 K 回想剛剛經歷了什麼。從區塊鏈瀏覽器和全節點上面看到的交易資料,已經補上了許多導出的欄位。真正在區塊鏈網路中廣播的交易原始資料,包含了下面內容:
「我們已經知道太多和交易相關的事了,但這些有點重要,因為後面我們提到一些擴展方案時,相關的概念會派上用場。」小雨總結。