第一章:智能合約簡介
單擊此處查看原文
一個簡單的例子
我們從一個最基礎的例子開始,即使你是零基礎也無所謂,你將會閱讀到詳細的文檔。
Storage
pragma solidity ^0.4.0;
contract SimpleStorage {
uint storedData;
function set(uint x) public {
storedData = x;
}
function get() public constant returns (uint) {
return storedData;
}
}
首行只是簡單的告訴編譯器使用 0.4.0 版本或者更新的版本(不包括 0.5.0 ),不影響功能。這么做可以保證合約代碼在新的編譯環境中不會突然出現不同的行為而導致 bug。通常關鍵詞 pragma
在這里的作用就是告訴編譯器要如何處理源代碼。
從某種意義上說,用 Solidity 寫的合約就是一個 functions 和 state 的集合,歸屬於以太坊區塊鏈中的一個指定地址。
第三行uint storedData;
聲明了一個 state variable :storedData
,類型是 uint
(unsigned integer of 256 bits)。你可以認為它是數據庫中的單個插槽,並可以通過調用代碼中的一些方法來查詢和修改,以此管理數據庫。
在以太坊中,合約被永久持有。在此例中,set
和 get
方法可以用來修改和查詢變量的值。
若要訪問一個 state variable ,你不需要加前綴 this
,它不同於其他語言的通常做法。
這個智能合約並沒有做很多事情(因為基礎建設以太坊團隊都幫你做好了),它除了允許世界上的任何用戶存儲單個數字之外,也沒有一個可行的方法來阻止你 publishing 這個數字。當然,任何人都可以調用 set
方法,用不同的值來覆蓋你的數字,而原來的數字仍然被保存在區塊鏈上。接下來,我們將會看到如何啟用訪問限制來使得只有你自己可以修改自己的數字。
注意:
所有的標識符(合約名、方法名、變量名)都被限定為只可以使用 ascii 字符來定義。只有 string 類型的變量你才可以給它賦一個 utf-8 的值。
警告:
使用 Unicode 文本要格外小心,因為類似的(甚至是相同的)字符可以有不同的代碼點,這意味着它們可以被編碼成不同的 byte array
Subcurrency 示例
譯者注: currency 是貨幣的意思,之后出現的 xxxcurrency 你可以把他們拆成 xxx currency 來理解
下面的合約將會實現一個最簡單的 cryptocurrency 。它可以憑空產生貨幣,不過只有創造者才可以做到(it is trivial to implement a different issuance scheme. 這里是一句括號內的說明性文字,翻譯出來有點奇怪就不翻譯了哈哈)。 此外人們不需要提供用戶名、密碼來注冊賬號就可以互相發送貨幣,只是需要借助一對 Ethereum keypair 。
pragma solidity ^0.4.0;
contract Coin {
// The keyword "public" makes those variables
// readable from outside.
address public minter;
mapping (address => uint) public balances;
// Events allow light clients to react on
// changes efficiently.
event Sent(address from, address to, uint amount);
// This is the constructor whose code is
// run only when the contract is created.
function Coin() public {
minter = msg.sender;
}
function mint(address receiver, uint amount) public {
if (msg.sender != minter) return;
balances[receiver] += amount;
}
function send(address receiver, uint amount) public {
if (balances[msg.sender] < amount) return;
balances[msg.sender] -= amount;
balances[receiver] += amount;
Sent(msg.sender, receiver, amount);
}
}
這個合約介紹了一些新概念,一行行來看。
address public minter;
這一行聲明了一個公有 state variable ,類型為 address
。
address
類型是一個不允許進行算數運算的 160 位的值。這非常適合用於存儲合約地址或者外部人員的 keypairs 。
public
關鍵字自動生成了一個方法,用來允許你訪問 state variable 的當前值。如果沒有這個關鍵字,其他的合約無法訪問該變量。生成的方法如下:
function minter() returns (address) {
return minter;
}
然而,你去手動添加一個完全一樣的方法是沒用的,因為我們需要的是相同名稱的一個 function 和一個 state variable。
而現在為了產生這兩個東西,只需要使用一個 public
關鍵字,剩下的編譯器已經幫你做好了。
下一行,mapping (address => uint) public balances;
, 同樣也創建了一個公共 state variable ,不過這是個更為復雜的數據類型。這個類型將地址映射到無符號整型。
映射可以看作是做實際初始化的 hash tables,這樣每個可能的 key 都會存在並且都可以映射到一個字節表示都為 0 的值。
這個比喻不會太過分,話雖如此,但你既不能獲取映射的所有鍵,也不能獲取所有值。所以要么你就記住(或者更好的,保留一個列表或者更高級的數據類型)添加了什么東西到映射中,要么你就在一個不需要使用到的上下文中使用它,就像這個。在此例中 public
關鍵字生成的 getter function
稍微有點復雜,它看起來就像下面的代碼:
function balances(address _account) public view returns (uint) {
return balances[_account];
}
如你所見,你可以使用這個方法輕松地查詢單個賬戶的余額。
event Sent(address from, address to, uint amount);
這一行聲明了一個所謂的 event
,在方法的最后一行被觸發。
用戶接口(當然還有服務端接口)可以低成本的監聽在區塊鏈上被觸發的事件。只要事件被觸發,監聽者也會收到參數 from
、to
、 amount
,這可以輕松地追蹤 transactions 。為了監聽這個事件,你需要這樣使用:
Coin.Sent().watch({}, '', function(error, result) {
if (!error) {
console.log("Coin transfer: " + result.args.amount +
" coins were sent from " + result.args.from +
" to " + result.args.to + ".");
console.log("Balances now:\n" +
"Sender: " + Coin.balances.call(result.args.from) +
"Receiver: " + Coin.balances.call(result.args.to));
}
})
你需要注意如何從用戶接口調用自動生成的方法 balance
。
特殊的方法: Coin
,是一個構造器,只在合約被創建的時候運行,之后都不能再調用。它永久保存了用戶創建的合約地址:
msg
(還有 tx
和 block
) 是一個 magic 全局變量,它包含了一些可以允許訪問區塊鏈的屬性。msg.sender
是當期方法調用的地址。
最后,方法實際上都會以合約結尾,可以被用戶調用,合約都是 mint
和 send
。如果 mint
被非本賬戶調用去創建合約,什么都不會發生。另一方面, send
可以被任何人(但必須也是持幣人)使用來向別的用戶發送貨幣。
注意如果你使用了合約來發送貨幣到一個地址,在你使用區塊鏈瀏覽器查找那個地址的時候,你看不到任何相關信息。因為事實上你發送的貨幣、改變的余額僅僅是被存儲在這個獨有的貨幣合約的寄存器中。
通過使用事件,很輕松就可以創建一個“區塊鏈瀏覽器”,用來追蹤你新貨幣的 transactions 和余額。
區塊鏈基礎
對於程序員來說區塊鏈概念並不會難以理解,原因是大部分的難點(mining,hashing,elliptic-curve cryptography,peer-to-peer networks,etc)都已經有了一套特定的 features 和 promises 。
一旦接受這些特性,你就無需再為底層技術而發愁 —— 就像是難道你必須知道亞馬遜雲的內部實現才能使用它的功能?
Transactions
一條區塊鏈就是一個全球共享的 transactional database 。這意味着每個參與進網絡的人都可以查詢數據庫中的記錄。如果你想更改數據庫中的記錄,
你必須創建一個所謂的 transaction 來讓其他所有人都接受。 transaction 這個詞意味着你希望做出的改動(假設你希望同一時間更改兩個值)要么全部都成功,要么全部都失敗。另外,當你的 transaction 保存到了數據庫,沒有別的 transaction 可以再修改它。
比如說,假設一個表記錄了一個電子貨幣賬戶的所有余額,如果發起了一個轉賬請求,數據庫的 transactional nature 會保證一個賬戶減去資金,另一個賬戶增加相應的資金。如果由於某些原因,目標賬戶增加余額失敗了,那么轉賬的源賬戶資金也不會有任何改動。
另外,一筆 transaction 總是會被發送者(創建者)簽名加密,直接保護了數據庫即將做出的修改的訪問權限。
在上述電子賬戶的例子中,用了一個簡單的驗證來保證必須持有賬戶相應的 key 才可以進行轉賬。
區塊
一個需要克服的重要難點,在比特幣中被稱為“雙重支付攻擊” (double-spend attach,也叫雙花攻擊):
如果網絡中兩筆 transactions 都想清空一個賬戶會發生什么事情?這也就是所謂的沖突。
一個抽象的回答是:你不必在意。transaction 的順序是由你來選擇的,transaction 被綁定在區塊中,之后會被執行,並且分布在所有參與的節點中。如果有兩筆 transactions 互斥,后來的一筆 transaction 總是會被拒絕成為區塊的一部分。
這些區塊在時間上形成一個線性序列,由此也形成了“區塊鏈”這個詞。區塊以一定的間隔被追加到鏈條中 —— 以太坊中的間隔大約是17秒。
作為 order selection mechanism(硬翻過來就是:訂單選擇機制,也稱為“采礦”)的一部分,區塊可能偶爾會被回滾,不過也只是在鏈條的最頂端才會出現,越多的區塊被追加到頂部,回滾發生的概率也就越小。所以你的 transaction 有可能被回滾甚至從區塊鏈中刪除,不過等待時間越久,發生的可能性越小。
The Ethereum Virtual Machine
概覽
EVM 是以太坊智能合約的運行時環境。它不僅是沙盒,事實上也是完全隔離的,意思就是 EVM 中的代碼運行不會訪問網絡、文件系統或其他進程,甚至智能合約之間也被限制訪問。
Accounts
以太坊有兩種 account,共享同一個地址空間:
external accounts
,被公私密鑰對控制contract accounts
,被賬戶存放的代碼控制
external accounts
的地址由公鑰決定,而合約的地址是在合約被創建時生成的(來源於創建者的地址並且它會發送 transaction 的數量,也就是所謂的“nonce”)。
無論賬戶是否存儲代碼,兩種類型都被 EVM 一視同仁。
每個賬戶都持久存儲了一個鍵值對,把一個 256-bit 的 word 映射成另一個 256-bit 的 word,稱之為 storage
。
另外,每個賬戶在 Ether(Wei 可能更准確)中都有一個 balance
,可以發送 transaction 來修改。
Transactions
一筆 transaction 是一條從 A 賬戶到 B 賬戶的 message
(兩個賬戶可能是同一個,也有可能是特殊的 zero-account
,詳情見下方),可以包含二進制數據(就是它的 payload)和 Ether 。
如果目標賬戶有代碼,那么這段代碼會被執行,傳遞過來的 payload 會作為實參輸入。
如果目標賬戶是 zero-account
(也就是賬戶地址為 0),那么這筆 transaction 會創建一個新的合約。如前所述,合約地址不是 0 ,而是一個來自發送者的地址及其發送的 transaction 數量(nonce)。這個合約創建的 transaction 的 payload 會被轉成 EVM bytecode 來執行。執行的結果會被作為代碼永久保存在合約中。這也意味着為了創建一個合約,你不用發送合約的真實代碼,but in fact code that returns that code 。
Gas
在創建 transaction 的時候,每次都需要支付一定數量的 gas ,目的是限制執行 transaction 所需的工作量,並為此執行付費。當 EVM 執行 transaction 的時候, gas 會以特定的規則逐漸耗盡。
gas price 是 transaction 的創建者設定好的,必須從發送方賬戶支付 gas_price * gas
的預付款。如果執行之后有 gas 遺留,將會原路退還。
如果 gas 在任意時刻用完了(也就是說變成了負數),就觸發一個 out-of-gas
的異常,會回滾對當前調用幀的 state
做出的修改。
Storage, Memory and the Stack
每個賬戶都有一個持久內存區稱為 storage
。storage
是一個 key-value store,將 256-bit 的 word 映射到另一 256-bit 的 word 。
從一個合約中列舉 storage
是不可能的,因為讀取的成本比較昂貴,修改 storage
更貴,一個合約除了它自己以外不能讀取或修改 storage
。
第二個內存區域叫做 memory
,其中一個合約為每個 message
的調用獲取一個新的實例(a freshly cleared instance)。memory
是線性的,可以在字節級別處理,但是讀取的 width 被限制為 256 bits,而寫入可以是 8 bits 或者 256 bits 。當訪問(讀或者寫)一個之前從未接觸的的 memory word
(比如一個 word 內的任意偏移量)時,memory
由一個 256-bit 的 word 展開。在展開的時候,必須支付 gas ,memory
越大越貴(尺寸的 2 次冪)。
EVM 並不是一個 register machine,而是一個 stack machine ,所有的計算都是在一個名為 stack
的區域上執行的。它的最大 size 是 1024 個元素。通過以下方式訪問 stack
僅限於頂部:將最上面的 16 個元素之一復制到 stack
的頂部,或者用下面的 16 個元素之一來交換頂層元素。所有其他的操作都從 stack
的最上面取兩個(或者一個或更多,取決於操作)元素,然后將結果 push 到 stack
上。當然,你可以把 stack
的元素移動到 storage
或者 memory
,但是如果不先把 stack
頂部刪除就不能隨意訪問 stack
深處的元素。
Instruction Set
為了避免不正確的實現導致一致性問題,EVM instruction set 一直都保持最小化。所有的 instruction 操作都在基本數據類型上進行,也就是 256-bit word 。通常的算法,位、邏輯、比較都是有的,有條件和無條件的跳轉也都有。另外,合約可以訪問當前區塊的相關屬性,比如序號(number)和時間戳(timestamp)。
調用 Message
合約可以調用另外的合約或者調用 message 來發送 Ether 到 non-contact 賬戶。調用 message 跟交易類似,它們有 source、target、data payload、Ether、gas、return data 。事實上,每筆交易都由一個 top-level message 調用,轉而又可以創建更多的 message 調用。
一個合約可以通過內部 message 調用來決定尚存的 gas 有多少需要被發送和有多少需要被留下。如果在內部調用的時候發生了 out-of-gas 異常(或其他的異常),就會通過推送到 stack
上的錯誤值來發送信號,此時只有跟調用一起發送的 gas 被用完。在 Solidity 中,這種情況下調用合約默認會觸發一個 manual exception ,所以會導致各種異常冒泡般地涌出。
如上所述,被調用的合約(可以跟調用者相同)將會收到一個 freshly cleared instance of memory ,並且有權調用 payload ,payload 將被發送到一個隔離的區域叫做 calldata
,執行完畢時,它將會返回一個數據存儲在調用者預分配的 memory
中。
調用的深度被限制在 1024, 意味着在更復雜的調用中,循環優先級大於遞歸。
Delegatecall / Callcode and Libraries
調用 message 的時候有一個特殊的變量叫做 delegatecall
,它跟調用 message 是一樣的,除了目標地址中的代碼是在被調用的合約中執行的,並且 msg.sender 和 msg.value 的值不改變。
意味着一個合約可以在運行時從不同的地址動態載入代碼。storage
、當前地址和余額仍然指向被調用的合約,僅僅只有代碼是來自被調用的地址。
這使得在 Solidity 實現 library 特性成為可能:可重用的 library 代碼可以被應用到一個合約的 storage
,比如說:為了實現一個復雜的數據結構。
Logs
可以在一種特殊的索引數據結構中存儲數據,它可以映射到區塊級別。這種特性稱為 logs
,用來實現 events
。logs
創建之后合約無法訪問其數據,不過它們可以有限的從外部訪問區塊鏈。自從 log data
的一部分被存儲到 bloom filter之后,就可以高效並安全加密地搜索這些數據,網絡節點(輕型客戶端)也無需下載整個區塊鏈就可以訪問這些日志。
Create
合約可以用一種特殊的操作碼來生成另外的合約(也就是說它們不是簡單的調用 zero address
)。調用 message 和調用 create 的唯一區別在於 payload data
會被用於執行並且返回結果會被作為代碼存儲,調用者/創建者會收到 stack
上新合約的地址。
Self-destruct
唯一可能從區塊鏈上移除代碼的做法就是讓在相應地址下的合約執行 selfdestruct
操作,該地址遺留的 Ether 會發送到指定的目標然后 storage
和 代碼就會從 state
中移除。
警告:
即使一個合約的代碼沒有selfdestruct
,它也可以通過delegatecall
或者callcode
來執行該操作。
注意:
修改舊合約可能會也可能不會被以太坊客戶端實現。另外,歸檔的節點可以選擇保留合約的storage
和code indefinitely
。
注意:
目前external accounts
不能從state
中移除。