iT邦幫忙

2025 iThome 鐵人賽

DAY 7
1

看完了區塊,兩人開始檢視第 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 ,和最終的手續費相同。

「但我看到組成手續費的欄位好多,不是只有燃氣用量、系統費、和小費嗎?」

「因為使用者在發送交易的時候,並不會知道交易打包到區塊後,最後燃氣會用多少、系統費會飄到哪」小雨說「使用者可以給他們一些限制以避免手續費變成天價」

  • 交易燃氣上限(Gas limit) 會指定這筆交易最多可以花到多少燃氣,撞到這個上限這交易就會停止運算,避免超支手續費。
  • Gas fees 那欄中間的 Max 是 燃氣單價上限(Max Gas Fee) ,限制燃氣單價最高收到哪裡。使用者可以預期燃氣上限乘上這個數字,就是手續費最多不會付超過這個乘積。
  • Gas fees 最後欄的 優先費上限(Max priority) 可以限制給驗證者的優先費。使用者並不直接設定優先費要給多少,而是設定假設燃氣單價上限扣掉系統費還剩很多,剩下的願意給驗證者給到多少。

特務 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 也注意到了一些陌生的欄位,小雨解說如下:

  • blockHash 與 blockNumber 是區塊雜湊值和區塊高度
  • chainId 是鏈的編號。1 號是以太坊主網路的意思。這個欄位很重要,因為以前曾經有 ETC 區塊鏈分岔,同一條鏈變兩條。使用者的帳戶在 ETH 和 ETC 上各有一份。當時沒有 chainId 這個設計,假設你在 ETH 鏈上發一筆交易把幣打給我,我可以在 ETC 上重覆播放這筆交易,你 ETC 上面的錢也會變我的。
  • nonce 計算這是從發送方帳戶送出的第幾筆交易。發送方帳戶底下也有記錄一個 nonce 數字。如果交易的 nonce 與發送方帳戶的數字不一樣,就是無效交易。如果沒有這個設計,你發送給我一顆以太幣,我可以重放這筆交易直到你帳戶沒錢。
  • input 是比較複雜的合約交易會使用到。我們後面看到合約交易時再談。
  • r, s, yParity 是 數位簽章 的欄位。數位簽章是一種密碼學演算法,簽章者有一對公鑰和私鑰。簽章僅私鑰持有者有能力產出。如果一個簽章能通過驗章演算法的查核,代表交易確實是由私鑰的持由者,也就是發送方,所發出。
  • accessList 先略過不談

特務 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 回想剛剛經歷了什麼。從區塊鏈瀏覽器和全節點上面看到的交易資料,已經補上了許多導出的欄位。真正在區塊鏈網路中廣播的交易原始資料,包含了下面內容:

  • 「誰?給誰?多少錢?」的部分:交易原始資料有收受方與轉帳金額的欄位。發送方地址 不用 在交易原始資料中註明,因為可以從簽章導出。
  • 代表發送方轉帳的意志:要有發送方的簽章(r, s, v 欄位),以及避免重放攻擊的欄位: Nonce 與 ChainID 欄位。
  • 限制手續費的欄位:maxPriorityFeePerGas, maxFeePerGas, gasLimit 等等。
  • 最後,複雜的運算有 data, accesslist 等。

「我們已經知道太多和交易相關的事了,但這些有點重要,因為後面我們提到一些擴展方案時,相關的概念會派上用場。」小雨總結。


上一篇
懂得自我調息的電腦
下一篇
人可以撞衫 資料不能撞號
系列文
那個有好多好多節點的電腦調查報告8
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言