智能合約概述

原文的翻譯,並運行了例子上的合約

存儲

pragma solidity ^0.4.0;

contract SimpleStorage {
    uint storedData;

    function set(uint x) public {
        storedData = x;
    }

    function get() public constant returns (uint) {
        return storedData;
    }
}

第一行就是告訴大家源代碼使用Solidity版本0.4.0寫的,並且使用0.4.0以上版本運行也沒問題(最高到0.5.0,但是不包含0.5.0)。這是爲了確保合約不會在新的編譯器版本中突然行爲異常。關鍵字 pragma 的含義是,一般來說,pragmas(編譯指令)是告知編譯器如何處理源代碼的指令的(例如, pragma once )。

Solidity中合約的含義就是一組代碼(它的 函數 )和數據(它的 狀態 ),它們位於以太坊區塊鏈的一個特定地址上。 代碼行 uint storedData; 聲明一個類型爲 uint (256位無符號整數)的狀態變量,叫做 storedData 。 你可以認爲它是數據庫裏的一個位置,可以通過調用管理數據庫代碼的函數進行查詢和變更。對於以太坊來說,上述的合約就是擁有合約(owning contract)。在這種情況下,函數 set 和 get 可以用來變更或取出變量的值。

要訪問一個狀態變量,並不需要像 this. 這樣的前綴,雖然這是其他語言常見的做法。

該合約能完成的事情並不多(由於以太坊構建的基礎架構的原因):它能允許任何人在合約中存儲一個單獨的數字,並且這個數字可以被世界上任何人訪問,且沒有可行的辦法阻止你發佈這個數字。當然,任何人都可以再次調用 set ,傳入不同的值,覆蓋你的數字,但是這個數字仍會被存儲在區塊鏈的歷史記錄中。隨後,我們會看到怎樣施加訪問限制,以確保只有你才能改變這個數字。

我們在Mist客戶端運行一下:

點擊錢包 -> 合約 -> 部署新合約,運行一下。私有鏈上的話要啓動挖礦miner.start()

選擇set函數,輸入數字值


Get函數變成10了。



註解

所有的標識符(合約名稱,函數名稱和變量名稱)都只能使用ASCII字符集。UTF-8編碼的數據可以用字符串變量的形式存儲。

警告

小心使用Unicode文本,因爲有些字符雖然長得相像(甚至一樣),但其字符碼是不同的,其編碼後的字符數組也會不一樣。

子貨幣(Subcurrency)例子

下面的合約實現了一個最簡單的加密貨幣。這裏,幣確實可以無中生有地產生,但是隻有創建合約的人才能做到(實現一個不同的發行計劃也不難)。而且,任何人都可以給其他人轉幣,不需要註冊用戶名和密碼 —— 所需要的只是以太坊密鑰對。

pragma solidity ^0.4.21;

contract Coin {
    // 關鍵字“public”讓這些變量可以從外部讀取
    address public minter;
    mapping (address => uint) public balances;

    // 輕客戶端可以通過事件針對變化作出高效的反應
    event Sent(address from, address to, uint amount);

    // 這是構造函數,只有當合約創建時運行
    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;
        emit Sent(msg.sender, receiver, amount);
    }
}

這個合約引入了一些新的概念,讓我們逐一解讀。

address public minter; 這一行聲明瞭一個可以被公開訪問的 address 類型的狀態變量。 address 類型是一個160位的值,且不允許任何算數操作。這種類型適合存儲合約地址或外部人員的密鑰對。關鍵字 public 自動生成一個函數,允許你在這個合約之外訪問這個狀態變量的當前值。如果沒有這個關鍵字,其他的合約沒有辦法訪問這個變量。由編譯器生成的函數的代碼大致如下所示:

function minter() returns (address) { return minter; }

當然,加一個和上面完全一樣的函數是行不通的,因爲我們會有同名的一個函數和一個變量,這裏,主要是希望你能明白——編譯器已經幫你實現了。

下一行, mapping (address => uint) public balances; 也創建一個公共狀態變量,但它是一個更復雜的數據類型。 該類型將address映射爲無符號整數。 Mappings 可以看作是一個 哈希表 它會執行虛擬初始化,以使所有可能存在的鍵都映射到一個字節表示爲全零的值。 但是,這種類比並不太恰當,因爲它既不能獲得映射的所有鍵的列表,也不能獲得所有值的列表。 因此,要麼記住你添加到mapping中的數據(使用列表或更高級的數據類型會更好),要麼在不需要鍵列表或值列表的上下文中使用它,就如本例。 而由 public 關鍵字創建的getter函數 getter function 則是更復雜一些的情況, 它大致如下所示:

function balances(address _account) public view returns (uint) {
    return balances[_account];
}

正如你所看到的,你可以通過該函數輕鬆地查詢到賬戶的餘額。

event Sent(address from, address to, uint amount); 這行聲明瞭一個所謂的“事件(event)”,它會在 send 函數的最後一行被髮出。 用戶界面(當然也包括服務器應用程序)可以監聽區塊鏈上正在發送的事件,而不會花費太多成本。一旦它被髮出,監聽該事件的listener都將收到通知。而所有的事件都包含了 from , to 和 amount 三個參數,可方便追蹤事務。 爲了監聽這個事件,你可以使用如下代碼:

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));
    }
})

這裏請注意自動生成的 balances 函數是如何從用戶界面調用的。

特殊函數 Coin 是在創建合約期間運行的構造函數,不能在事後調用。 它永久存儲創建合約的人的地址: msg (以及 tx 和 block ) 是一個神奇的全局變量,其中包含一些允許訪問區塊鏈的屬性。 msg.sender 始終是當前(外部)函數調用的來源地址。

最後,真正被用戶或其他合約所調用的,以完成本合約功能的方法是 mint 和 send。 如果 mint被合約創建者外的其他人調用則什麼也不會發生。 另一方面, send 函數可被任何人用於向他人發送幣 (當然,前提是發送者擁有這些幣)。記住,如果你使用合約發送幣給一個地址,當你在區塊鏈瀏覽器上查看該地址時是看不到任何相關信息的。因爲,實際上你發送幣和更改餘額的信息僅僅存儲在特定合約的數據存儲器中。通過使用事件,你可以非常簡單地爲你的新幣創建一個“區塊鏈瀏覽器”來追蹤交易和餘額。

我們來運行一下:



copy一個賬戶地址,執行一下函數mint函數,餘額變爲10了



區塊鏈基礎

對於程序員來說,區塊鏈這個概念並不難理解,這是因爲大多數難懂的東西 (挖礦, 哈希 ,橢圓曲線密碼學 ,點對點網絡(P2P) 等) 都只是用於提供特定的功能和承諾。你只需接受這些既有的特性功能,不必關心底層技術,比如,難道你必須知道亞馬遜的 AWS 內部原理,你才能使用它嗎?

交易/事務

區塊鏈是全球共享的事務性數據庫,這意味着每個人都可加入網絡來閱讀數據庫中的記錄。如果你想改變數據庫中的某些東西,你必須創建一個被所有其他人所接受的事務。事務一詞意味着你想做的(假設您想要同時更改兩個值),要麼一點沒做,要麼全部完成。此外,當你的事務被應用到數據庫時,其他事務不能修改數據庫。

舉個例子,設想一張表,列出電子貨幣中所有賬戶的餘額。如果請求從一個賬戶轉移到另一個賬戶,數據庫的事務特性確保瞭如果從一個賬戶扣除金額,它總被添加到另一個賬戶。如果由於某些原因,無法添加金額到目標賬戶時,源賬戶也不會發生任何變化。

此外,交易總是由發送人(創建者)簽名。

這樣,就可非常簡單地爲數據庫的特定修改增加訪問保護機制。 在電子貨幣的例子中,一個簡單的檢查可以確保只有持有賬戶密鑰的人才能從中轉賬。

區塊

在比特幣中,要解決的一個主要難題,被稱爲“雙花攻擊 (double-spend attack)”:如果網絡存在兩筆交易,都想花光同一個賬戶的錢時(即所謂的衝突)會發生什麼情況?交易互相沖突?

簡單的回答是你不必在乎此問題。網絡會爲你自動選擇一條交易序列,並打包到所謂的“區塊”中,然後它們將在所有參與節點中執行和分發。如果兩筆交易互相矛盾,那麼最終被確認爲後發生的交易將被拒絕,不會被包含到區塊中。

這些塊按時間形成了一個線性序列,這正是“區塊鏈”這個詞的來源。區塊以一定的時間間隔添加到鏈上 —— 對於以太坊,這間隔大約是17秒。

作爲“順序選擇機制”(也就是所謂的“挖礦”)的一部分,可能有時會發生塊(blocks)被回滾的情況,但僅在鏈的“末端”。末端增加的塊越多,其發生回滾的概率越小。因此你的交易被回滾甚至從區塊鏈中抹除,這是可能的,但等待的時間越長,這種情況發生的概率就越小。

以太坊虛擬機

概述

以太坊虛擬機 EVM 是智能合約的運行環境。它不僅是沙盒封裝的,而且是完全隔離的,也就是說在 EVM 中運行代碼是無法訪問網絡、文件系統和其他進程的。甚至智能合約之間的訪問也是受限的。

賬戶

以太坊中有兩類賬戶(它們共用同一個地址空間): 外部賬戶 由公鑰-私鑰對(也就是人)控制; 合約賬戶 由和賬戶一起存儲的代碼控制.

外部賬戶的地址是由公鑰決定的,而合約賬戶的地址是在創建該合約時確定的(這個地址通過合約創建者的地址和從該地址發出過的交易數量計算得到的,也就是所謂的“nonce”)

無論帳戶是否存儲代碼,這兩類賬戶對 EVM 來說是一樣的。

每個賬戶都有一個鍵值對形式的持久化存儲。其中 key 和 value 的長度都是256位,我們稱之爲 存儲 。

此外,每個賬戶有一個以太幣餘額( balance )(單位是“Wei”),餘額會因爲發送包含以太幣的交易而改變。

交易

交易可以看作是從一個帳戶發送到另一個帳戶的消息(這裏的賬戶,可能是相同的或特殊的零帳戶,請參閱下文)。它能包含一個二進制數據(合約負載)和以太幣。

如果目標賬戶含有代碼,此代碼會被執行,並以 payload 作爲入參。

如果目標賬戶是零賬戶(賬戶地址爲 0 ),此交易將創建一個 新合約 。 如前文所述,合約的地址不是零地址,而是通過合約創建者的地址和從該地址發出過的交易數量計算得到的(所謂的“nonce”)。 這個用來創建合約的交易的 payload 會被轉換爲 EVM 字節碼並執行。執行的輸出將作爲合約代碼被永久存儲。這意味着,爲創建一個合約,你不需要向合約發送真正的合約代碼,而是發送能夠產生真正代碼的代碼。

Gas

一經創建,每筆交易都收取一定數量的 gas ,目的是限制執行交易所需要的工作量和爲交易支付手續費。EVM 執行交易時,gas 將按特定規則逐漸耗盡。

gas price 是交易發送者設置的一個值,發送者賬戶需要預付的手續費= gas_price * gas 。如果交易執行後還有剩餘, gas 會原路返還。

無論執行到什麼位置,一旦 gas 被耗盡(比如降爲負值),將會觸發一個 out-of-gas 異常。當前調用幀(call frame)所做的所有狀態修改都將被回滾。

譯者注:調用幀(call frame),指的是下文講到的EVM的運行棧(stack)中當前操作所需要的若干元素。

存儲,內存和棧

每個賬戶有一塊持久化內存區稱爲 存儲 。 存儲是將256位字映射到256位字的鍵值存儲區。 在合約中枚舉存儲是不可能的,且讀存儲的相對開銷很高,修改存儲的開銷甚至更高。合約只能讀寫存儲區內屬於自己的部分。

第二個內存區稱爲 內存 ,合約會試圖爲每一次消息調用獲取一塊被重新擦拭乾淨的內存實例。 內存是線性的,可按字節級尋址,但讀的長度被限制爲256位,而寫的長度可以是8位或256位。當訪問(無論是讀還是寫)之前從未訪問過的內存字(word)時(無論是偏移到該字內的任何位置),內存將按字進行擴展(每個字是256位)。擴容也將消耗一定的gas。 隨着內存使用量的增長,其費用也會增高(以平方級別)。

EVM 不是基於寄存器的,而是基於棧的,因此所有的計算都在一個被稱爲 棧(stack) 的區域執行。 棧最大有1024個元素,每個元素長度是一個字(256位)。對棧的訪問只限於其頂端,限制方式爲:允許拷貝最頂端的16個元素中的一個到棧頂,或者是交換棧頂元素和下面16個元素中的一個。所有其他操作都只能取最頂的兩個(或一個,或更多,取決於具體的操作)元素,運算後,把結果壓入棧頂。當然可以把棧上的元素放到存儲或內存中。但是無法只訪問棧上指定深度的那個元素,除非先從棧頂移除其他元素。

指令集

EVM的指令集量應儘量少,以最大限度地避免可能導致共識問題的錯誤實現。所有的指令都是針對"256位的字(word)"這個基本的數據類型來進行操作。具備常用的算術、位、邏輯和比較操作。也可以做到有條件和無條件跳轉。此外,合約可以訪問當前區塊的相關屬性,比如它的編號和時間戳。

消息調用

合約可以通過消息調用的方式來調用其它合約或者發送以太幣到非合約賬戶。消息調用和交易非常類似,它們都有一個源、目標、數據、以太幣、gas和返回數據。事實上每個交易都由一個頂層消息調用組成,這個消息調用又可創建更多的消息調用。

合約可以決定在其內部的消息調用中,對於剩餘的 gas ,應發送和保留多少。如果在內部消息調用時發生了out-of-gas異常(或其他任何異常),這將由一個被壓入棧頂的錯誤值所指明。此時,只有與該內部消息調用一起發送的gas會被消耗掉。並且,Solidity中,發起調用的合約默認會觸發一個手工的異常,以便異常可以從調用棧裏“冒泡出來”。 如前文所述,被調用的合約(可以和調用者是同一個合約)會獲得一塊剛剛清空過的內存,並可以訪問調用的payload——由被稱爲 calldata 的獨立區域所提供的數據。調用執行結束後,返回數據將被存放在調用方預先分配好的一塊內存中。 調用深度被 限制 爲 1024 ,因此對於更加複雜的操作,我們應使用循環而不是遞歸。

委託調用/代碼調用和庫

有一種特殊類型的消息調用,被稱爲 委託調用(delegatecall) 。它和一般的消息調用的區別在於,目標地址的代碼將在發起調用的合約的上下文中執行,並且 msg.sender 和 msg.value 不變。 這意味着一個合約可以在運行時從另外一個地址動態加載代碼。存儲、當前地址和餘額都指向發起調用的合約,只有代碼是從被調用地址獲取的。 這使得 Solidity 可以實現”庫“能力:可複用的代碼庫可以放在一個合約的存儲上,如用來實現複雜的數據結構的庫。

日誌

有一種特殊的可索引的數據結構,其存儲的數據可以一路映射直到區塊層級。這個特性被稱爲 日誌(logs) ,Solidity用它來實現 事件(events) 。合約創建之後就無法訪問日誌數據,但是這些數據可以從區塊鏈外高效的訪問。因爲部分日誌數據被存儲在 布隆過濾器(Bloom filter) 中,我們可以高效並且加密安全地搜索日誌,所以那些沒有下載整個區塊鏈的網絡節點(輕客戶端)也可以找到這些日誌。

創建

合約甚至可以通過一個特殊的指令來創建其他合約(不是簡單的調用零地址)。創建合約的調用 create calls 和普通消息調用的唯一區別在於,負載會被執行,執行的結果被存儲爲合約代碼,調用者/創建者在棧上得到新合約的地址。

自毀

合約代碼從區塊鏈上移除的唯一方式是合約在合約地址上的執行自毀操作 selfdestruct 。合約賬戶上剩餘的以太幣會發送給指定的目標,然後其存儲和代碼從狀態中被移除。

警告

儘管一個合約的代碼中沒有顯式地調用 selfdestruct ,它仍然有可能通過 delegatecall 或 callcode 執行自毀操作。

註解

舊合約的刪減可能會,也可能不會被以太坊的各種客戶端程序實現。另外,歸檔節點可選擇無限期保留合約存儲和代碼。

註解

目前, 外部賬戶 不能從狀態中移除。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章