深度解析:在發送1個DAI時發生了什麼?
BlockBeats 律動財經 2023-05-16 19:00
你有 1 個 DAI,使用錢包(如 Metamask)發送 1 個 DAI 到「0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045」(就是 vitalik.eth),點擊發送。
一段時間後,錢包顯示交易已被確認。突然,vitalik.eth 現在有了 1 個 DAI 的財富。這背後到底發生了什麼?
讓我們回放一下。並以慢動作回放。
準備好了嗎?
構建交易
錢包是便於向以太坊網路發送交易的軟體。
交易只是告訴以太坊網路,你作為一個用戶,想要執行一個行動的一種方式。在此案例中,這將是向 Vitalik 發送 1 個 DAI。而錢包(如 Metamask)有助於以一種相對簡單的方式建立這種交易。
讓我們先來看看錢包將建立的交易,可以被表示為一個帶有字段和相應數值的對象。
我們的交易開始時看起來像這樣:
其中字段 to 說明目標地址。在此案例中,「0x6b175474e89094c44da98b954eedeac495271d0f」是 DAI 智能合約的地址。
等等,什麼?
我們不是應該發送 1 個 DAI 給 Vitalik 嗎?to 不應該是 Vitalik 的地址嗎?
嗯,不是。要發送 DAI,必須製作一個交易,執行儲存在區塊鏈(以太坊數據庫的花哨名稱)中的一段代碼,將更新 DAI 的記錄餘額。執行這種更新的邏輯和相關儲存都保存在以太坊數據庫中的一個不可改變的公共計算機程序中 - DAI 智能合約。
因此,你想建立一個交易,告訴合約「嘿,夥計,更新你的內部餘額,從我的餘額中取出 1 個 DAI,並添加 1 個 DAI 到 Vitalik 的餘額」。在以太坊的行話中,「hey buddy」這句話翻譯為在交易的「to」字段中設置 DAI 的地址。
然而,「to」字段是不夠的。從你喜歡的錢包的用戶界面中提供的資訊,錢包會要求你填寫其他幾個字段,以建立一個格式良好的交易:
所以你給 Vitalik 發送 1 個 DAI,你既沒有使用 Vitalik 的地址,也沒有在 amount 字段里填上 1。這就是生活的艱難(而我們只是在熱身)。amount 字段實際上包含在交易中,表示你在交易中發送多少 ETH(以太坊的原始貨幣)。由於你現在不想發送 ETH,那麼錢包會正確地將該字段設置為 0。
至於「chainId」,它是一個指定交易執行的鏈的字段。對於以太坊 Mainnet,它是 1。然而,由於我將在 mainnet 的本地 Fork 上運行這個實驗,我將使用其鏈 ID:31337,其他鏈有其他標識符。
那「nonce」字段呢?那是一個數字,每次你向網路發送交易時都應該增加。它是一種防禦機制,以避免重放問題。錢包通常為你設置這個數字。為了做到這一點,他們會查詢網路,詢問你的帳戶最新使用的 nonce 是什麼,然後相應地設置當前交易的 nonce。在上面的例子中,它被設置為 0,儘管在現實中它將取決於你的帳戶所執行的交易數量。
我剛才說,錢包「查詢網路」。我的意思是,錢包執行對以太坊節點的只讀調用,而節點則回答所要求的數據。從以太坊節點讀取數據有多種方式,這取決於節點的位置,以及它所暴露的 API 種類。
讓我們想象一下,錢包可以直接網路訪問一個以太坊節點。更常見的是,錢包與第三方供應商 (如 Infura、Alchemy、QuickNode 和許多其他供應商) 交互。與節點交互的請求遵循一個特殊的協議來執行遠程調用。這種協議被稱為JSON-RPC。
一個試圖獲取帳戶 nonce 的錢包請求將類似於這樣:
其中「0x6fC27A75d76d8563840691DDE7a947d7f3F179ba」將是發起者的帳戶。從響應中你可以看到,它的 nonce 是 0。
錢包使用網路請求(在此案例中,通過 HTTP)來獲取數據,請求節點暴露的 JSON-RPC 端點。上面我只包括了一個,但實際上錢包可以查詢任何他們需要的數據來建立一個交易。如果在現實生活中,你注意到有更多的網路請求來查詢其他東西,請不要驚訝。例如,下面是一個本地測試節點在幾分鐘內收到的 Metamask 流量快照:
交易的數據字段
DAI 是一個智能合約。它的主要邏輯在以太坊主網的地址「0x6b175474e89094c44da98b954eedeac495271d0f」實現。
更具體地說,DAI 是一個符合 ERC20 標準的同質 Token -- 一種特殊的合約類型。意思是 DAI 至少實現ERC20 規範中詳述的接口。用 (有點牽強的)web2 術語來說,DAI 是一個運行在以太坊上的不可變的開源網路服務。鑒於它遵循 ERC20 規範,我們有可能提前知道 (不一定要看源代碼) 與它交互的確切暴露的接口。
簡短的附帶說明:不是所有的 ERC20 Token 都是這樣。實現某種接口(有利於交互和集成),單不能保證具體的行為。不過,在這個練習中,我們可以安全地假設 DAI 在行為上是相當標準的 ERC20 Token。
在 DAI 智能合約中,有許多功能(源代碼可在這裡),其中許多直接來自 ERC20 規範。特別值得注意的是外部轉移(external transferr) 函數。
這個函數允許任何持有 DAI Token 的人將其中一部分轉賬到另一個以太坊帳戶。它的簽名是「transfer(address,uint256)」。其中第一個參數是接收方帳戶的地址,第二個參數是無符號整數,代表要轉賬的 Token 數量。
現在我們不關注該函數行為的具體細節。相信我,你會了解到的,該函數將發送方的餘額減去所傳遞的金額,然後相應地增加接收方的金額。
這一點很重要,因為當建立一個交易與智能合約交互時,人們應該知道合約的哪個函數要被執行。以及要傳遞哪些參數。這就像在 web2 中,你想向一個網路 API 發送一個 POST 請求。你很可能需要在請求中指定確切的 URL 和它的參數。這也是一樣的。我們想轉移 1 個 DAI,所以我們必須知道如何在交易中指定它應該在 DAI 智能合約上執行「轉移」功能。
幸運的是,這是非常直接和直觀的。
哈哈,我開玩笑。不是的。
下面是你在交易中必須包含的內容,以發送 1 個 DAI 給維塔利克(記住,地址「0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045」):
讓我解釋一下。
為了簡化集成,並有一個標準化的方式來與智能合約交互,以太坊生態系統採用(某種形式)的「合約 ABI 規範」(ABI 代表應用二進制接口)。在普通使用場景中,我強調,在普通使用場景中,為了執行智能合約功能,你必須首先按照合約 ABI 規範對調用進行編碼。更高級的使用場景可能不遵循這個規範,但我們肯定不會進入這個兔子洞。我只想說,用Solidity編程的常規智能合約,如 DAI,通常遵循合約 ABI 規範。
你可以看到上面是用 DAI 的「transfer(address,uint256)」函數將 1 個 DAI 轉移到地址「0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045」的 ABI 編碼的結果字節。
現在有很多工具可以對交易進行 ABI 編碼(如:https://chaintool.tech/calldata),而且大多數錢包都以某種方式實現 ABI 編碼來與合約交互。為了這個例子,我們可以用一個叫做 cast 的命令行工具來驗證上面的字節序列是否正確,它能夠用特定的參數對調用進行 ABI-編碼:
有什麼困擾你的嗎?有什麼問題嗎?
哦,對不起,是的。那個 100000000000000。說實話,我真的很想在這裡為你提供一個更有力的論據。很多 ERC20 Token 都用 18 位小數表示。比如說 DAI。
在合約里我們只能使用無符號整數。因此,1 個 DAI 實際上被儲存為 1 * 10^18 - 這是 100000000000000。
現在我們有一個漂亮的 ABI 編碼的字節序列,包含在交易的「data」字段中。現在看來是這樣的:
一旦我們進入交易的實際執行階段,我們將重新審視這個「data」字段的內容。
Gas
下一步是決定為交易支付多少錢。因為請記住,所有交易都必須向花費時間和資源來執行和驗證它們的節點網路支付費用。
執行交易的費用是以 ETH 支付的。而 ETH 的最終數額將取決於你的交易消耗了多少凈 Gas(也就是計算成本有多高),你願意為每個 Gas 單位的花費支付多少錢,以及網路願意接受的最低數額。
從用戶的角度來看,通常是,支付的越多,交易的速度就越快。因此,如果你想在下一個區塊中向 Vitalik 支付 1 個 DAI,你可能需要設置一個更高的費用,而不是你願意等待幾分鐘(或更長的時間),直到 Gas 更便宜。
不同的錢包可能採取不同的方法來決定支付多少 Gas 費。我不知道有什麼單一的機制被所有人使用。確定正確費用的策略可能涉及從節點查詢與 Gas 有關的資訊(如網路接受的最低基本費用)。
例如,在下面的請求中,你可以看到 Metamask 瀏覽器插件在建立交易時向本地測試節點發送請求,以獲取 Gas 費數據:
而簡化後的請求-響應看起來像:
「eth_feeHistory」端點被一些節點暴露出來,允許查詢交易費用數據。如果你很好奇,可以閱讀這裡或這裡玩玩它,或者看看規範這裡。
流行的錢包也使用更複雜的鏈外服務來獲取 Gas 交易成本來估計,並向用戶建議合理的價值。這裡有一個例子,一個錢包請求了一個網路服務的公共端點,並收到了一堆有用的 Gas 相關數據:
看一下響應片段:
很酷,對嗎?
無論如何,希望你能熟悉設置 Gas 費用價格並不簡單,它是建立一個成功交易的基本步驟。即使你想做的只是發送 1 個 DAI。這裡是一個有趣的介紹性指南,可以深入挖掘其中的一些機制,在交易中設置更準確的費用。
在一些初步的背景下,現在讓我們回到實際的交易。有三個與 Gas 有關的字段需要設置:
錢包將使用一些提到的機制來為你填寫前兩個字段。有趣的是,每當錢包 UI 讓你在某個版本的「慢速」、「常規」或「快速」交易中進行選擇時,它實際上是在試圖決定什麼值最適合這些確切的參數。現在你可以更好地理解上面從錢包收到的 JSON 格式的響應內容了。
為了確定第三個字段的值,即 GasLimit,有一個方便的機制,錢包可以用來在真正提交交易之前模擬交易。這使他們能夠確切的估計一筆交易會消耗多少 Gas,從而設定一個合理的 GasLimit。
為什麼不直接設置一個巨大的 GasLimit?當然是為了保護你的資金。智能合約可能有任意的邏輯,你是為其執行付費的人。通過在交易開始時就選擇一個合理的 GasLimit,你可以保護自己,避免在 Gas 費用中耗盡你帳戶的所有 ETH 資金的尷尬情況。
可以通過節點的 「eth_estimateGas」 端點進行 Gas 估算。在發送 1 個 DAI 之前,錢包可以利用這一機制來模擬你的交易,並確定你的 DAI 轉賬的正確 GasLimit。來自錢包的請求-回應可能是這樣的:
在響應中,你可以看到,轉賬將需要大約 34706 個 Gas 單位。
讓我們把這些資訊納入交易的有效載荷中:
記住,「maxPriorityFeePerGas」和 「maxFeePerGas」最終將取決於發送交易時的網路條件。上面我只是為了這個例子而設置了一些任意的值。至於為 GasLimit 設置的值,我只是把估計值增加了一點,以提交執行交易的可能性。
訪問列表和交易類型
讓我們簡單評論一下在你的交易中設置的另外兩個字段。
首先,「accessList」字段。高級使用場景或邊緣場景可能需要交易提前指定要訪問的帳戶地址和合約的儲存槽,從而使交易的成本降低一些。
然而,提前建立這樣的列表可能並不直接,目前節省的 Gas 可能並不那麼顯著。特別是對於簡單的交易,如發送 1 個 DAI。因此,我們可以直接將其設置為一個空的列表。儘管記住它確實存在有原因,而且它在未來可能變得更有意義。
第二,交易類型。它在 「type」 字段中被指定。類型是交易內部內容的一個指標。我們的將是一個類型 2 的交易--因為它遵循這裡指定的格式。
簽署交易
節點如何知道是你的帳戶,而不是其他人的帳戶在發送交易?
我們已經來到了建立有效交易的關鍵步驟:簽名。
一旦錢包收集了足夠的資訊來建立交易,並且你點擊發送,它將對你的交易進行數字簽名。如何簽名?使用你的帳戶的私鑰 (你的錢包可以訪問),和一個涉及橢圓曲線的加密算法,稱為ECDSA。
對於好奇的人來說,實際上被簽署的是交易類型和 RLP 編碼內容之間串聯的「keccak256」哈希值。
雖然你不應該有那麼多的密碼學知識來理解這個。簡單地說,這個過程是對交易的密封。它通過在上面蓋上一個只有你的私鑰才能產生的聰明的印章,使其具有防篡改性。從現在開始,任何能夠訪問該簽名交易的人(例如,以太坊節點)都可以通過密碼學來驗證是你的帳戶產生了該交易。
明確一下:簽名不是加密。你的交易始終是明文的。一旦它們被公開,任何人都可以從它們的內容中獲得其含義。
簽署交易的過程中,毫不奇怪,會產生一個簽名。在實踐中是一堆奇怪的不可讀的值,你通常會發現它們被稱為「v」,「r」和「s」。
如果你想更深入地了解這些實際代表的內容,以及它們對還原你的帳戶地址的重要性,網路是你的朋友。
你可以通過查看RLP。交易的編碼方式如下:
0x02 || rlp([chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, value, data, accessList, v, r, s])
其中初始字節是交易類型。
在前面的代碼片段的基礎上,你可以實際看到序列化的交易這樣添加:
console.log(signedTx.serialize().toString("hex")); // 02f8b1827a69808477359400851bf08eb000829c40946b175474e89094c44da98b954eedeac495271d0f80b844a9059cbb000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa960450000000000000000000000000000000000000000000000000de0b6b3a7640000c001a0057d733933b12238a2aeb0069b67c6bc58ca8eb6827547274b3bcf4efdad620a9fe49937ec81db89ce70ebec5e51b839c0949234d8aad8f8b55a877bd78cc293
這就是在我的以太坊主網上的本地 Fork 中向 Vitalik 發送 1 個 DAI 的實際有效載荷。
一旦建立、簽署和序列化,該交易必須被發送到一個以太坊節點。
節點會提供方便的 JSON-RPC 端點,節點可以在那裡接收交易請求。
發送交易使用 eth_sendRawTransaction。下面是一個錢包在提交交易時使用的網路流量:
其中初始字節是交易類型。
在前面的代碼片段的基礎上,你可以實際看到序列化的交易這樣添加:
這就是在我的以太坊主網上的本地 Fork 中向 Vitalik 發送 1 個 DAI 的實際有效載荷。
提交交易
一旦建立、簽署和序列化,該交易必須被發送到一個以太坊節點。
節點會提供方便的 JSON-RPC 端點,節點可以在那裡接收交易請求。
發送交易使用「eth_sendRawTransaction」。下面是一個錢包在提交交易時使用的網路流量:
總結的請求-響應看起來像:
響應中包含的結果包含交易的哈希值:「bf77c4a9590389b0189494aeb2b2d68dc5926a5e20430fb5bc3c610b59db3fb5」. 這個 32 字節長的十六進制字符序列是所提交交易的唯一標識符。
節點接收
我們應該如何去弄清楚當以太坊節點收到序列化的簽名交易時會發生什麼?
有些人可能會在 Twitter 上詢問,有些人可能會閱讀一些 Medium 文章。其他的人甚至可能會閱讀文檔,或者看影音
只有一個地方可以找到真相:在源碼。讓我們用go-ethereum v1.10.18(又名 Geth),一個流行的以太坊節點的實現(一旦以太坊轉向 Proof-of-Stake,就是 "執行客戶端")。從現在開始,我將包括 Geth 的源代碼鏈接,以便你能跟隨。
在收到對其「eth_sendRawTransaction」端點的 JSON-RPC 調用後,該節點需要對請求正文中包含的序列化交易進行分析。所以它開始對交易進行反序列化。從現在開始,節點將能更容易地訪問交易的字段。
在這一點上,節點已經開始驗證交易了。首先,確保交易的費用(即價格 * GasLimit)不超過節點願意接受的最大限度(顯然,默認情況下,這是一個以太幣)。還有然後,確保交易是受重放保護的(按照EIP155--記得我們在交易中設置的「鏈 ID」字段嗎?),或者節點願意接受不受保護的交易。
接下來的步驟包括髮送交易到交易池(又稱 mempool)。簡單地說,這個池子代表了節點在某個特定時刻所知道的交易集合。就只有節點所知,這些還沒有被納入區塊鏈。
在真正將交易納入池中之前,節點檢查它是否已經知道它。而且它的 ECDSA 簽名是有效的。否則就拋棄該交易。
然後沉重的 mempool 開始。正如你可能注意到的,有很多瑣碎的邏輯來確保交易池是「快樂和健康」的。
這裡有相當多的重要驗證。例如,GasLimit 低於區塊 GasLimit,或者交易的大小不超過允許的最大,或者 nonce 是預期的,或者發送方有足夠的資金來支付潛在的成本(即價值 + GasLimit*價格),等等。
雖然我們可以繼續下去,但我們在這裡不是要成為 mempool 專家。即使我們想這樣做,我們也需要考慮,只要他們遵循網路共識規則,每個節點營運商可能採取不同的方法來管理 mempool。這意味著執行特殊的驗證或遵循自定義的交易優先級規則。為了只發送 1 個 DAI,我們可以將 mempool 視為一組急切等待被拾取並被納入區塊的交易。
在成功地將交易添加到池中(並做內部記錄的事情),節點返回交易哈希值。這正是我們之前在 JSON-RPC 請求-響應中看到的返回內容。
檢查 mempool
暢行幣圈交易全攻略,專家駐群實戰交流
▌立即加入鉅亨買幣實戰交流 LINE 社群(點此入群)
不管是新手發問,還是老手交流,只要你想參與虛擬貨幣現貨交易、合約跟單、合約網格、量化交易、理財產品的投資,都歡迎入群討論學習!
- 從零開始學合約系列講座熱烈報名中
- 掌握全球財經資訊點我下載APP
文章標籤
上一篇
下一篇