• <pre id="x08qo"></pre><object id="x08qo"></object><track id="x08qo"></track>

    1. <track id="x08qo"></track>
        <acronym id="x08qo"><strong id="x08qo"><address id="x08qo"></address></strong></acronym>
        收藏本站 網站導航 開放平臺 Tuesday, November 7, 2023 星期二
        • 微信

        深度解析:在發送1個DAI時發生了什么?

        來源 中金網 06-16 17:28
        摘要: 這不是「神奇」的互聯網貨幣。你可以看到每一塊代碼的移動,甚至能夠觸摸到它們。

          原文來源:NOTONLYOWNER原文作者:tincho原文編譯:登鏈社區翻譯小組

          你有 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」字段中?,F在看來是這樣的:

          一旦我們進入交易的實際執行階段,我們將重新審視這個「data」字段的內容。

          Gas

          下一步是決定為交易支付多少錢。因為請記住,所有交易都必須向花費時間和資源來執行和驗證它們的節點網絡支付費用。

          執行交易的費用是以 ETH 支付的。而 ETH 的最終數額將取決于你的交易消耗了多少凈 Gas(也就是計算成本有多高),你愿意為每個 Gas 單位的花費支付多少錢,以及網絡愿意接受的最低數額。

          從用戶的角度來看,通常是,支付的越多,交易的速度就越快。因此,如果你想在下一個區塊中向 Vitalik 支付 1 個 DAI,你可能需要設置一個更高的費用,而不是你愿意等待幾分鐘(或更長的時間),直到 Gas 更便宜。

          不同的錢包可能采取不同的方法來決定支付多少 Gas 費。我不知道有什么單一的機制被所有人使用。確定正確費用的策略可能涉及從節點查詢與 Gas 有關的信息(如網絡接受的最低基本費用)。

          例如,在下面的請求中,你可以看到 Metamask 瀏覽器插件在建立交易時向本地測試節點發送請求,以獲取 Gas 費數據:

          而簡化后的請求-響應看起來像:

          「eth_feeHistory」端點被一些節點暴露出來,允許查詢交易費用數據。如果你很好奇,可以閱讀這里或這里玩玩它,或者看看規范這里。

          流行的錢包也使用更復雜的鏈外服務來獲取 Gas 交易成本來估計,并向用戶建議合理的價值。這里有一個例子,一個錢包請求了一個網絡服務的公共端點,并收到了一堆有用的 Gas 相關數據:

          看一下響應片段:

          很酷,對嗎?

          無論如何,希望你能熟悉設置 Gas 費用價格并不簡單,它是建立一個成功交易的基本步驟。即使你想做的只是發送 1 個 DAI。這里是一個有趣的介紹性指南,可以深入挖掘其中的一些機制,在交易中設置更準確的費用。

          在一些初步的背景下,現在讓我們回到實際的交易。有三個與 Gas 有關的字段需要設置:

          錢包將使用一些提到的機制來為你填寫前兩個字段。有趣的是,每當錢包 UI 讓你在某個版本的「慢速」、「常規」或「快速」交易中進行選擇時,它實際上是在試圖決定什么值最適合這些確切的參數?,F在你可以更好地理解上面從錢包收到的 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」。

          如果你想更深入地了解這些實際代表的內容,以及它們對還原你的賬戶地址的重要性,互聯網是你的朋友。

          你可以通過查看@ethereumjs/tx軟件包來更好地了解簽名實現時的樣子。也可以使用ethers包中的一些實用工具。作為一個極其簡化的例子,簽署交易以發送 1 個 DAI 可以是這樣的:

          由此產生的對象將看起來像:

          序列化

          下一步是序列化簽名的交易。這意味著將上面的漂亮對象編碼成一個二進制字節序列,這樣它就可以被發送到以太坊網絡并被接收的節點消費。

          以太坊選擇的編碼方法被稱為 RLP。交易的編碼方式如下:

          其中初始字節是交易類型。

          在前面的代碼片段的基礎上,你可以實際看到序列化的交易這樣添加:

          這就是在我的以太坊主網上的本地 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

          如果你通過 Metamask 或任何默認連接到傳統節點的類似錢包發送交易,在某些時候,它將「降落」在公共節點的 mempools 上。你可以通過自己檢查 mempools 來確保這一點。

          有一個方便的端點,一些節點暴露了出來,叫做「eth_newPendingTransactionFilter」。它也許是frontrunning(搶跑) bots 的好朋友。定期查詢這個端點可以讓我們在交易被納入鏈中之前觀察到,現在 1 個 DAI 進入了本地測試節點的 mempool 中。

          在 Javascript 代碼中,這可以通過以下方式完成:

          要看到實際的「eth_newPendingTransactionFilter」調用,我們可以直接檢查網絡流量:

          從現在開始,腳本將(自動)輪詢 mempool 中的變化。這是隨后的許多周期性調用中的第一個,檢查變化:

          在收到交易后,節點最終用它的哈希值來響應:

          總結的請求 - 響應看起來像:

          早些時候我說過 “傳統節點”,但沒有解釋太多。我的意思是,有一些更專業的節點具有私人內存池的特點。它們允許用戶在交易被納入區塊之前,從公眾那里 “隱藏 ”交易。

          不管具體情況如何,這種機制通常包括在交易發起者和區塊構建者之間建立私人通道。Flashbots 保護服務就是一個明顯的例子。實際的結果是,即使你用上面的方法來監控 mempools,你也不能通過私人通道來獲取那些進入區塊構建者的交易。

          假設發送 1 個 DAI 的交易是通過普通通道提交給網絡的,沒有利用這種服務。

          傳播

          為了使交易被包含在區塊中,它需要以某種方式到達能夠建立和提出交易的節點。在工作量證明以太坊中,這些節點被稱為礦工。在Proof-of-Stake以太坊中,稱為驗證者。雖然現實往往更復雜一些。請注意,可能有一些方法可以將區塊構建外包給專業服務。

          作為一個普通用戶,你應該不需要知道這些區塊生產者是誰,也不需要知道他們在哪里。相反,你可以簡單地發送一個有效的交易到網絡中的任何常規節點,讓它包括在交易池中,并讓點對點協議做他們的事情。

          有一些這樣的 p2p 協議將以太坊節點相互連接。除其他事項外,它們允許頻繁地交換交易。

          從一開始,所有節點都與他們的對等節點(默認情況下,最多 50 個對等節點(Peers))一起監聽和廣播交易。

          一旦一個交易到達 mempool,它就會被發送給所有尚未知道該交易的連接對等節點。

          為了提高效率,只有一個隨機的連接節點子集(平方根)被發送完整交易。其余的是只發送交易哈希。如果需要的話,這些節點可以請求返回完整的交易。

          一個交易不能永遠停留在一個節點的 mempool 中。如果它沒有被其他原因首先被丟棄(例如,池子滿了,交易價格低了,或者它被更高的 nonce/價格的新交易取代)的話,它可能在一定時間后(默認為3 小時)被自動刪除。

          在 mempool 中被認為可以被區塊構建者拾取和處理的有效交易被跟蹤在一個待處理交易的列表中。這個數據結構可以被區塊構建者查詢,以獲得被允許進入鏈上的可處理交易。

          工作準備和交易納入

          交易應該在瀏覽了 mempools 之后到達一個挖礦節點(至少在寫這篇文章的時候)。這種類型的節點是特別重的多任務處理器。對于熟悉 Golang 的人來說,這意味著在挖礦相關的邏輯中,有相當多的 go 例程和通道。對于那些不熟悉 Golang 的人來說,這意味著礦工的常規操作不能像我想的那樣被線性解釋。

          本節的目標有兩個方面。首先,了解我們的交易是如何以及何時被礦工從 mempool 中提取的。第二,找出交易的執行在哪一點上開始。

          當節點的挖礦組件被初始化時,至少有兩件相關的事情發生。第一,它開始監聽新交易到達 mempool 的情況。第二,一些基本的循環被觸發了。

          在 Geth 的行話中,用交易建立一個區塊并將其密封的行為被稱為 “提交工作(committing work)”。因此,我們想了解這是在什么情況下發生的。

          重點是“新工作”loop。這是一個獨立的例程,當節點收到不同類型的通知時,會觸發工作提交。該觸發器需要發送一個工作要求到該節點的另一個活躍監聽器(運行在礦工的“main” loop中)。當收到這樣的工作要求時,提交工作開始。

          節點開始進行一些初始準備。主要包括建立區塊頭。這包括尋找父區塊,確保正在建立的區塊的時間戳是正確的,設置區塊編號,GasLimit,coinbase 地址和基本費用等任務。

          之后,共識引擎被調用,進行區塊頭的“共識準備”。這計算出正確的區塊難度(取決于當前的網絡版本)。如果你聽說過以太坊的 “難度炸彈”,你就知道了。

          譯者注: TheMerge 之后已經沒有難度炸彈了。

          接下來,區塊密封上下文被創建。撇開其他動作,這包括獲取最后的已知狀態。這是正在建立的區塊中的第一個交易將被執行的狀態。這可能是我們的交易發送 1 個 DAI。

          在準備好區塊后,它就開始填充交易。

          我們達到了這里:到目前為止,我們的未決(pending)交易只是舒適地坐在節點的內存池中,與其他交易一起被拾起。

          默認情況下,交易在一個區塊內按價格和 nonce 排序。對于我們的情況,交易在區塊中的位置實際上是無關的。

          現在開始按順序執行這些交易。一個交易被執行之后,每個交易都建立在前一個交易的結果狀態之上。

          執行

          一個以太坊交易可以被認為是一個狀態轉換。

          狀態 0:你有 100 個 DAI,Vitalik 也有 100 個。

          交易:你發送 1 個 DAI 給 Vitalik。

          狀態 1:你有 99 個 DAI,而 Vitalik 有 101 個。

          因此,執行交易需要對區塊鏈的當前狀態應用一系列的操作。產生一個新的(不同的)狀態作為結果。這將被認為是新的當前狀態,直到有另一個交易進來。

          在現實中,這更有趣(也更復雜)。讓我們來看看。

          準備工作(第一部分)

          用 Geth 的行話說,礦工在區塊中提交交易。提交交易的行為是在一個環境中進行的。這種環境包含一個特定的狀態(先不管其他的)。

          因此,簡而言之,提交一個交易本質上是:(1)記住當前的狀態,(2)通過應用交易來修改它,(3)根據交易的成功,要么接受新狀態,要么回滾到原來的狀態。

          有趣的事情發生在(2):應用交易。

          首先要注意的是,交易被變成了一個 消息「Message」。如果你熟悉 Solidity,在那里你通常會寫諸如「msg.data」或「msg.sender」這樣的東西,最后在 Geth 的代碼中讀到「message」就是歡迎你進入友好之地的標志。

          當檢查消息時,會很快就會注意到它與交易的至少一個區別。一條信息有一個「from」字段! 這個字段是簽名者的以太坊地址,它是由交易中的公共簽名衍生出來的(還記得奇怪的「v」、「r」和「s」字段嗎?)。

          現在,執行的環境被進一步準備。首先,與區塊相關的環境被創建,其中包括區塊編號、時間戳、coinbase 地址和區塊 GasLimit 等內容。然后...

          野獸走了進來,它就是以太坊虛擬機。

          以太坊虛擬機(EVM),負責執行交易的基于堆棧的256 位計算引擎,我們可以期待它做什么?

          EVM 是一臺機器。作為一臺機器,它有一套可以執行的指令(又稱操作碼)。該指令集多年來一直在變化。因此,一定會有段代碼告訴 EVM 今天應該使用哪些操作碼。當 EVM實例化解釋器時,它選擇正確的操作代碼集,取決于正在使用的版本。

          最后,在真正的執行之前有兩個最后步驟。EVM 的交易上下文被創建(在你的 Solidity 智能合約中使用過「tx.origin」或「tx.gasPrice」嗎?),EVM 被賦予訪問當前狀態的權限。

          準備工作 (第二部分)

          現在輪到 EVM 執行狀態轉換了。給定一個信息、一個環境和原始狀態,它將使用一組有限的指令來轉移到一個新的狀態。其中,維塔利克有 1 個額外的 DAI。

          在應用狀態轉換之前,EVM 必須確保它遵守特定的共識規則。讓我們看看這一點是如何做到的。

          驗證開始于 Geth 所說的“預檢查”,它包括:

          1. 驗證信息的 nonce。它必須與信息的 “from”地址的 nonce 匹配。此外,它必須不是可能的最大 nonce(通過檢查 nonce +1 是否會導致溢出)。

          2. 確保與信息的「from」地址相對應的賬戶沒有代碼。也就是說,交易起源是一個外部擁有的賬戶(EOA)。從而遵守EIP 3607規范。

          3. 驗證交易中設置的「maxFeePerGas」(Geth 中的「gasFeeCap」)和「maxPriorityFeePerGas」(Geth 中的「gasTipCap」)字段是在預期范圍內。此外,優先權費用不大于最大費用。并且 maxFeePerGas大于當前區塊的基本費用。

          4. 購買 Gas,檢查賬戶是否能夠支付它打算消費的所有 Gas。而且該區塊中還有足夠的 Gas來處理這筆交易。最后讓賬戶提前支付Gas 費用(別擔心,以后還有退款機制)。

          接下來,EVM核算交易消耗的 “內在 (intrinsic) Gas”。在計算內在 Gas 時,有幾個因素需要考慮。首先,交易是否是合約創建。我們這里不是,所以 Gas 初始為 21000 個單位](https://github.com/ethereum/go-ethereum/blob/v1.10.18/params/protocol_params.go#L32)。之后,信息的 “數據 ”字段中的非零字節的數量也被考慮在內。每個非零字節收取16 個單位(遵循本規范)。每個零字節只收取4 個單位的費用。最后,如果我們提供訪問列表,一些更多的 Gas 將被提前計算。

          我們將交易的「value」字段設置為零。如果我們指定一個正值,現在將是 EVM 檢查發送方賬戶是否真的有足夠的余額以執行 ETH 轉賬的時刻。此外,如果我們設置了訪問列表,現在它們將被初始化為狀態。

          正在執行的交易并不是在創建一個合約。EVM 知道它因為「to」字段不是零。因此,它將起者的賬戶 nonce增加1,并執行一個調用。

          調用將從 from 到 to 信息的地址,傳遞 data,沒有 value,以及消耗內在 Gas 后剩下的任何 Gas。

          調用

          DAI 智能合約存儲在地址「0x6b175474e89094c44da98b954eedeac495271d0f」。這就是我們在交易的「to」字段中設置的地址。這個初始調用是為了讓 EVM 執行存儲在它那里的任何代碼,逐個操作碼執行。

          操作碼是 EVM 的指令,用十六進制數字表示,范圍從 00 到 FF。盡管它們通常用它們的名字來指代。例如,「00」是「STOP」,「FF」是「SELFDESTRUCT」。一個方便的操作碼列表可以在evm.codes上找到。

          那么 DAI 的操作碼到底是什么?很高興你這么問:

          不要驚慌?,F在要想弄清這一切還為時尚早。

          讓我們慢慢開始,把初始調用分解。它的簡要文檔提供了一個很好的總結:

          首先,邏輯檢查是否已經觸及調用深度限制。這個限制是設置為 1024,這意味著在一個交易中最多只能有 1024 個嵌套調用。這里是一篇有趣的文章,可以閱讀關于 EVM 這種行為背后的一些推理和微妙之處。稍后我們將探討如何增加/減少調用深度。

          相關的附帶說明:調用深度限制 并不是 EVM 的堆棧大小限制 -- 堆棧大?。ㄇ珊??)也是 1024 個元素。

          下一步是確保,如果在調用中指定了一個正的 value,那么發送方有足夠的余額來執行轉移(執行幾步之后)。我們可以忽略這一點,因為我們調用的 value 是零。此外,一個當前狀態的快照被拍攝下來。這允許在失敗時輕松恢復任何狀態變化。

          我們知道 DAI 的地址指的是一個有代碼的賬戶。因此,它必須已經存在于以太坊的狀態空間里。

          然而,讓我們暫時想象一下,這不是一個發送 1 個 DAI 的交易。假設它是一個沒有價值的垃圾交易,目標是一個新的地址。相應的賬戶將需要被添加到狀態。然而,如果該賬戶缺只是空的呢?除了浪費節點的磁盤空間之外,似乎沒有理由對它進行跟蹤。EIP 158對以太坊協議進行了一些修改,以幫助避免這種情況的發生。這就是為什么你在調用任何賬戶時看到這個「if」條件。

          我們知道的另一件事是,DAI不是預編譯合約。什么是預編譯合約?下面是以太坊黃皮書提供的內容:

          簡而言之,在以太坊的狀態下,(到目前為止)有 9 個不同的特殊合約。這些賬戶(范圍從0x0000000000000000000000000001到0x0000000000000000000009)開箱即包含執行黃皮書中提到的操作的必要代碼。當然,你可以在 Geth 的代碼中自己檢查其實現。

          為了給預編譯合約的故事增添一些色彩,請注意,在以太坊主網中,所有這些賬戶的余額至少有 1wei。這是故意的(至少在用戶開始錯誤地發送以太幣之前)???,這里有一個近 5 年的交易向「0x0000000000000000000000000000000009」預編譯的賬戶發送了 1wei。

          不管怎樣。在意識到調用的目標地址并不對應于預編譯的合約后,節點從狀態中讀取賬戶的代碼。然后確保它是不空的。最后,命令 EVM使用它的解釋器,用給定的輸入(交易的「data」字段的內容)來運行該代碼。

          解釋器 (第一部分)

          現在是 EVM 實際執行 DAI 代碼的時候了。為了完成這個任務,EVM 手頭有幾個元素。它有一個堆棧,可以容納多達 1024 個元素(盡管只有前 16 個元素可以通過可用的操作碼直接訪問);它有一個易失性的讀/寫內存空間;它有一個程序計數器;它有一個特殊的只讀內存空間,叫做 calldata保存調用的輸入數據。還有一寫其他東西。

          像往常一樣,在進入多汁的東西之前有一些必要的設置和驗證。首先,調用深度遞增1。其次,如果有必要,只讀模式被設置。我們的調用不是只讀的(見這里傳遞的「false」參數)。否則一些 EVM 操作將不被允許。這包括改變狀態的 EVM 指令「SSTOR E」, 「CREAT E」, 「CREATE2」, 「SELFDESTRUCT」, 「CALL」 有正值,和「LOG」 。

          解釋器現在進入了執行循環。它包括按順序執行DAI 代碼中由程序計數器和當前 EVM 指令集所指示的操作碼。目前我們使用的是倫敦指令集--這是在解釋器第一次實例化時在跳轉表中配置的。

          循環還負責保持一個健康的堆棧(避免上下溢出)。并花費每個操作的固定 Gas 成本,以及適當時的動態 Gas 成本。這些動態成本包括,例如,EVM 內存的擴展(閱讀更多關于內存擴展成本的計算這里)。請注意,Gas 是在執行操作碼之前(--而不是之后)消耗的。

          每個可能指令的實際行為可以在這個 Geth 文件中找到實現。只要略微瀏覽一下,就可以開始看到這些指令是如何與堆棧、內存、Calldata 和狀態一起工作的。

          在這一點上,我們需要直接跳到 DAI 的操作碼中,并為我們的交易跟蹤它們的執行。然而,我不認為這是處理這個問題的最好方法。我寧愿先從 EVM 和 Geth 中走出來,然后進入 Solidity 領地。這應該給我們一個更有價值的關于 ERC20 轉移操作的高級行為的概述。

          Solidity 執行

          DAI 智能合約是用Solidity編碼的。它是一種面向對象的高級語言,當被編譯時,輸出 EVM 字節碼,能夠在 EVM 兼容的鏈上部署智能合約 (在我們的例子中是以太坊)。

          DAI 的源代碼可以找到在區塊瀏覽器中驗證,或在 GitHub。為了便于參考,我將會指向第一個。

          在我們開始之前,讓我們始終牢記,EVM 對 Solidity 一無所知。它對其變量、函數、合約的布局、ABI 編碼等一無所知。以太坊區塊鏈存儲的是普通的 EVM 字節碼,而不是花哨的 Solidity 代碼。

          你可能會問,為什么當你去任何區塊瀏覽器時,他們會在以太坊地址上向你顯示 Solidity 代碼。嗯,這只是一個幌子。在大多數區塊瀏覽器中,人們可以上傳 Solidity 源代碼,而瀏覽器則負責用特定的編譯器設置來編譯該源代碼。如果編譯器產生的輸出與區塊鏈上的指定地址存儲的內容相匹配,那么合約的源代碼就被稱為 “驗證”。從那時起,任何導航到該地址的人都會看到該地址的 Solidity 代碼,而不是只看到存儲在該地址的 EVM 字節碼。

          上述情況的一個非微不足道的后果是,在某種程度上,我們相信區塊瀏覽器會向我們展示合法的代碼(這不一定是真的,即使是意外)。不過這可能有替代方案--除非每次你想讀一個合約時,都要對照自己的節點來驗證源代碼。

          無論如何,現在回到 DAI 的 Solidity 代碼。

          在 DAI 的智能合約上(用 Solidity v0.5.12編譯),讓我們專注于函數的執行:「transfer」。

          當「transfer」運行時,它將調用另一個名為「transferFrom」的函數,然后返回后者返回的任何布爾標志?!竧ransfer」的第一個和第二個參數(這里稱為「dst」和「wad」)被直接傳遞給「transferFrom」。這個函數另外讀取發起者的地址(在「msg.sender」中作為一個Solidity 全局變量)。

          對于我們的例子,這些將是傳遞給「transferFrom」的值:

          讓我們看看「transferFrom」函數,然后:

          首先,發起者的余額被檢查與被轉移的金額相對照。

          這很簡單:你轉移的 DAI 不能多于你的余額。如果我沒有 1 個 DAI,執行將在這一點上停止,返回一個錯誤信息。請注意,每個地址的余額都在智能合約的存儲中被跟蹤。在一個名為「balanceOf」的 Map 的數據結構中。如果你至少有 1 個 DAI,我可以向你保證你的賬戶地址在那里的某個地方有記錄。

          第二,Token allowances 被驗證。

          這與我們現在沒有關系。因為我們沒有代表另一個賬戶執行轉賬。雖然要注意到這是所有 ERC20 Token 應該實現的機制--DAI 不是例外。實質上,你可以授權其他賬戶從你的賬戶轉賬 DAI Token。

          第三,實際進行余額互換。

          當發送 1 個 DAI 時,發送方的余額減少了 100000000000000,接收方的余額增加了 100000000000000。這些操作是在「balanceOf」數據結構上進行讀寫的。值得注意的是使用了兩個特殊的函數「add」和「sub」來進行計算。

          為什么不簡單地使用「+」和「-」運算符?

          記?。哼@個合約是用 Solidity 0.5.12 編譯的。在那個時候,編譯器并沒有像今天這樣包括上/下溢檢查。因此,開發者必須記?。ɑ虮惶嵝眩?,在適當的地方自己實現它們。因此在 DAI 合約中使用了「add」和「sub」。它們只是自定義的內部函數,用于執行加法和減法,并帶有約束檢查以避免算術問題。

          add 函數將 x 和 y 相加,如果運算結果小于 x,則停止執行(從而防止整數溢出)。

          sub 函數從 x 中減去 y,如果操作的結果大于 x,則停止執行(從而防止整數下溢)。

          第四,觸發一個轉移事件(正如ERC20 規范所建議的)。

          一個事件是一個記錄操作。在事件中發出的數據后可以從讀取區塊鏈的鏈外服務中獲取,但絕不會被其他合約獲取。

          在我們的轉賬操作中,發出的事件似乎記錄了三個元素。發起者的地址「0x6fC27A75d76d8563840691DDE7a947d7f3F179ba」,接收者的地址「0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045」,和發送金額「100000000000000」。

          前兩個對應于事件聲明中標記為「indexed」的參數。索引參數有利于數據的檢索,允許過濾任何相應的記錄值。除非事件被標記為「匿名」,否則事件的標識符也會作為一個主題被包含。

          因此,更具體地說,我們正在處理的「Transfer」事件實際上記錄了 3 個主題(事件的標識符、發起者的地址和接受者的地址)和 1 個值(轉移的 DAI 數量)。一旦我們涉及到低級別的 EVM 的東西,我們將涵蓋關于這個事件的更多細節。

          在函數的最后,布爾值 “true ”被返回(正如ERC20 規范所建議的)。

          這是一種信號,表明轉移被成功執行。這個布爾標志被傳遞給啟動調用的「transfer」函數(它也簡單地返回它)。

          這就是了! 如果你曾經發送過 DAI,這就是你所執行的邏輯。這就是你花錢讓一個全球去中心化的節點網絡為你做的工作。

          等一下。我可能偏得有點遠了。因為正如我之前告訴你的,EVM 對 Solidity 一無所知。節點不執行 Solidity。它們執行的是 EVM 的字節碼。

          是時候進行真正的交易了。

          EVM 執行

          在這一節中,將變得相當技術化。我假設你對 EVM 的字節碼比較熟悉。如果你不熟悉,我強烈建議你閱讀這個專欄或這個系列。在那里,你會發現本節中的很多概念都有單獨和更深入的解釋。

          DAI 的原始字節碼是很難閱讀的 -- 我們已經在上一節見證了它。研究它的一個更漂亮的方法是使用反匯編的版本。你可以在這里找到 Dai 的反匯編字節碼 (為了便于參考,我已經把它提取到這個 gist中)。

          空閑內存指針和調用的值

          如果你已經熟悉 Solidity 編譯器,前三條指令不應該感到驚訝。它只是在初始化空閑內存指針。

          Solidity 編譯器為內部的東西保留了從「0x00」到「0x80」的內存插槽。所以「空閑內存指針」是一個指向 EVM 內存中第一個可以自由使用的插槽的指針。它存儲在「0x40」,初始化時指向「0x80」。

          請記住,所有 EVM 操作碼在 Geth 中都有對應的實現。例如,你可以真正看到「MSTORE」的實現是如何彈出兩個堆棧元素并向 EVM 內存寫入一個 32 字節的字:

          在 DAI 的字節碼中,接下來的 EVM 指令確保調用不持有任何價值。如果它有,執行將在「REVERT」指令處停止。注意使用「CALLVALUE」指令(在此實現來讀取當前調用的 Value。

          我們的調用沒有持有任何值(交易的「value」字段被設置為零)--所以我們可以繼續。

          驗證 calldata(第一部分)

          接下來:由編譯器引入的另一個檢查。這一次,它要弄清楚 calldata 的大?。ㄍㄟ^「CALLDATASIZE」指令獲得--在這里實現是否低于 4 字節(見下面的「0x4」和「LT」指令)。在此案例中,它將跳到「0x142」位置。在「0x146」位置的 REVERT 指令上停止執行。

          這意味著在 DAI 智能合約中,calldata 的大小被強制要求為至少 4 字節。這是因為 Solidity 使用的 ABI 編碼機制用其簽名的 keccak256 哈希值的前四個字節來識別函數 (通常稱為 “函數選擇器” - 見規范 - Solidity 文檔)。

          如果 calldata 沒有至少 4 個字節,就不可能識別出該函數。所以編譯器引入了必要的 EVM 指令,以在此案例中提前失敗。這就是你在上面目睹的情況。

          為了調用「transfer(address,uint256)」函數,calldata 的前四個字節必須與函數的選擇器匹配。這四個字節是:

          與我們之前建立的交易的「data」字段的前 4 個字節完全相同:

          現在 calldata 的長度已經得到驗證,是時候使用它了。請看下面如何將前 4 個字節的 calldata 放在堆棧的頂部(這里需要注意的主要 EVM 指令是「CALLDATALOAD」,這里實現)。

          實際上「CALLDATALOAD」將32 字節的 calldata推到堆棧中。它需要用「SHR」指令來截斷,以保留前四個字節。

          函數調度器

          不要試圖逐行理解下面的內容。相反,注意突出的高級模式就好。我會添加一些分界線,使之更加清晰:

          一些被推送到堆棧的十六進制值有 4 個字節長,這并不是巧合。這些確實是函數選擇器。

          上面這組指令是 Solidity 編譯器產生的字節碼的一個常見結構。它通常被稱為 “函數調度器”。它類似于一個 if-else 或 switch 流程。它只是試圖將 calldata 的前四個字節與合約的函數的已知選擇器集合相匹配。一旦它找到一個匹配項,執行將跳到字節碼的另一個部分。在那里,該特定函數的指令被放置在這個部分。

          按照上述邏輯,EVM 將 calldata 的前四個字節與 ERC20「transfer」函數的選擇器相匹配:「0xa9059cbb」。并跳轉到字節碼位置「0x6b4」。這就是告訴 EVM 開始執行 DAI 的轉移。

          驗證 calldata(第二部分)

          在匹配了選擇器和跳轉后,現在 EVM 要開始運行與函數有關的具體代碼了。但在跳轉到其細節之前,它需要以某種方式記住位置,一旦所有與功能相關的邏輯被執行,在哪里繼續執行。

          做到這一點的方法是簡單地保持堆棧中適當的字節碼位置。請看下面正在推送的「0x700」值。它將在堆棧中徘徊,直到在某個時間點(稍后)被檢索到,并被用來跳回以結束執行。

          現在讓我們更具體地了解一下「transfer」函數。

          編譯器嵌入了一些邏輯,以確保 calldata 的大小對一個有兩個「address」和「uint256」類型的參數的函數是正確的。對于「transfer」函數,至少是 68 字節(4 字節用于選擇器+64 字節用于兩個 ABI 編碼的參數)。

          如果 calldata 的大小更小,執行將在位置「0x6c9」的「REVERT」處停止。由于我們的交易的 calldata 已經被正確的 ABI 編碼,因此有適當的長度,執行會跳到位置「0x6ca」。

          讀取參數

          下一步是讓 EVM 讀取 calldata 中提供的兩個參數。這些是 20 字節長的地址「0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045」和數字「100000000000000」(十六進制的「0x0de0b6b3a7640000」)。兩者都是以 32 個字節為一組的 ABI 編碼。因此,需要進行一些基本的操作來讀取正確的數值,并把它們放在堆棧的頂部。

          為了更加直觀,在依次應用上述指令集后(直到「0x6fb」),堆棧頂部看起來像這樣:

          這就是 EVM 如何迅速地從 calldata 中提取兩個參數,將它們放在堆棧中供將來使用。

          上面的最后兩條指令(字節碼位置「0x6fc」和「0x6ff」)只是讓執行跳到位置「0x1df4」。讓我們在那里繼續。

          「transfer」函數

          在簡單的 Solidity 分析中,我們看到「transfer(address,uint256)」函數是一個包裝器,調用更復雜的「transferFrom(address,address,uint256)」函數。編譯器將這種內部調用翻譯成這些 EVM 指令:

          首先注意推送值「0x1e01」的指令。這就是指示 EVM「記住」它應該跳回的確切位置,以便在即將到來的內部調用后繼續執行。

          然后,注意「CALLER」的使用(因為在 Solidity 中,內部調用使用「msg.sender」)。以及兩個「DUP5」指令。這些都是把「transferFrom」的三個必要參數放在堆棧的頂部:調用者的地址,接收者的地址,以及要轉移的金額。后兩個參數已經在堆棧的某處,因此使用了「DUP5」?,F在堆棧的頂部有所有必要的參數:

          最后,在指令「0x1dfd」和「0x1e00」之后,執行跳轉到位置「0xa25」。在那里 EVM 將開始執行與「transferFrom」函數對應的指令。

          「transferFrom」函數

          首先需要檢查的是發送方是否有足夠的 DAI 余額--否則將被回退。發送方的余額被保存在合約存儲器中。然后需要的基本 EVM 指令是「SLOAD」。然而,「SLOAD」需要知道什么存儲槽需要被讀取。對于映射(在 DAI 智能合約中保存賬戶余額的 Solidity 數據結構的類型),這不是那么直接的告訴。

          我不會在這里深入研究合約存儲中 Solidity 狀態變量的內部布局。你可以閱讀它這里是 v0.5.15。我只想說,給定映射「balanceOf」的鍵地址「k」,它相應的「uint256」值將被保存在存儲槽「keccak256(k . p)」,其中「p」是映射本身的槽位置,「.」是連接。你可以自己做數學題。

          參考狀態變量的存儲空間 。

          為了簡單起見,我們只強調幾個需要發生的操作。EVM 必須 i) 計算映射的存儲槽,ii) 讀取數值,iii) 將其與要轉賬的數量(已經在堆棧中的數值)進行比較。因此,我們應該看到像 “SHA3 ”這樣的指令用于散列,“SLOAD” 用于讀取存儲,“LT ”用于比較。

          如果發送方沒有足夠的 DAI,執行將在「0xa6f」處繼續,最后在「0xadb」處碰到「REVERT」。由于我沒有忘記在我的發送方賬戶余額中裝入 1 個 DAI,那么讓我們繼續到「0xadc」位置。

          下面一組指令對應于 EVM 驗證調用者是否與發起者的地址相符(記得合約中的「if (src != msg.sender ...) { ...}」合約中的代碼段)。

          既然不匹配,就在「0xdb2」位置繼續執行。

          下面這段代碼沒有讓你想起什么嗎?檢查一下正在使用的指令。同樣,不要單獨一行行理解。用你的直覺來發現高級模式和最相關的指令。

          感覺它類似于從存儲器中讀取映射,那是因為它就是這樣! 上面是 EVM 從「balanceOf」映射中讀取發送方的余額。

          然后執行跳轉到「0x1e77」的位置,這里是「sub」函數的主體。

          「sub」函數將兩個數字相減,在整數下溢時恢復到原狀。我這里沒有寫字節碼,盡管你可以在這里 找到他。算術運算的結果被保存在堆棧中。

          回到對應于「transferFrom」函數主體的指令,現在減法的結果將被寫入存儲空間-更新「balanceOf」映射。試著注意下面的計算,以獲得映射項的適當存儲槽,這通過「SSTORE」指令的執行。這條指令是有效地將數據寫入狀態的指令--也就是更新合約的存儲。

          一組相當類似的操作碼被運行以更新接收者的賬戶余額。首先是從存儲中的「balanceOf」映射中讀取。然后使用「add」函數將余額加到正在轉移的金額上。最后,結果被寫到適當的存儲槽。

          事件記錄(Log)

          在合約的代碼中,「Transfer」事件是在更新余額之后發出的。因此,在分析的字節碼中必須有一組指令來處理這種帶有適當數據的事件。

          然而,事件是另一個屬于 Solidity 的幻想世界的東西。在 EVM 世界中,事件對應于記錄操作。

          記錄是通過可用的「LOG」指令集進行的。有幾個變體,取決于有多少個主題要被記錄。在 DAI 的案例中,我們已經注意到,發出的「Transfer」事件有 3 個主題。

          那么找到一組運行「LOG3」指令的指令就不奇怪了。

          在這些指令中,至少有一個值是突出的:「0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef」. 這是該事件的主要標識符。也叫主題 0。它是編譯器在編譯時計算的一個靜態值(嵌入在合約的運行時字節碼中)。如前所述,事件簽名的哈希值:

          就在到達「LOG3」指令之前,堆??雌饋硐襁@樣:

          那么轉移的金額在哪里?在內存中!

          在到達「LOG3」之前,EVM 首先被指示將金額存儲在內存中。這樣它就可以在以后被記錄指令消耗掉。如果你看一下「0xf21」位置,你會看到「MSTORE」指令負責這樣做。

          所以一旦到達「LOG3」,EVM 就可以安全的從內存中抓取實際記錄的數值,從偏移量「0x80」開始,讀取「0x20」字節(上面的前兩個堆棧元素)。

          另一種理解日志的方法是看它的在 Geth 中的實現。在那里你會發現一個負責處理所有日志指令的單一函數。你可以看到 i) 一個空的主題數組被初始化,ii) 內存偏移量和數據大小從堆棧中讀取,iii) 主題從堆棧中讀取并插入數組中,iv) 值從內存中讀取,v) 包含它被發出的地址、主題和值的日志被附加。

          這些日志后來是如何還原的,我們很快就會發現。

          返回值

          「transferFrom」函數的最后一件事是返回布爾值「true」。這就是為什么在「LOG3」之后的第一條指令只是將「0x1」的值推到堆棧中。

          接下來的指令準備讓堆棧退出「transferFrom」函數,回到它的包裝「transfer」函數。請記住,這個下一步跳轉的位置已經存儲在堆棧中了--這就是為什么你在下面的操作碼中沒有看到它。

          回到「transfer」函數中,要做的就是為最后的跳轉準備堆棧。到一個執行將被結束的位置。這個即將跳轉的位置之前也已經存儲在堆棧中了(還記得被推送的「0x700」值嗎?)

          剩下的就是為最后一條指令準備堆棧:「RETURN」。這條指令負責從內存中讀取一些數據,并將其傳回給原始調用者。

          對于 DAI 轉賬,返回的數據將簡單地包括由「transfer」函數返回的「true」布爾標志。記住,這個值已經放在堆棧里了。

          EVM 開始抓取第一個可用的空閑內存位置。這是通過讀取空閑內存的指針來完成的:

          接下來,必須用「MSTORE」將該值存儲在內存中。雖然不是那么直接地告訴你,下面的指令只是編譯器認為最適合為「MSTORE」操作準備堆棧的指令。

          「RETURN」指令從內存中復制返回的數據。所以它需要被告知要讀取多少內存,以及從哪里開始。下面的指令簡單的告訴 EVM 從內存中讀取并返回「0x20」字節,從空閑內存指針開始。

          返回值 “0x0000000000000000000000000000000000000000000000000001”(對應于布爾值 “true”)。

          執行停止。

          解釋器 (第二部分)

          字節碼的執行已經結束。解釋器必須停止迭代。在 Geth 中,它是這樣做的:

          這意味著「RETURN」操作碼的執行應該以某種方式返回一個錯誤。即使是像我們這樣成功的執行。事實上,它確實。盡管它作為一個標志--當它與成功執行「RETURN」操作碼所返回的標志相匹配時,錯誤實際上被刪除。

          Gas 退款和付款

          隨著解釋器運行的結束,我們回到了最初觸發它的調用中。該運行被成功完成。因此,返回的數據和任何剩余的 Gas 被簡單地返回。

          調用也完成了。執行是在包裹狀態轉換之后進行的。

          首先提供 Gas 退款。它被添加到交易中任何剩余的 Gas 中。退款金額的上限是所使用 Gas 的 1/5(由于EIP 3529)。所有現在可用的 Gas(剩余的加上退還的)被以 ETH 形式支付到發起者的賬戶,按照發起者在交易中最初設定的費用價格。所有剩余的 Gas 被重新添加到區塊中的可用 Gas 中,以便后續交易可以消耗這些 Gas。

          然后向 coinbase 地址(PoW 中的礦工地址,PoS 中的驗證者地址)支付最初承諾小費。有趣的是,對執行過程中使用的所有 Gas 都進行支付。即使其中一些后來被退還了。此外,請注意這里*有效的小費是如何計算的。不僅注意到它被 “maxPriorityFeePerGas” 交易字段所限制。但更重要的是,意識到它不包括基本費用(base-fee)! 這沒有錯 -- 以太坊喜歡看著 ETH 燃燒。

          最后執行結果被包裹在一個更漂亮的結構中。包括使用的 Gas,任何可能中止執行的 EVM 錯誤(在我們的例子中沒有),以及從 EVM 返回的數據。

          建立交易收據

          代表執行結果的結構現在被傳遞 向上 返回 。在這一點上,Geth 對執行狀態做了一些內部清理。一旦完成,它就會累積交易中使用的 Gas(包括退款)。

          最重要的是,現在是創建交易收據的時候了。收據是一個總結與交易執行有關的數據的對象。它包括的信息有:執行狀態(成功/失?。?,交易的哈希值,使用的 Gas 單位,創建合約的地址(在我們的例子中沒有),發出的日志,交易的 bloom 過濾器,和其他。

          我們很快就會檢索到我們交易的收據的全部內容。

          如果你想深入了解交易的日志和 bloom filter 的作用,請查看noxx 的文章。

          挖掘區塊

          后續交易的執行繼續發生,直到區塊的空間耗盡。

          這時節點會調用共識引擎來最終完成該區塊。在 PoW 中,這需要積累挖礦獎勵(向 coinbase 地址發放 ETH 的全額獎勵,以及其他區塊的部分獎勵)并相應地更新區塊的最終狀態根。

          接下來,實際的區塊被組裝,將所有數據放在正確的位置。包括頭的交易哈?;蚴論5刃畔?。

          現在為真正的 PoW 挖礦做好了所有準備。一個新的 “任務”被創建并推送給正確的監聽者。委托給共識引擎的挖掘任務開始。

          我不會詳細解釋 PoW 的實際開采是如何進行的?;ヂ摼W上已經有很多關于它的內容。只需注意,在 Geth 中,這涉及一個多線程的嘗試和錯誤過程,以找到一個數字,滿足一個必要條件。不用說,一旦以太坊切換到權益證明,挖掘過程的處理方式將有很大不同。

          挖出的區塊被推送到適當的 channel和在結果循環中接收。其中收據和日志會相應地更新,并在其被有效挖出后提供最新的區塊數據。

          區塊最后被寫入鏈中,置于鏈的頂端。

          廣播區塊

          下一步是向整個網絡宣布,一個新的區塊已經被開采出來。同時,該區塊在內部存儲到待定區塊集合。耐心地等待其他節點的確認。

          公告已經完成發布一個特定的事件,被挖掘的廣播循環 (loop)接收。在那里,該區塊被完全傳播到一個子網的對等節點,并以較輕的方式提供給其他人。

          更具體地說,廣播需要向連接的對等節點的平方根發送塊數據。在內部實現是將數據推送到塊通道(channel)隊列,直到其通過p2p 層發送。p2p 消息被識別為 NewBlockMsg。其余節點的收到一個包括區塊哈希的輕量級通告。

          請注意,這只對 PoW 有效。在權益證明中,區塊傳播將發生在共識引擎上。

          驗證區塊

          對等節點不斷監聽消息。每種類型的可能的消息都有一個相關的處理程序,一旦收到相應的消息,就立即調用。

          因此,在得到帶有區塊數據的 “NewBlockMsg ”消息時,其相應的處理程序被執行。處理程序對消息進行解碼并對傳播的塊運行一些早期驗證。這些驗證包括對報頭數據的初步理智檢查,主要是確保它們被填充和約束。以及對區塊的uncle和交易哈希值進行驗證。

          然后發送消息的對等節點被標記為擁有該區塊。從而避免了以后將區塊傳播回給它。

          最后,數據包被向下傳遞到第二個處理程序,在那里區塊將被入隊導入到鏈的本地副本。入隊是通過向相應的通道(channel)直接發送導入請求完成的。當請求被拾取時,它就會觸發實際的入隊操作。最后推送區塊數據到隊列中。

          該區塊現在在本地隊列中,準備被處理。這個隊列在節點的區塊提取器主循環中被定期讀取。當區塊到達前面時,節點將拾取它并嘗試導入。

          在實際插入候選塊之前,至少有兩個值得強調的驗證。

          首先,本地鏈必須已經包括被傳播塊的父節點。

          第二,區塊的頭必須是有效的。這些驗證是真正的驗證。意思是說,那些對共識真正重要的,并且在以太坊的黃皮書中被指定。因此,它們是由共識引擎處理。

          舉例來說,引擎會檢查區塊的工作證明是否有效,或者區塊的時間戳是否不在過去或不在未來太遠,或者區塊高度是否已經正確增加,等等。

          在驗證了它符合共識規則之后,整個區塊被進一步傳播到一個對等節點的子集。然

        免責聲明:中金網發布此信息目的在于傳播更多信息,與本網站立場無關。中金網不保證該信息的準確性、真實性、完整性、有效性等。相關信息并未經過本網站證實,不構成任何投資建議,據此操作,風險自擔。
        亚洲Av日韩AV欧美Av中文Av_免费看日本黄色大片_正常体位视频裸体视频_中国Av特黄精品
      1. <pre id="x08qo"></pre><object id="x08qo"></object><track id="x08qo"></track>

        1. <track id="x08qo"></track>
            <acronym id="x08qo"><strong id="x08qo"><address id="x08qo"></address></strong></acronym>