web3.eth.getTransactionCount("0x9e713963a92c02317a681b9bb3065a8249de124f") 40
交易 (Transactions)
交易 (transaction) 是由外部擁有帳戶 (EOA) 發起的簽名訊息,由以太坊網路發送,並紀錄以太坊區塊鏈上。在這個基本定義背後,有很多令人驚訝和著迷的細節。看待交易的另一種方式是,它是唯一能觸發狀態改或是或促使合約在 EVM 中執行的東西。以太坊是一個全域單例 (global singleton) 的狀態機(state machine),交易是唯一能夠轉動這狀態機去改變狀態的方法。合約無法自行運行。以太坊不會在後臺運行。所有的狀態改變與執行皆始於交易。
在本節中,我們將剖析交易,展示它們的工作方式,並瞭解詳細資訊。請注意,本章的大部分內容針對的是那些有興趣於使用低階的方式管理自己的交易的人,也許是因為他們正在撰寫錢包的應用程序; 您可能會發現有趣的細節,但如果您對使用現有的錢包應用程序感到滿意就不必擔心這一點!
交易的結構
首先讓我們來看看交易的基本結構,因為它是在以太坊網路上進行序列化和傳輸的。接收序列化交易的每個客戶端和應用程式將使用其自己的內部資料結構將其儲存在記憶體中,還會使用網路序列化交易本身中不存在的元數據進行修飾。交易的網路序列化是交易結構的唯一通用標準。
交易是一個序列化的二進制消息,其中包含以下數據:
- nonce
-
由始發EOA(外部擁有帳戶)發出的序列號,用於防止消息重播。
- gas price
-
發起人願意支付的gas價格(以wei為單位)。
- start gas
-
發起人願意支付的最大gas量。
- to
-
目標以太坊地址。
- value
-
發送到目標地址的ether數量。
- data
-
變長二進制數據。
- v,r,s
-
始發EOA的ECDSA簽名的三個組成部分。
交易消息的結構使用遞迴長度前綴(RLP)編碼方案(參見 [rlp] )進行序列化,該方案是專門為以太坊中準確和字節完美的數據序列化而創建的。以太坊中的所有數字都被編碼為大端序整數,其長度為8位的倍數。
請注意,欄位的標籤(“to”,“start gas”等)在這裡是為清楚起見而顯示,但不是包含欄位值的RLP編碼交易序列化數據的一部分。通常,RLP不包含任何欄位分隔符或標籤。RLP的長度前綴用於標識每個欄位的長度。因此,超出定義長度的任何內容都屬於結構中的下一個欄位。
雖然這是實際傳輸的交易結構,但大多數內部表示和用戶界面可視化都使用來自交易或區塊鏈的附加資訊來修飾它。
例如,你可能會注意到沒有表示發起人EOA的地址的“from”數據。EOA的公鑰可以很容易地從ECDSA簽名的v,r,s組成部分中派生出來。EOA的地址又可以很容易地從公鑰中派生出來。當你看到顯示“from”欄位的交易時,是該交易所用的軟體添加了該欄位。客戶端軟體經常添加到交易中的其他元數據包括塊編號(被挖掘之後生成)和交易ID(計算出的雜湊)。同樣,這些數據來源於交易,但不是交易資訊本身的一部分。
交易的隨機數(nonce)
nonce是交易中最重要和最少被理解的組成部分之一。黃皮書中的定義(見 [yellow_paper] )寫道:
nonce:與此地址發送的交易數量相等的標量值,或者,對於具有關聯程式碼的帳戶,表示此帳戶創建的合約數量。
嚴格地說,nonce是始發地址的一個屬性(它只在發送地址的上下文中有意義)。但是,該nonce並未作為賬戶狀態的一部分顯式儲存在區塊鏈中。相反,它是根據來源於此地址的已確認交易的數量動態計算的。
nonce值也用於防止帳戶餘額的錯誤計算。例如,假設一個賬戶有10個以太的餘額,並且簽署了兩個交易,都花費6個ether,分別具有nonce 1和nonce 2。這兩筆交易中哪一筆有效?在以太坊這樣的分佈式系統中,節點可能無序地接收交易。nonce強制任何地址的交易按順序處理,不管間隔時間如何,無論節點接收到的順序如何。這樣,所有節點都會計算相同的餘額。支付6以太幣的交易將被成功處理,賬戶餘額減少到4 ether。無論什麼時候收到,所有節點都認為與帶有nonce 2的交易無效。如果一個節點先收到nonce 2的交易,會持有它,但在收到並處理完nonce 1的交易之前不會驗證它。
使用nonce確保所有節點計算相同的餘額,並正確地對交易進行排序,相當於比特幣中用於防止“雙重支付”的機制。但是,因為以太坊跟蹤賬戶餘額並且不會單獨跟蹤獨立的幣(在比特幣中稱為UTXO),所以只有在賬戶餘額計算錯誤時才會發生“雙重支付”。nonce機制可以防止這種情況發生。
跟蹤nonce
實際上,nonce是源自帳戶的 已確認(已開採)交易數量的最新計數。要找到nonce是什麼,你可以詢問區塊鏈,例如通過web3界面:
Tip
|
該nonce是一個基於零的計數器,意味著第一個交易的nonce是0.在 Retrieving the transaction count of our example address中,我們有一個交易的計數為40,這意味著從0到39nonce已經被看到。下一個交易的nonce將是40。 |
你的錢包將跟蹤其管理的每個地址的nonce。這很簡單,只要你只是從單一點發起交易即可。假設你正在編寫自己的錢包軟體或其他一些發起交易的應用程式。你如何跟蹤nonce?
當你創建新的交易時,你將分配序列中的下一個nonce。但在確認之前,它不會計入 getTransactionCount 的總數。
不幸的是,如果我們連續發送一些交易,getTransactionCount 函數會遇到一些問題。有一個已知的錯誤,其中 getTransactionCount 不能正確計數待處理(pending)交易。我們來看一個例子:
web3.eth.getTransactionCount("0x9e713963a92c02317a681b9bb3065a8249de124f", "pending") 40 web3.eth.sendTransaction({from: web3.eth.accounts[0], to: "0xB0920c523d582040f2BCB1bD7FB1c7C1ECEbdB34", value: web3.toWei(0.01, "ether")}); web3.eth.getTransactionCount("0x9e713963a92c02317a681b9bb3065a8249de124f", "pending") 41 web3.eth.sendTransaction({from: web3.eth.accounts[0], to: "0xB0920c523d582040f2BCB1bD7FB1c7C1ECEbdB34", value: web3.toWei(0.01, "ether")}); web3.eth.getTransactionCount("0x9e713963a92c02317a681b9bb3065a8249de124f", "pending") 41 web3.eth.sendTransaction({from: web3.eth.accounts[0], to: "0xB0920c523d582040f2BCB1bD7FB1c7C1ECEbdB34", value: web3.toWei(0.01, "ether")}); web3.eth.getTransactionCount("0x9e713963a92c02317a681b9bb3065a8249de124f", "pending") 41
如你所見,我們發送的第一筆交易將交易計數增加到了41,顯示了待處理交易。但是當我們連續發送3個更多的交易時,getTransactionCount 調用並沒有正確計數。它只計算一個,即使在mempool中有3個待處理交易。如果我們等待幾秒鐘,一旦塊被挖掘,getTransactionCount 調用將返回正確的數字。但在此期間,雖然有多項交易待處理,但對我們無幫助。
當你構建生成交易的應用程式時,無法依賴 getTransactionCount 處理未完成的交易。只有在待處理和已確認相同(所有未完成的交易都已確認)時,才能信任 getTransactionCount 的輸出以開始你的nonce計數器。此後,請跟蹤你的應用中的nonce,直到每筆交易被確認。
Parity的JSON RPC接口提供 parity_nextNonce 函數,該函數返回應在交易中使用的下一個nonce。parity_nextNonce 函數可以正確地計算nonce,即使你連續快速構建多個交易,但沒有確認它們。
Parity 有一個用於訪問JSON RPC接口的Web控制台,但在這裡我們使用命令行HTTP客戶端來訪問它:
curl --data '{"method":"parity_nextNonce","params":["0x9e713963a92c02317a681b9bb3065a8249de124f"],"id":1,"jsonrpc":"2.0"}' -H "Content-Type: application/json" -X POST localhost:8545 {"jsonrpc":"2.0","result":"0x32","id":1}
nonce的間隔,重複的nonce和確認
如果你正在以編程方式創建交易,跟蹤nonce是十分重要的,特別是如果你同時從多個獨立進程執行此操作。
以太坊網路根據nonce順序處理交易。這意味著如果你使用nonce 0傳輸一個交易,然後傳輸一個具有nonce 2的交易,則第二個交易將不會被挖掘。它將儲存在mempool中,以太坊網路等待丟失的nonce出現。所有節點都會假設缺少的nonce只是延遲了,具有nonce 2的交易被無序地接收到。
如果你隨後發送一個丟失的nonce 1的交易,則交易(交易1和2)將被開採。一旦你填補了空白,網路可以挖掘它在mempool中的失序交易。
這意味著如果你按順序創建多個交易,並且其中一個交易未被挖掘,則所有後續交易將“卡住”,等待丟失的事件。交易可以在nonce序列中產生無意的“間隙”,比如因為它無效或gas不足。為了讓事情繼續進行,你必須傳輸一個具有丟失的nonce的有效交易。
另一方面,如果你不小心重複一個nonce,例如傳輸具有相同nonce的兩個交易,但收件人或值不同,則其中一個將被確認,另一個將被拒絕。哪一個被確認將取決於它們到達第一個接收它們的驗證節點的順序。
正如你所看到的,跟蹤nonce是必要的,如果你的應用程式沒有正確地管理這個過程,你會遇到問題。不幸的是,如果你試圖併發地做到這一點,事情會變得更加困難,我們將在下一節中看到。
併發,交易的發起和隨機數
併發是計算機科學的一個複雜方面,有時候它會突然出現,特別是在像Ethereum這樣的去中心化/分佈式實時系統中。
簡單來說,併發是指多個獨立系統同時進行計算。這些可以在相同的程式(例如線程)中,在相同的CPU(例如多進程)上,或在不同的計算機(即分佈式系統)上。按照定義,以太坊是一個允許操作(節點,客戶端,DApps)併發的系統,但是強制實施一個單一的狀態(例如,對於每個開採的區塊只有一個公共/共享狀態的系統)。
現在,假設我們有多個獨立的錢包應用程式正在從同一個地址或同一組地址生成交易。這種情況的一個例子是從熱錢包進行提款的交易所。理想情況下,你希望有多臺計算機處理提款,以便它不會成為瓶頸或單點故障。然而,這很快就會成為問題,因為有多臺計算機生產提款會導致一些棘手的併發問題,其中最重要的是選擇nonce。多臺電腦如何從同一個熱錢包賬戶協調生成,簽署和廣播交易?
你可以使用一臺計算機根據先到先得的原則為簽署交易的計算機分配nonce。但是,這臺電腦現在是可能故障的單點。更糟糕的是,如果分配了多個nonce,並且其中一個從沒有被使用(因為計算機處理具有該nonce的交易失敗),所有後續交易都會卡住。
你可以生成交易,但不為它們簽名或為其分配臨時值。然後將它們排隊到一個簽名它們的節點,並跟蹤隨機數。再次,你有了一個可能故障的單點。nonce的簽名和跟蹤是你的操作的一部分,可能在負載下變得擁塞,而未簽名交易的生成是你並不需要實現並行化的部分。你有併發性,但不是在過程中任何有用的部分。
最後,除了跟蹤獨立進程中的賬戶餘額和交易確認的難度之外,這些併發問題迫使大多數實現朝著避免併發和創建瓶頸進行,諸如單個進程處理交易所中的所有取款交易。
交易gas
我們在 [gas] 中詳細討論gas。但是,讓我們介紹有關交易的 gasPrice 和 startGas 欄位的一些基本知識。
gas是以太坊的燃料。gas不是ether,它是獨立的虛擬貨幣,有相對於ether的匯率。以太坊使用gas來控制交易可以花費的資源量,因為它將在全球數千臺計算機上處理。開放式(圖靈完備的)計算模型需要某種形式的計量,以避免拒絕服務攻擊或無意中的資源吞噬交易。
gas與ether分離,以保護系統免受隨著ether價值快速變化而產生的波動。
交易中的 gasPrice 欄位允許交易創建者設置每個單位的gas的匯率。gas價格以每單位gas多少 wei 測量。例如,在我們最近一個例子創建的交易中,我們的錢包已將 gasPrice 設置為 3 Gwei(3千兆,30億wei)。
網站 ethgasstation.info 提供有關以太坊主網路當前gas價格以及其他相關gas指標的資訊:
錢包可以在他們發起的交易中調整 gasPrice,以更快地確認(挖掘)交易。gasPrice 越高,交易可能被驗證的速度越快。相反,較低優先級的交易可能會降低他們願意為gas支付的價格,導致確認速度減慢。可以設置的最低gasPrice 為零,這意味著免費的交易。在區塊空間需求低的時期,這些交易將被開採。
Tip
|
最低可接受的gasPrice為零。這意味著錢包可以產生完全免費的交易。根據能力的不同,這些可能永遠不會被開採,但協議中沒有任何禁止免費交易內容。你可以在以太坊區塊鏈中找到幾個此類交易成功開採的例子。 |
web3界面通過計算幾個區塊的中間價格來提供gasPrice建議:
truffle(mainnet)> web3.eth.getGasPrice(console.log) truffle(mainnet)> null BigNumber { s: 1, e: 10, c: [ 10000000000 ] }
與gas有關的第二個重要領域是 startGas。這在 [gas] 中有更詳細的解釋。簡單地說,startGas 定義交易發起人願意花費多少單位完成交易。對於簡單付款,意味著將ether從一個EOA轉移到另一個EOA的交易,所需的gas量固定為21,000個gas單位。要計算需要花費多少ether,你需要將你願意支付的 gasPrice 乘以21,000:
truffle(mainnet)> web3.eth.getGasPrice(function(err, res) {console.log(res*21000)} ) truffle(mainnet)> 210000000000000
如果你的交易的目的地址是合約,則可以估計所需的gas量,但無法準確確定。這是因為合約可以評估不同的條件,導致不同的執行路徑和不同的gas成本。這意味著合約可能只執行簡單的計算或更復雜的計算,具體取決於你無法控制且無法預測的條件。為了說明這一點,我們使用一個頗為人為的例子:每次調用合約時,它會增加一個計數器,並在第100次(僅)計算一些複雜的事情。如果你調用99次合約,會發生一件事情,但在第100次調用時,會發生完全不同的事情。你要支付的gas數量取決於交易開採前有多少其他交易調用了該功能。也許你的估計是基於第99次交易,並且在你的交易被開採之前,其他人已經調用了99次合約。現在,你是第100個要調用的交易,計算工作量(和gas成本)要高得多。
借用以太坊使用的常見類比,你可以將startGas視為汽車中的油箱(你的汽車是交易)。你認為它需要旅行(驗證交易所需的計算),就用盡可能多的gas填滿油箱。你可以在某種程度上估算金額,但你的旅程可能會有意想不到的變化,例如分流(更復雜的執行路徑),這會增加燃油消耗。
然而,與燃料箱的比較有些誤導。這更像是一家加油站公司的信用賬戶,根據你實際使用的gas量,在旅行完成後支付。當你傳輸你的交易時,首先驗證步驟之一是檢查它源自的帳戶是否有足夠的金額支付 gasPrice * startGas 費用。但是,在交易執行結束之前,金額實際上並未從你的帳戶中扣除。只收取你最終交易實際消耗的天然氣,但在發送交易之前,你必須有足夠的餘額用於你願意支付的最高金額。
交易的接收者
交易的收件人在to欄位中指定。這包含一個20字節的以太坊地址。地址可以是EOA或合約地址。
以太坊沒有進一步驗證這個欄位。任何20字節的值都被認為是有效的。如果20字節的值對應於沒有相應私鑰的地址,或沒有相應的合約,則該交易仍然有效。以太坊無法知道某個地址是否是從公鑰(從私鑰匯出的)正確匯出的。
Warning
|
以太坊不能也不會驗證交易中的接收者地址。你可以發送到沒有相應私鑰或合約的地址,從而“燃燒”ether,使其永遠不會被花費。驗證應該在用戶界面層級完成。 |
交易的價值和數據
交易的主要“負載”包含在兩個欄位中:value 和 data。交易可以同時具有value和data,只有value,只有data,或沒有value和data。所有四種組合都是有效的。
只有value的交易是 支付 payment。只有data的交易是 調用 invocation。既沒有value也沒有data的交易,這可能只是浪費gas!但它仍然有可能。
讓我們嘗試所有上述組合:
首先,我們從我們的錢包中設置源地址和目標地址,以使演示更易於閱讀:
src = web3.eth.accounts[0];
dst = web3.eth.accounts[1];
有value的交易(支付),沒有data
web3.eth.sendTransaction({from: src, to: dst, value: web3.toWei(0.01, "ether"), data: ""});
我們的錢包顯示確認螢幕,指示要發送的value,並且沒有data:
有value(支付)data的交易
web3.eth.sendTransaction({from: src, to: dst, value: web3.toWei(0.01, "ether"), data: "0x1234"});
我們的錢包顯示一個確認螢幕,指示要發送的value和data:
0 value 的交易,只有數據
web3.eth.sendTransaction({from: src, to: dst, value: 0, data: "0x1234"});
我們的錢包顯示一個確認螢幕,指示value為0並顯示data:
既沒有value(支付)也沒有data的交易
web3.eth.sendTransaction({from: src, to: dst, value: 0, data: ""}));
我們的錢包顯示確認螢幕,指示0 value並且沒有data:
將value傳遞給EOA和合約
當你構建包含 value 的以太坊交易時,它等同於payment。根據目的地址是否為合約,這些交易行為會有所不同。
對於EOA地址,或者更確切地說,對於未在區塊鏈中註冊為合約的任何地址,以太坊將記錄狀態更改,並將你發送的value添加到地址的餘額中。如果地址之前沒有被查看過,則會創建地址並將其餘額初始化為你的付款value。
如果目標地址(to)是合約,則EVM將執行合約並嘗試調用你的交易的 data 中指定的函數(參見 [invocation] )。如果你的交易中沒有 data,那麼EVM將調用目標合約的 fallback 函數,如果該函數是payable,則將執行該函數以確定下一步該做什麼。
合約可以通過在調用付款功能時立即拋出異常或由付款功能中編碼的條件確定來拒絕收款。如果付款功能成功終止(沒有意外),則更新合約狀態以反映合約的ether餘額增加。
將數據傳輸到EOA或合約
當你的交易包含data時,它很可能是發送到合約地址的。這並不意味著你無法向EOA發送data。事實上,你可以做到這一點。但是,在這種情況下,data的解釋取決於你用來訪問EOA的錢包。大多數錢包會忽略它們控制的EOA交易中收到的任何data。將來,可能會出現允許錢包以合約的方式解釋data編碼的標準,從而允許交易調用在用戶錢包內運行的函數。關鍵的區別在於,與合約執行不同,EOA對data的任何解釋都不受以太坊共識規則的約束。
現在,假設你的交易是向合約地址提供 data。在這種情況下,data 將被EVM解釋為 函數調用 function invocation,調用指定的函數並將任何編碼參數傳遞給該函數。
發送到合約的 data 是一個十六進制序列化的編碼:
- 函數選擇器(function selector)
-
函數prototype的Keccak256雜湊的前4個字節。這使EVM能夠明確地識別你希望調用的功能。
- 函數參數
-
函數的參數,根據EVM定義的各種基本類型的規則進行編碼。
我們來看一個簡單的例子,它來自我們的[solidity_faucet_example]。在Faucet.sol中,我們為取款定義了一個函數:
function withdraw(uint withdraw_amount) public {
withdraw函數的prototype被定義為包含函數名稱的字串,隨後是括號中括起來的每個參數的數據類型,並用單個逗號分隔。函數名稱是withdraw,它只有一個參數是uint(它是uint256的別名)。所以withdraw的原型將是:
withdraw(uint256)
我們來計算這個字串的Keccak256雜湊值(我們可以使用truffle控制台或任何JavaScript web3控制台來做到這一點):
web3.sha3("withdraw(uint256)");
'0x2e1a7d4d13322e7b96f9a57413e1525c250fb7a9021cf91d1540d5b69f16a49f'
雜湊的前4個字節是 0x2e1a7d4d。這是我們的“函數選擇器”的值,它會告訴EVM我們想調用哪個函數。
接下來,讓我們計算一個值作為參數 withdraw_amount 傳遞。我們要取款0.01 ether。我們將它編碼為一個十六進制序列化的大端序無符號256位整數,以wei為單位:
withdraw_amount = web3.toWei(0.01, "ether");
'10000000000000000'
withdraw_amount_hex = web3.toHex(withdraw_amount);
'0x2386f26fc10000'
現在,我們將函數選擇器添加到這個參數上(填充為32字節):
2e1a7d4d000000000000000000000000000000000000000000000000002386f26fc10000
這就是我們的交易的 data,調用 withdraw 函數並請求0.01 ether作為 withdraw_amount。
特殊交易:合約註冊
有一種特殊的帶有data,沒有value的交易。表示註冊一個新的合約。合約登記交易被發送到一個特殊的目的地地址,即零地址。簡而言之,合約註冊交易中的to欄位包含地址 0x0。該地址既不代表EOA(沒有相應的私人/公共密鑰對)也不代表合約。它永遠不會花費ether或啟動交易。它僅用作目的地,具有“註冊此合約”的特殊含義。
儘管零地址僅用於合約註冊,但它有時會收到來自各個地址的付款。對此有兩種解釋:無論是偶然的,導致ether的喪失,還是故意的_ ether燃燒_(見[burning_ether])。如果你想進行有意識的ether燃燒,你應該向網路明確你的意圖,並使用專門指定的燃燒地址:
0x000000000000000000000000000000000000dEaD
Warning
|
發送至合約註冊地址 0x0 或指定燃燒地址 0x0 ... dEaD 的任何ether將變得不可消費並永遠丟失。 |
合約註冊交易不應包含ether value,只能包含合約編譯 Bytecode 的data。此次交易的唯一影響是註冊合約。
作為例子,我們可以發佈 [intro] 中使用的 Faucet.sol。合約需要編譯成二進制十六進制表示。這可以用Solidiy編譯器完成。
> solc --bin Faucet.sol ======= Faucet.sol:Faucet ======= Binary: 6060604052341561000f57600080fd5b60e58061001d6000396000f300606060405260043610603f576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680632e1a7d4d146041575b005b3415604b57600080fd5b605f60048080359060200190919050506061565b005b67016345785d8a00008111151515607757600080fd5b3373ffffffffffffffffffffffffffffffffffffffff166108fc829081150290604051600060405180830381858888f19350505050151560b657600080fd5b505600a165627a7a72305820d276ddd56041f7dc2d2eab69f01dd0a0146446562e25236cf4ba5095d2ee802f0029
相同的資訊也可以從Remix在線編譯器獲得。 現在我們可以創建交易。
> src = web3.eth.accounts[0];
> faucet_code = "0x6060604052341561000f57600080fd5b60e58061001d6000396000f300606060405260043610603f576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680632e1a7d4d146041575b005b3415604b57600080fd5b605f60048080359060200190919050506061565b005b67016345785d8a00008111151515607757600080fd5b3373ffffffffffffffffffffffffffffffffffffffff166108fc829081150290604051600060405180830381858888f19350505050151560b657600080fd5b505600a165627a7a72305820d276ddd56041f7dc2d2eab69f01dd0a0146446562e25236cf4ba5095d2ee802f0029"
> web3.eth.sendTransaction({from: src, data: faucet_code, gas: 113558, gasPrice: 200000000000})
"0x7bcc327ae5d369f75b98c0d59037eec41d44dfae75447fd753d9f2db9439124b"
不需要指定to參數,將使用預設的零地址。你可以指定 gasPrice 和 gas 限制。 一旦合約被開採,我們可以在etherscan區塊瀏覽器上看到它。
你可以查看交易的接收者以獲取有關合約的資訊。
> eth.getTransactionReceipt("0x7bcc327ae5d369f75b98c0d59037eec41d44dfae75447fd753d9f2db9439124b");
{
blockHash: "0x6fa7d8bf982490de6246875deb2c21e5f3665b4422089c060138fc3907a95bb2",
blockNumber: 3105256,
contractAddress: "0xb226270965b43373e98ffc6e2c7693c17e2cf40b",
cumulativeGasUsed: 113558,
from: "0x2a966a87db5913c1b22a59b0d8a11cc51c167a89",
gasUsed: 113558,
logs: [],
logsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
status: "0x1",
to: null,
transactionHash: "0x7bcc327ae5d369f75b98c0d59037eec41d44dfae75447fd753d9f2db9439124b",
transactionIndex: 0
}
在這裡我們可以看到合約的地址。我們可以按照 將數據傳輸到EOA或合約 所示,從合約發送和接收資金。
> contract_address = "0xb226270965b43373e98ffc6e2c7693c17e2cf40b"
> web3.eth.sendTransaction({from: src, to: contract_address, value: web3.toWei(0.1, "ether"), data: ""});
"0x6ebf2e1fe95cc9c1fe2e1a0dc45678ccd127d374fdf145c5c8e6cd4ea2e6ca9f"
> web3.eth.sendTransaction({from: src, to: contract_address, value: 0, data: "0x2e1a7d4d000000000000000000000000000000000000000000000000002386f26fc10000"});
"0x59836029e7ce43e92daf84313816ca31420a76a9a571b69e31ec4bf4b37cd16e"
過一段時間,這兩個交易都可以在ethescan上看到
數位簽章
到目前為止,我們還沒有深入探討“數位簽章”的細節。在本節中,我們將探討數位簽章是如何工作的,以及如何在不洩露私鑰的情況下提供私鑰所有權的證明。
橢圓曲線數位簽章演算法(ECDSA)
以太坊中使用的數位簽章演算法是Elliptic Curve Digital Signature Algorithm,或ECDSA。ECDSA是用於基於橢圓曲線私鑰/公鑰對的數位簽章的演算法,如 [elliptic_curve] 中所述。
數位簽章在以太坊中有三種用途(請參閱下面的邊欄)。首先,簽名證明私鑰的所有者,暗示著以太坊賬戶的所有者,已經授權支付ether或執行合約。其次,授權的證明是undeniable(不可否認)。第三,簽名證明交易數據在交易簽名後沒有也不能被任何人修改。
數位簽章如何工作
數位簽章是一種數學簽名,由兩部分組成。第一部分是使用私鑰(簽名密鑰)從消息(交易)中創建簽名的演算法。第二部分是允許任何人僅使用消息和公鑰來驗證簽名的演算法。
創建數位簽章
在以太坊實現的ECDSA中,被簽名的“消息”是交易,或者更確切地說,來自交易的RLP編碼數據的Keccak256雜湊。簽名密鑰是EOA的私鑰。結果是簽名:
\(\(Sig = F_{sig}(F_{keccak256}(m), k)\)\)
其中:
-
k是簽名私鑰
-
m是RLP編碼的交易
-
Fkeccak256 是Keccak256雜湊函數
-
Fsig 是簽名演算法
-
Sig 是由此產生的簽名
更多關於ECDSA數學的細節可以在 ECDSA數學 中找到。
函數 Fsig 產生一個由兩個值組成的簽名Sig,通常稱為R和S:
Sig = (R, S)
驗證簽名
要驗證簽名,必須有簽名(R和S),序列化交易和公鑰(與用於創建簽名的私鑰對應)。實質上,對簽名的驗證意味著“只有生成此公鑰的私鑰的所有者才能在此交易上產生此簽名。”
簽名驗證演算法採用消息(交易的雜湊或其部分),簽名者的公鑰和簽名(R和S值),如果簽名對此消息和公鑰有效,則返回TRUE。
ECDSA數學
如前所述,簽名由數學函數 Fsig 創建,該函數生成由兩個值R和S組成的簽名。在本節中,我們將更詳細地討論函數 Fsig 。
簽名演算法首先生成ephemeral(臨時的)私鑰/公鑰對。在涉及簽名私鑰和交易雜湊的轉換之後,此臨時密鑰對用於計算R和S值。
臨時密鑰對由兩個輸入值生成:
1.一個隨機數q,用作臨時私鑰 1.和橢圓曲線生成點G
從q和G開始,我們生成相應的臨時公鑰Q(以Q = q * G計算,與以太坊公鑰的派生方式相同,參見[pubkey])。數位簽章的R值就是臨時公鑰Q的x座標。
然後,演算法計算簽名的S值,以便:
S ≡ q-1 (Keccak256(m) + k * R) (mod p)
其中:
-
q是臨時私鑰
-
R是臨時公鑰的x座標
-
k是簽名(EOA所有者)的私鑰
-
m是交易數據
-
p是橢圓曲線的質數階
驗證是簽名生成函數的反函數,使用R,S值和公鑰來計算一個值Q,它是橢圓曲線上的一個點(簽名創建中使用的臨時公鑰):
Q ≡ S-1 * Keccak256(m) * G + S-1 * R * K (mod p)
其中:
-
R和S是簽名值
-
K是簽名者(EOA所有者)的公鑰
-
m是被簽名的交易數據
-
G是橢圓曲線生成點
-
p是橢圓曲線的質數階
如果計算的點Q的x座標等於R,則驗證者可以斷定該簽名是有效的。
請注意,在驗證簽名時,私鑰既不被知道也不會透露。
Tip
|
ECDSA必然是一門相當複雜的數學; 完整的解釋超出了本書的範圍。許多優秀的在線指南會一步一步地通過它:搜索“ECDSA explained”或嘗試這一個:http://bit.ly/2r0HhGB[]。 |
實踐中的交易簽名
為了產生有效的交易,發起者必須使用橢圓曲線數位簽章演算法將數位簽章應用於消息。當我們說“簽署交易”時,我們實際上是指“簽署RLP序列化交易數據的Keccak256雜湊”。簽名應用於交易數據的雜湊,而不是交易本身。
Tip
|
在#2,675,000塊,Ethereum實施了“Spurious Dragon”硬分叉,除其他更改外,還推出了包括交易重播保護的新簽名方案。這個新的簽名方案在EIP-155中指定(參見[eip155])。此更改會影響簽名過程的第一步,在簽名之前向交易添加三個欄位(v,r,s)。 |
要在以太坊簽署交易,發件人必須:
-
創建一個包含九個欄位的交易資料結構:nonce,gasPrice,startGas,to,value,data,v,r,s
-
生成交易的RLP編碼的序列化消息
-
計算此序列化消息的Keccak256雜湊
-
計算ECDSA簽名,用發起EOA的私鑰簽名雜湊
-
在交易中插入ECDSA簽名計算出的 r 和 s 值
原始交易創建和簽名
讓我們創建一個原始交易並使用 ethereumjs-tx 庫對其進行簽名。此範例的源程式碼位於GitHub儲存庫中的 raw_tx_demo.js 中:
link:code/web3js/raw_tx/raw_tx_demo.js[]
運行範例程式碼:
$ node raw_tx_demo.js RLP-Encoded Tx: 0xe6808609184e72a0008303000094b0920c523d582040f2bcb1bd7fb1c7c1ecebdb348080 Tx Hash: 0xaa7f03f9f4e52fcf69f836a6d2bbc7706580adce0a068ff6525ba337218e6992 Signed Raw Transaction: 0xf866808609184e72a0008303000094b0920c523d582040f2bcb1bd7fb1c7c1ecebdb3480801ca0ae236e42bd8de1be3e62fea2fafac7ec6a0ac3d699c6156ac4f28356a4c034fda0422e3e6466347ef6e9796df8a3b6b05bed913476dc84bbfca90043e3f65d5224
用EIP-155創建原始交易
EIP-155“簡單重播攻擊保護”標準在簽名之前指定了重播攻擊保護(replay-attack-protected)的交易編碼,其中包括交易數據中的chain identifier。這確保了為一個區塊鏈(例如以太坊主網)創建的交易在另一個區塊鏈(例如Ethereum Classic或Ropsten測試網路)上無效。因此,在一個網路上廣播的交易不能在另一個網路上廣播,因此得名“重放攻擊保護”。
EIP-155向交易資料結構添加了三個欄位 v,r和s。r和s 欄位被初始化為零。這三個欄位在編碼和雜湊之前被添加到交易數據中。因此,三個附加欄位會更改交易的雜湊,稍後將應用簽名。通過在被簽名的數據中包含鏈識別碼,交易簽名可以防止任何更改,因為如果鏈識別碼被修改,簽名將失效。因此,EIP-155使交易無法在另一個鏈上重播,因為簽名的有效性取決於鏈識別碼。
簽名前綴欄位v被初始化為鏈識別碼,其值為:
Chain |
Chain ID |
|
Ethereum main net |
1 |
|
Morden (obsolete), Expanse |
2 |
|
Ropsten |
3 |
|
Rinkeby |
4 |
|
Rootstock main net |
30 |
|
Rootstock test net |
31 |
|
Kovan |
42 |
|
Ethereum Classic main net |
61 |
|
Ethereum Classic test net |
62 |
|
Geth private testnets |
1337 |
由此產生的交易結構被進行RLP編碼,雜湊和簽名。簽名演算法也稍作修改,以在v前綴中對鏈ID進行編碼。
有關更多詳細資訊,請參閱EIP-155規範: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md
簽名前綴值(v)和公鑰恢復
如交易的結構所述,交易消息不包含任何“from”欄位。這是因為發起者的公鑰可以直接從ECDSA簽名中計算出來。一旦你有公鑰,你可以很容易地計算出地址。恢復簽名者公鑰的過程稱為公鑰恢復。
給定 ECDSA數學 中計算的值 r 和 s,我們可以計算兩個可能的公鑰。
首先,我們根據簽名中的x座標 r 值計算兩個橢圓曲線點R和R'。有個兩點,因為橢圓曲線在x軸上是對稱的,所以對於任何值x,在x軸的兩側有兩個可能的值適合曲線。
從 r 開始,我們也計算r-1這是 r 的倒數。
最後我們計算 z,它是消息雜湊的最低位,其中n是橢圓曲線的階數。
然後兩個可能的公鑰是:
K1 = r-1 (sR - zG)
和
K2 = r-1 (sR' - zG)
其中:
-
K1 和 K2 是簽名者公鑰的兩種可能性
-
r-1是簽名的r值的倒數
-
s是簽名的s值
-
R和R'是臨時公鑰Q的兩種可能性
-
z是消息雜湊的最低位
-
G是橢圓曲線生成點
為了使事情更有效率,交易簽名包括一個前綴值 v,它告訴我們兩個可能的R值中哪一個是臨時的公鑰。如果 v 是偶數,那麼R是正確的值。如果 v 是奇數,那麼選擇R'。這樣,我們只需要計算R的一個值。
分離簽名和傳輸(離線簽名)
一旦交易被簽署,它就可以傳送到以太坊網路。創建,簽署和廣播交易的三個步驟通常發生在單個函數中,例如使用web3.eth.sendTransaction。但是,正如我們在原始交易創建和簽名中看到的那樣,你可以通過兩個單獨的步驟創建和簽署交易。一旦你簽署了交易記錄,你就可以使用web3.eth.sendSignedTransaction傳輸該交易記錄,該方法採用十六進制編碼的簽名交易資訊並在Ethereum網路上傳輸。
你為什麼要分開交易的簽署和傳輸?最常見的原因是安全:簽名交易的計算機必須將解鎖的私鑰加載到記憶體中。傳輸的計算機必須連接到互聯網並運行以太坊客戶端。如果這兩個功能都在一臺計算機上,那麼你的在線系統上有私鑰,這非常危險。分離簽名和傳輸功能稱為 離線簽名 offline signing,是一種常見的安全措施。
根據你所需的安全級別,你的“離線簽名”計算機可能與在線計算機存在不同程度的分離,從隔離和防火牆子網(在線但隔離)到完全脫機系統,成為 氣隙 air-gapped系統 。在氣隙系統中根本沒有網路連接 - 計算機與在線環境是“空氣”隔離的。使用數據儲存介質或(更好)網路攝像頭和QR碼將交易記錄到氣隙計算機上,以簽署交易。當然,這意味著你必須手動傳輸你想要簽名的每個交易,不能批量化。
儘管沒有多少環境可以利用完全氣隙系統,但即使是小程度的隔離也具有顯著的安全優勢。例如,帶防火牆的隔離子網只允許通過消息隊列協議,可以提供大大降低的攻擊面,並且比在線系統上簽名的安全性高得多。許多公司使用諸如ZeroMQ(0MQ)的協議,因為它為簽名計算機提供了減少的攻擊面。有了這樣的設置,交易就被序列化並排隊等待簽名。排隊協議以類似於TCP套接字的方式將序列化的消息發送到簽名計算機。簽名計算機從隊列中讀取序列化的交易(仔細地),使用適當的密鑰應用簽名,並將它們放置在傳出隊列中。傳出隊列將簽名的交易傳輸到使用Ethereum客戶端的計算機上,客戶端將這些交易出隊並傳輸。
交易傳播
以太坊網路使用“泛洪”路由協議。每個以太坊客戶端,在Peer-to-Peer(P2P)中作為node,(理想情況下)構成mesh網路。沒有網路節點是“特殊的”,它們都作為平等的對等體。我們將使用術語“節點”來指代連接並參與P2P網路的以太坊客戶端。
交易傳播開始於創建(或從離線接收)簽名交易的以太坊節點。交易被驗證,然後傳送到直接連接到始發節點的所有其他以太坊節點。平均而言,每個以太坊節點保持與至少13個其他節點的連接,稱為鄰居。每個鄰居節點在收到交易後立即驗證交易。如果他們同意這是有效的,他們會保存一份副本並將其傳播給所有的鄰居(除了它的鄰居)。結果,交易從始發節點向外漣漪式地遍歷網路,直到網路中的所有節點都擁有交易的副本。
幾秒鐘內,以太坊交易就會傳播到全球所有以太坊節點。從每個節點的角度來看,不可能辨別交易的起源。發送給我們節點的鄰居可能是交易的發起者,或者可能從其鄰居那裡收到它。為了能夠跟蹤交易的起源或干擾傳播,攻擊者必須控制所有節點的相當大的百分比。這是P2P網路安全和隱私設計的一部分,尤其適用於區塊鏈。
記錄到區塊鏈中
雖然以太坊中的所有節點都是相同的對等節點,但其中一些節點由礦工操作,並將交易和數據塊提供給挖礦農場,這些節點是具有高性能圖形處理單元(GPU)的計算機。挖掘計算機將交易添加到候選塊,並嘗試查找使得候選塊有效的Proof-of-Work。我們將在[consensus]中更詳細地討論這一點。
不深入太多細節,有效的交易最終將被包含在一個交易塊中,並記錄在以太坊區塊鏈中。一旦開採成塊,交易還通過修改賬戶餘額(在簡單付款的情況下)或通過調用改變其內部狀態的合約來修改以太坊單例的狀態。這些更改將與交易一起以交易收據 receipt 的形式記錄,該交易也可能包含事件 events。我們將在 [evm] 中更詳細地檢查所有這些。
我們的交易已經完成了從創建到被EOA簽署,傳播以及最終採礦的旅程。它改變了單例的狀態,並在區塊鏈上留下了不可磨滅的印記。
多重簽名(multisig)交易
如果你熟悉比特幣的腳本功能,那麼你就知道有可能創建一個比特幣多重簽名賬戶,該賬戶只能在多方簽署交易時花費資金(例如2個或3個或4個簽名)。以太坊的價值交易沒有多重簽名的規定,儘管可以部署任意條件的任意合約來處理ether和代幣的轉讓。
為了在多重簽名情況下保護你的ether,將它們轉移到多重簽名合約中。無論何時你想將資金轉入其他賬戶,所有必需的用戶都需要使用常規錢包軟體將交易發送至合約,從而有效授權合約執行最終交易。
這些合約也可以設計為在執行本地程式碼或觸發其他合約之前需要多個簽名。該方案的安全性最終由多重簽名合約程式碼確定。