智能合約介紹

翻譯原文
date:20170612

一個簡單的智能合約

讓我們中最簡單的例子開始?,F(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.)。在這個例子中,setget函數(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ù)和變量的名稱相同。但是別擔心,編譯器會自動完成這個操作的,把兩者區(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ā)。當事件觸發(fā)的時候,監(jiān)聽函數(shù)將會接收到from,toamount參數(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)建人的addressmsg(還有txblock)是一個全局變量,包含了一些允許進入?yún)^(qū)塊鏈的屬性信息。msg.sender的值恒為函數(shù)調(diào)用者的address。
最后,這些函數(shù)實現(xiàn)了合約,并且可以被用戶使用來履行mintsend約定。如果mint被非創(chuàng)建者調(diào)用,那么不會進行任何操作。另一方面,send可以被其他任何有貨幣的用戶調(diào)用,來進行轉(zhuǎn)賬操作。需要注意的是,如果你通過這個合約給一個address轉(zhuǎn)賬,在區(qū)塊鏈瀏覽器中你在這個address中將看不到任何有關(guān)這個操作的信息。因為這些轉(zhuǎn)賬信息,余額信息都是存在這個特定的合約當中。通過使用事件,你可以輕易的實現(xiàn)該貨幣的區(qū)塊鏈瀏覽器,來記錄該貨幣的轉(zhuǎn)賬和余額信息。

區(qū)塊鏈基礎(chǔ)

區(qū)塊鏈對于程序員來說并不是難于理解的一個概念。因為很多難題(挖礦,哈希,橢圓曲線加密,p2p網(wǎng)絡(luò)等)都是為了提供一系列的功能和承諾。一旦你接受了這些功能,你就不會擔心難于理解技術(shù)問題了。難道你必須理解阿里云怎么提供服務(wù)之后才能使用它嗎?

交易(transactions)

一條區(qū)塊鏈是一個共享的交易數(shù)據(jù)庫。這意味著所有加入網(wǎng)絡(luò)的人都可以訪問數(shù)據(jù)。如果你想要對這個數(shù)據(jù)庫做些改變,那么你必須要進行能夠被別人認可的交易。交易這個詞說明了要么你完成改變數(shù)據(jù)(假設(shè)你要同時修改兩個值),要么什么也沒有發(fā)生。另外,當你的交易被數(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)心。會自動為你確認交易順序。交易寫入?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)建的時候確認(它根據(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ù)(負載)和以太幣。
如果目標賬號包含代碼,那么執(zhí)行代碼,二進制負載數(shù)據(jù)作為代碼的輸入。
如果目標賬號是0賬號(address為0),交易會生成一個新的合約。之前提到過的,合約賬號是由發(fā)送者的address和轉(zhuǎn)賬金額生成的,絕對是非0賬號。這類創(chuàng)建合約交易的負載成為EVM的字節(jié)碼并且執(zhí)行。執(zhí)行的輸出被永久保存在合約代碼中。這意味著,要創(chuàng)建一個合同,你并不發(fā)送合同的實際代碼,而是返回合同代碼的代碼。(ps:有點繞,可以看原文)

燃料(gas)

在創(chuàng)建的時候,每個交易都會收取特定的gas,這個目的在于,限制交易執(zhí)行的工作量并且對這個執(zhí)行交易收費。當EVM執(zhí)行交易,gas按照特定的規(guī)則緩慢的消耗掉了。
gas的價格在創(chuàng)建交易的時候就設(shè)定了,必須在交易之前支付gas價格 * gas數(shù)目的費用。如果執(zhí)行完畢之后還有g(shù)as剩余,會原路返回到賬戶中。
如果gas在任何時候消耗完畢(例如成了負數(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位的。當接觸到(讀取或者寫入)之前為觸及的內(nèi)存字的時候,內(nèi)存以256位的字長擴展。當擴展的時候,必須支付gas。所以內(nèi)存消耗越大,花費更大。
EVM不是一種存儲器機,而是一種堆棧機。所以所有的計算都在堆棧上執(zhí)行。最大的是1024個元素并包含256位的字。訪問stack的深度有如下的限制規(guī)則:可以將頂端下的16個棧中拷貝出一個元素放置在頂端,或者可以將頂端的元素與下面的16個元素中的任意一個互換。所有其他的操作將距離頂端的兩個(或者一個,或者多個,全看操作需要)取出計算,將結(jié)果放置在棧頂端。當然也可以將堆棧元素放置在storage或者內(nèi)存里。但是不移除棧頂?shù)臅r候是無法再繼續(xù)深入訪問其他元素的。

指令集

EVM的指令集一直保持最小化,來避免導(dǎo)致一致性問題的可能性。所有指令操作都基于基本的數(shù)據(jù)類型,256位的字長。允許算術(shù)操作,位操作,邏輯操作以及比較操作??梢詶l件跳轉(zhuǎn)和非條件跳轉(zhuǎn)。另外,合約可以訪問當前區(qū)塊的相關(guān)屬性,例如序號和時間戳。

消息調(diào)用(message call)

合約可以調(diào)用其他合約或者可以通過消息調(diào)用發(fā)送以太幣到非合約賬號。消息調(diào)用和jiao交易非常相似,有原始地址,目的地址,數(shù)據(jù)負載,以太幣,gas和返回值。實際上,每個交易都是由高級的消息調(diào)用組成。所謂高級,就是可以生成其他消息調(diào)用。
一個合約可以決定發(fā)送多少gas,保留多少。如果一個gas不足異常(或者其他異常)在內(nèi)部的消息調(diào)用中生成的時候,將會在堆棧頂標記一個錯誤的值。在這個例子中,當且僅當與消息一起發(fā)送的gas使用完畢時。在solidity中,調(diào)用合約引起一個認為異常,導(dǎo)致調(diào)用棧上升。(?理解的稀里糊涂,推薦看原文)
就像之前所說,被調(diào)用的合約(可以是調(diào)用者自身)能夠得到一塊嶄新的內(nèi)存,并且可以訪問到負載(在單獨的區(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)用就是說目的地址中的代碼在當前調(diào)用者的上下文中運行,msg.sendermsg.value不會改變,其他方面跟一般的消息調(diào)用一致。
這就意味著一個合約可以在運行時動態(tài)的從不同地址中調(diào)用代碼。storage,當前地址和余額都是引用當前的調(diào)用合約,僅僅只是代碼從調(diào)用地址中獲取的,其他的都是當前合約的。
這就可以實現(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)用的負載是可以執(zhí)行的,并且返回值是代碼,調(diào)用者(創(chuàng)建者)可以獲取到新合約的地址。

自我銷毀

代碼從區(qū)塊鏈中移除的唯一可能就是合約調(diào)用了selfdestruct操作。該地址剩余的以太幣將會返回到設(shè)定的賬戶中。storage和代碼都從狀態(tài)中移除了。

警告:即便合同中不包含selfdistruct代碼,它依舊可以通過delegatecall或者callcode來完成自毀

注意:除去老舊合同的功能也許會,也許不會出現(xiàn)在以太坊客戶端中。另外,檔案節(jié)點可以選擇是否永久保留代碼和storage。

注意:目前外部賬號不能移除

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容