翻譯原文
date:20170612
一個簡單的智能合約
讓我們中最簡單的例子開始∮ぃ現(xiàn)在對所有這一切不了解都沒有關(guān)系负蚊。我們會在后續(xù)的文章中介紹他們的點點滴滴神妹。
存儲(storage)
pragma solidity ^0.4.0;
contract SimpleStorage {
uint storedData;
function set(uint x) {
storedData = x;
}
function get() constant returns (uint) {
return storedData;
}
}
第一行說明了這份代碼的運行環(huán)境是Solidity的0.4.0版本或者兼容這個版本的后續(xù)版本(可以到0.5.0,但是不包括0.5.0)家妆。這保證了在不同版本的編譯器里鸵荠,代碼的執(zhí)行效果都是一樣的。prama
關(guān)鍵詞用來指示編譯器如何編譯代碼(例如c和c++里的#pragma once)伤极。
用Solidity編寫的合約是一個代碼和數(shù)據(jù)的集合蛹找。合約保存在以太坊區(qū)塊鏈中一個特定的地址中。uint storedData;
這行代碼聲明了一個名為storedData
的哨坪、類型為uint
(256位的無符號整形數(shù)據(jù))的變量庸疾。你可以想像成在數(shù)據(jù)庫中的一個小小的數(shù)據(jù)片,我們可以通過調(diào)用函數(shù)來查詢和改變這個值齿税。在以太坊中彼硫,這些函數(shù)總是在各自合約中的函數(shù)(?In the case of Ethereum,this is always the owning contract.)凌箕。在這個例子中拧篮,set
和get
函數(shù)可以用來改變和取出變量storedData
的值。
使用變量牵舱,我們不需要加this.
前綴串绩,這一點和其他的語言相同。
這個合約并沒有做很多事情(很多基礎(chǔ)的事情芜壁,以太坊平臺已經(jīng)幫你完成了),除了實現(xiàn)這樣的功能:允許任何人存儲一個數(shù)據(jù)和獲取數(shù)據(jù)礁凡。任何人都可以調(diào)用set
函數(shù)來覆蓋你寫入的數(shù)據(jù)高氮,但是你之前的數(shù)據(jù)已經(jīng)寫入到區(qū)塊鏈中了。稍后顷牌,我們給合約將增加限制訪問的功能剪芍。這樣一來,那就只有你才能改變這個數(shù)字了窟蓝。
子貨幣例子(subcurrency example)
以下的合約將實現(xiàn)一個簡單的加密貨幣罪裹。憑空產(chǎn)生貨幣是可能的(?It is possible to generate coins out of thin air)运挫,但是只有生成合約的人才能這樣做状共。這是一種狹隘的實現(xiàn)發(fā)行計劃。(谁帕?it is trivial to implement a different issuance scheme)
而且峡继,所有人都可以發(fā)送貨幣給任何人,而不需要用戶名密碼注冊匈挖。你所需要的只是是以太坊的密鑰對碾牌。
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() {
minter = msg.sender;
}
function mint(address receiver, uint amount) {
if (msg.sender != minter) return;
balances[receiver] += amount;
}
function send(address receiver, uint amount) {
if (balances[msg.sender] < amount) return;
balances[msg.sender] -= amount;
balances[receiver] += amount;
Sent(msg.sender, receiver, amount);
}
}
這個合約中將會引入新的概念。我們來一一介紹关划。
address public minter;
這一行聲明了一個公有的address
類型的變量小染。address
類型是一個160位的值翘瓮,它不允許進行任何的算數(shù)運算贮折。它合適用于存儲合同的地址或者其他人的鍵值對。關(guān)鍵詞public
自動的生成一個函數(shù)用于獲取這個變量的值资盅。如果沒有這個關(guān)鍵詞调榄,那其他合同將無法獲取到這個變量。自動生成的函數(shù)如下所示:
function minter() returns (address) { return minter; }
如果直接添加這樣的函數(shù)是行不通的呵扛,因為函數(shù)和變量的名稱相同每庆。但是別擔(dān)心,編譯器會自動完成這個操作的,把兩者區(qū)分開來今穿。
下一行缤灵,mapping (address => uint) public balances;
同樣是生成一個公有變量,但是有著更加復(fù)雜的數(shù)據(jù)類型蓝晒。這個類型將地址映射成為一個無符號整形腮出。該映射可以看作是一個哈希表,這個表里羅列了所有可能的鍵芝薇,并且對應(yīng)于一個字節(jié)表示為全零的值胚嘲。(?Mappings can be seen as hash tables which are virtually initialized such that every possible key exists and is mapped to a value whose byte-representation is all zeros.)但是這個邏輯是行不通的洛二,因為不可能列舉完所有的鍵馋劈,所有的值攻锰。所以要么留意下你映射的類型(最好使用更加高級的類型),要么在不需要的地方使用它妓雾,就像這個例子一樣娶吞。(?So either keep in mind (or better, keep a list or use a more advanced data type) what you added to the mapping or use it in a context where this is not needed, like this one. )(ps:這段話有些難以理解械姻。寝志。。)這個例子由于public
關(guān)鍵字而生成的getter
函數(shù)會比較復(fù)雜些策添,如下所示:
function balances(address _account) returns (uint) {
return balances[_account];
}
如你所見材部,你可以使用這個函數(shù)很方便的查詢到余額。
下一行唯竹,event Sent(address from, address to, uint amount);
聲明了所謂的“事件”乐导,這個事件通過最后一行的send
函數(shù)觸發(fā)。接口(服務(wù)器程序)能夠很容易的監(jiān)聽到這些事件的觸發(fā)浸颓。當(dāng)事件觸發(fā)的時候物臂,監(jiān)聽函數(shù)將會接收到from
,to
产上,amount
參數(shù)棵磷。為了能夠監(jiān)聽這個事件,代碼應(yīng)該這么寫:
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));
}
})
這里注意下晋涣,接口是如何調(diào)用自動生成的函數(shù)balance
仪媒。
下面說下比較特殊的函數(shù),Coin
函數(shù)是合約的構(gòu)造函數(shù)谢鹊,它在合約創(chuàng)建的時候執(zhí)行算吩,并且不會延遲執(zhí)行。它會固定的存儲創(chuàng)建人的address
:msg
(還有tx
和block
)是一個全局變量佃扼,包含了一些允許進入?yún)^(qū)塊鏈的屬性信息偎巢。msg.sender
的值恒為函數(shù)調(diào)用者的address
。
最后兼耀,這些函數(shù)實現(xiàn)了合約压昼,并且可以被用戶使用來履行mint
和send
約定。如果mint
被非創(chuàng)建者調(diào)用瘤运,那么不會進行任何操作窍霞。另一方面,send
可以被其他任何有貨幣的用戶調(diào)用尽超,來進行轉(zhuǎn)賬操作官撼。需要注意的是,如果你通過這個合約給一個address
轉(zhuǎn)賬似谁,在區(qū)塊鏈瀏覽器中你在這個address
中將看不到任何有關(guān)這個操作的信息傲绣。因為這些轉(zhuǎn)賬信息掠哥,余額信息都是存在這個特定的合約當(dāng)中。通過使用事件秃诵,你可以輕易的實現(xiàn)該貨幣的區(qū)塊鏈瀏覽器
续搀,來記錄該貨幣的轉(zhuǎn)賬和余額信息。
區(qū)塊鏈基礎(chǔ)
區(qū)塊鏈對于程序員來說并不是難于理解的一個概念菠净。因為很多難題(挖礦禁舷,哈希,橢圓曲線加密毅往,p2p網(wǎng)絡(luò)等)都是為了提供一系列的功能和承諾牵咙。一旦你接受了這些功能,你就不會擔(dān)心難于理解技術(shù)問題了攀唯。難道你必須理解阿里云怎么提供服務(wù)之后才能使用它嗎洁桌?
交易(transactions)
一條區(qū)塊鏈?zhǔn)且粋€共享的交易數(shù)據(jù)庫。這意味著所有加入網(wǎng)絡(luò)的人都可以訪問數(shù)據(jù)侯嘀。如果你想要對這個數(shù)據(jù)庫做些改變另凌,那么你必須要進行能夠被別人認(rèn)可的交易。交易這個詞說明了要么你完成改變數(shù)據(jù)(假設(shè)你要同時修改兩個值)戒幔,要么什么也沒有發(fā)生吠谢。另外,當(dāng)你的交易被數(shù)據(jù)庫接受之后诗茎,其他的交易不會改變它工坊。
舉個例子,想象一個表格错沃,它列舉了一個電子貨幣里所有賬戶的余額栅组。如果發(fā)起一個轉(zhuǎn)賬操作,將一筆錢從一個賬戶打到另一個賬戶枢析,這個交易性質(zhì)的數(shù)據(jù)庫保證了,一個賬戶余額減少刃麸,另一個賬戶一定多出相應(yīng)的金額醒叁。如果一個賬戶無法收錢,那么原賬戶并不會少錢泊业。
另外把沼,一個交易總是被發(fā)起者(創(chuàng)建者)加密。這直觀的保護了要修改的數(shù)據(jù)吁伺。在電子貨幣這個例子中饮睬,簡單的驗證保證了只有擁有key的人才能從中轉(zhuǎn)賬。
區(qū)塊
在比特幣項目中篮奄,需要克服的一個比較大的問題是所謂的“雙花攻擊”捆愁。雙花攻擊就是網(wǎng)絡(luò)中有兩筆交易都想清空一個賬戶割去,而引發(fā)沖突。
對這個問題比較抽象的解答就是昼丑,你無需關(guān)心呻逆。會自動為你確認(rèn)交易順序。交易寫入?yún)^(qū)塊菩帝,然后執(zhí)行咖城、分發(fā)給網(wǎng)絡(luò)中所有參與的節(jié)點。如果兩筆交易相互矛盾呼奢,那么后一筆交易會駁回宜雀,不會寫入?yún)^(qū)塊。
這些區(qū)塊根據(jù)時間握础,串在一起州袒,形成”區(qū)塊鏈“。這就是區(qū)塊鏈的由來弓候。區(qū)塊按照特定的時間加入到區(qū)塊鏈中郎哭。在以太坊平臺中,這個時間是17秒左右菇存。
由于順序選擇機制(所謂的挖礦)夸研,頂端的區(qū)塊可能經(jīng)常的撤回。越多的區(qū)塊寫入?yún)^(qū)塊鏈中依鸥,你的交易越不容易撤回亥至。
以太坊虛擬機(EVM)
概覽
以太坊虛擬機(EVM)是以太坊平臺智能合約的運行環(huán)境。它并不是一個沙盒贱迟,但是是完全獨立的姐扮,這意味著EVM不能訪問網(wǎng)絡(luò),文件系統(tǒng)或者其他進程衣吠。有些智能合約也被約束為不能訪問其他的智能合約茶敏。
賬號
以太坊平臺有兩種賬號,但是公用一個地址空間:對外賬號——被鍵值對控制著——和合約賬號——與賬號一同保存的代碼缚俏。
對外賬號的address
由公鑰決定惊搏,而合約賬號的地址由合約創(chuàng)建的時候確認(rèn)(它根據(jù)創(chuàng)建者的地址、交易的數(shù)目忧换,組成所謂的“nonce”——“Number used once“——臨時數(shù)據(jù))恬惯。
無論賬號中是否有代碼,EVM都是相同對待的亚茬。
每個賬號都有一個固定的鍵值對酪耳,對應(yīng)于256比特字到256比特字的存儲。(?Every account has a persistent key-value store mapping 256-bit words to 256-bit words called storage.)
另外刹缝,每個賬號都有以太幣余額碗暗,它可以通過進行比特幣交易來改變颈将。
交易
交易是一種從一個賬號發(fā)送到另一個賬號(可能是同個賬號,或者0賬號)的消息讹堤。它包含二進制數(shù)據(jù)(負(fù)載)和以太幣吆鹤。
如果目標(biāo)賬號包含代碼,那么執(zhí)行代碼洲守,二進制負(fù)載數(shù)據(jù)作為代碼的輸入疑务。
如果目標(biāo)賬號是0賬號(address
為0),交易會生成一個新的合約梗醇。之前提到過的知允,合約賬號是由發(fā)送者的address
和轉(zhuǎn)賬金額生成的,絕對是非0賬號叙谨。這類創(chuàng)建合約交易的負(fù)載成為EVM的字節(jié)碼并且執(zhí)行温鸽。執(zhí)行的輸出被永久保存在合約代碼中。這意味著手负,要創(chuàng)建一個合同涤垫,你并不發(fā)送合同的實際代碼,而是返回合同代碼的代碼竟终。(ps:有點繞蝠猬,可以看原文)
燃料(gas)
在創(chuàng)建的時候,每個交易都會收取特定的gas统捶,這個目的在于榆芦,限制交易執(zhí)行的工作量并且對這個執(zhí)行交易收費。當(dāng)EVM執(zhí)行交易喘鸟,gas按照特定的規(guī)則緩慢的消耗掉了匆绣。
gas的價格在創(chuàng)建交易的時候就設(shè)定了,必須在交易之前支付gas價格 * gas
數(shù)目的費用。如果執(zhí)行完畢之后還有g(shù)as剩余什黑,會原路返回到賬戶中崎淳。
如果gas在任何時候消耗完畢(例如成了負(fù)數(shù)),就會觸發(fā)gas不足的異常兑凿,將會撤銷本次交易的所有修改凯力。
storage,內(nèi)存和堆棧
每個賬號都有一個永久的存儲區(qū)域礼华,稱為storage
。storage有一個鍵值存儲拗秘,對應(yīng)于256比特字到256比特字的映射圣絮。例舉一個合同里的storage是不可能的。讀取或者其他修改storage的操作都是代價昂貴的雕旨。合約只能修改自己的storage扮匠,不能修改其他的捧请。
內(nèi)存區(qū)域被稱為內(nèi)存(memory),每次執(zhí)行的時候會獲取到一個清除干凈的內(nèi)存棒搜。內(nèi)存是線性的疹蛉,可以實現(xiàn)byte尋址。但是讀取必須是256位的力麸,寫入可以是8位或者256位的可款。當(dāng)接觸到(讀取或者寫入)之前為觸及的內(nèi)存字的時候,內(nèi)存以256位的字長擴展克蚂。當(dāng)擴展的時候闺鲸,必須支付gas。所以內(nèi)存消耗越大埃叭,花費更大摸恍。
EVM不是一種存儲器機,而是一種堆棧機赤屋。所以所有的計算都在堆棧上執(zhí)行立镶。最大的是1024個元素并包含256位的字。訪問stack的深度有如下的限制規(guī)則:可以將頂端下的16個棧中拷貝出一個元素放置在頂端类早,或者可以將頂端的元素與下面的16個元素中的任意一個互換媚媒。所有其他的操作將距離頂端的兩個(或者一個,或者多個莺奔,全看操作需要)取出計算欣范,將結(jié)果放置在棧頂端。當(dāng)然也可以將堆棧元素放置在storage或者內(nèi)存里令哟。但是不移除棧頂?shù)臅r候是無法再繼續(xù)深入訪問其他元素的恼琼。
指令集
EVM的指令集一直保持最小化,來避免導(dǎo)致一致性問題的可能性屏富。所有指令操作都基于基本的數(shù)據(jù)類型晴竞,256位的字長。允許算術(shù)操作狠半,位操作噩死,邏輯操作以及比較操作∩衲辏可以條件跳轉(zhuǎn)和非條件跳轉(zhuǎn)已维。另外,合約可以訪問當(dāng)前區(qū)塊的相關(guān)屬性已日,例如序號和時間戳垛耳。
消息調(diào)用(message call)
合約可以調(diào)用其他合約或者可以通過消息調(diào)用發(fā)送以太幣到非合約賬號。消息調(diào)用和jiao交易非常相似,有原始地址堂鲜,目的地址栈雳,數(shù)據(jù)負(fù)載,以太幣缔莲,gas和返回值哥纫。實際上,每個交易都是由高級的消息調(diào)用組成痴奏。所謂高級蛀骇,就是可以生成其他消息調(diào)用。
一個合約可以決定發(fā)送多少gas抛虫,保留多少松靡。如果一個gas不足異常(或者其他異常)在內(nèi)部的消息調(diào)用中生成的時候,將會在堆棧頂標(biāo)記一個錯誤的值建椰。在這個例子中雕欺,當(dāng)且僅當(dāng)與消息一起發(fā)送的gas使用完畢時。在solidity中棉姐,調(diào)用合約引起一個認(rèn)為異常屠列,導(dǎo)致調(diào)用棧上升。(伞矩?理解的稀里糊涂笛洛,推薦看原文)
就像之前所說,被調(diào)用的合約(可以是調(diào)用者自身)能夠得到一塊嶄新的內(nèi)存乃坤,并且可以訪問到負(fù)載(在單獨的區(qū)域中提供苛让,稱之為calldata區(qū)域)。在執(zhí)行結(jié)束之后湿诊,它可以返回數(shù)據(jù)狱杰,并存儲到調(diào)用者的內(nèi)存中。
調(diào)用的最大深度是1024厅须,這意味著更復(fù)雜的調(diào)用可以采用遞歸仿畸,而不是循環(huán)。
代理調(diào)用/調(diào)用代碼 和庫(Delegatcall/Callcode and Libraries)
存在一種消息調(diào)用的變種朗和,稱為代理調(diào)用错沽。代理調(diào)用就是說目的地址中的代碼在當(dāng)前調(diào)用者的上下文中運行,msg.sender
和msg.value
不會改變眶拉,其他方面跟一般的消息調(diào)用一致千埃。
這就意味著一個合約可以在運行時動態(tài)的從不同地址中調(diào)用代碼。storage忆植,當(dāng)前地址和余額都是引用當(dāng)前的調(diào)用合約镰禾,僅僅只是代碼從調(diào)用地址中獲取的皿曲,其他的都是當(dāng)前合約的唱逢。
這就可以實現(xiàn)類庫的功能:可以復(fù)用的代碼可以放在合約的storage中吴侦,來實現(xiàn)復(fù)雜的數(shù)據(jù)結(jié)構(gòu)。
日志
可以將數(shù)據(jù)保存在特殊的索引數(shù)據(jù)結(jié)構(gòu)中坞古,該結(jié)構(gòu)與區(qū)塊一一對應(yīng)备韧。這個功能稱為日志功能,不需要實現(xiàn)事件(event)痪枫。在日志創(chuàng)建之后织堂,合約就不能訪問這些數(shù)據(jù)了。但是這些數(shù)據(jù)可以在區(qū)塊鏈之外的地方輕松訪問到奶陈。由于一些日志數(shù)據(jù)用bloom filter算法保存易阳,所以可以搞笑的安全的方式訪問到數(shù)據(jù),而且節(jié)點也不需要下載所有的區(qū)塊鏈(輕客戶端)吃粒,也能找到這些日志潦俺。
創(chuàng)建
合約可以用特殊的代碼(不是簡單的調(diào)用0地址)創(chuàng)建其他的合約。創(chuàng)建調(diào)用和其他一般的消息調(diào)用不同的一點是徐勃,創(chuàng)建調(diào)用的負(fù)載是可以執(zhí)行的事示,并且返回值是代碼,調(diào)用者(創(chuàng)建者)可以獲取到新合約的地址僻肖。
自我銷毀
代碼從區(qū)塊鏈中移除的唯一可能就是合約調(diào)用了selfdestruct
操作肖爵。該地址剩余的以太幣將會返回到設(shè)定的賬戶中。storage和代碼都從狀態(tài)中移除了臀脏。
警告:即便合同中不包含selfdistruct
代碼劝堪,它依舊可以通過delegatecall
或者callcode
來完成自毀
注意:除去老舊合同的功能也許會,也許不會出現(xiàn)在以太坊客戶端中揉稚。另外秒啦,檔案節(jié)點可以選擇是否永久保留代碼和storage。
注意:目前外部賬號不能移除