天天看點

BPlayers談以太坊開發入門Solidity開發第一章

一個簡單的智能合約

先從一個非常基礎的例子開始,不用擔心你現在還一點都不了解,我們将逐漸了解到更多的細節。

存儲

contract SimpleStorage {

    uint storedData;

    function set(uint x) {

        storedData = x;

    }

    function get() constant returns (uint retVal) {

        return storedData;

    }

}

在Solidity中,一個合約由一組代碼(合約的函數)和資料(合約的狀态)組成。合約位于以太坊區塊鍊上的一個特殊位址。uint storedData; 這行代碼聲明了一個狀态變量,變量名為storedData,類型為 uint (256bits無符号整數)。你可以認為它就像資料庫裡面的一個存儲單元,跟管理資料庫一樣,可以通過調用函數查詢和修改它。在以太坊中,通常隻有合約的擁有者才能這樣做。在這個例子中,函數 set 和 get 分别用于修改和查詢變量的值。

BPlayers談以太坊開發入門Solidity開發第一章

跟很多其他語言一樣,通路狀态變量時,不需要在前面增加 this. 這樣的字首。

這個合約還無法做很多事情(受限于以太坊的基礎設施),僅僅是允許任何人儲存一個數字。而且世界上任何一個人都可以來存取這個數字,缺少一個(可靠的)方式來保護你釋出的數字。任何人都可以調用set方法設定一個不同的數字覆寫你釋出的數字。但是你的數字将會留存在區塊鍊的曆史上。稍後我們會學習如何增加一個存取限制,使得隻有你才能修改這個數字。

代币的例子

接下來的合約将實作一個形式最簡單的加密貨币。空中取币不再是一個魔術,當然隻有建立合約的人才能做這件事情(想用其他貨币發行模式也很簡單,隻是實作細節上的差異)。而且任何人都可以發送貨币給其他人,不需要注冊使用者名和密碼,隻要有一對以太坊的公私鑰即可。

注意對于線上solidity環境來說,這不是一個好的例子。如果你使用線上solidity環境 來嘗試這個例子。調用函數時,将無法改變from的位址。是以你隻能扮演鑄币者的角色,可以鑄造貨币并發送給其他人,而無法扮演其他人的角色。這點線上solidity環境将來會做改進。

contract Coin {

//關鍵字“public”使變量能從合約外部通路。

    address public minter;

    mapping (address => uint) public balances;

//事件讓輕用戶端能高效的對變化做出反應。

    event Sent(address from, address to, uint amount);

//這個構造函數的代碼僅僅隻在合約建立的時候被運作。

    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 bits,不支援任何算術操作。适用于存儲合約的位址或其他人的公私鑰。public關鍵字會自動為其修飾的狀态變量生成通路函數。沒有public關鍵字的變量将無法被其他合約通路。另外隻有本合約内的代碼才能寫入。自動生成的函數如下:

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

當然我們自己增加一個這樣的通路函數是行不通的。編譯器會報錯,指出這個函數與一個狀态變量重名。

下一行代碼mapping (address => uint) public balances; 建立了一個public的狀态變量,但是其類型更加的複雜。該類型将一些address映射到無符号整數。mapping可以被認為是一個哈希表,每一個可能的key對應的value被虛拟的初始化為全0.這個類比不是很嚴謹,對于一個mapping,無法擷取一個包含其所有key或者value的連結清單。是以我們得自己記着添加了哪些東西到mapping中。更好的方式是維護一個這樣的連結清單,或者使用其他更進階的資料類型。或者隻在不受這個缺陷影響的場景中使用mapping,就像這個例子。在這個例子中由public關鍵字生成的通路函數将會更加複雜,其代碼大緻如下:

function balances(address _account) returns (uint balance) {

    return balances[_account];

}

我們可以很友善的通過這個函數查詢某個特定賬号的餘額。

event Sent(address from, address to, uint value)這行代碼聲明了一個“事件”。由send函數的最後一行代碼觸發。用戶端(服務端應用也适用)可以以很低的開銷來監聽這些由區塊鍊觸發的事件。事件觸發時,監聽者會同時接收到from,to,value這些參數值,可以友善的用于跟蹤交易。為了監聽這個事件,你可以使用如下代碼:

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可以被任何人(擁有一定數量的代币)調用,發送一些币給其他人。注意,當你通過該合約發送一些代币到某個位址,在區塊鍊浏覽器中查詢該位址将什麼也看不到。因為發送代币導緻的餘額變化隻存儲在該代币合約的資料存儲中。通過事件我們可以很容易建立一個可以追蹤你的新币交易和餘額的“區塊鍊浏覽器”。

區塊鍊基礎

對于程式員來說,區塊鍊這個概念其實不難了解。因為最難懂的一些東西(挖礦,哈希,橢圓曲線加密,點對點網絡等等)隻是為了提供一系列的特性和保障。你隻需要接受這些既有的特性,不需要關心其底層的技術。就像你如果僅僅是為了使用亞馬遜的AWS,并不需要了解其内部工作原理。

交易/事務

區塊鍊是一個全局共享的,事務性的資料庫。這意味着參與這個網絡的每一個人都可以讀取其中的記錄。如果你想修改這個資料庫中的東西,就必須建立一個事務,并得到其他所有人的确認。事務這個詞意味着你要做的修改(假如你想同時修改兩個值)隻能被完完全全的實施或者一點都沒有進行。

此外,當你的事務被應用到這個資料庫的時候,其他事務不能修改該資料庫。

舉個例子,想象一張表,裡面列出了某個電子貨币所有賬号的餘額。當從一個賬戶到另外一個賬戶的轉賬請求發生時,這個資料庫的事務特性確定從一個賬戶中減掉的金額會被加到另一個賬戶上。如果因為某種原因,往目标賬戶上增加金額無法進行,那麼源賬戶的金額也不會發生任何變化。

此外,一個事務會被發送者(建立者)進行密碼學簽名。這項措施非常直覺的為資料庫的特定修改增加了通路保護。在電子貨币的例子中,一個簡單的檢查就可以確定隻有持有賬戶密鑰的人,才能從該賬戶向外轉賬。

區塊

區塊鍊要解決的一個主要難題,在比特币中被稱為“雙花攻擊”。當網絡上出現了兩筆交易,都要花光一個賬戶中的錢時,會發生什麼?一個沖突?

簡單的回答是你不需要關心這個問題。這些交易會被排序并打包成“區塊”,然後被所有參與的節點執行和分發。如果兩筆交易互相沖突,排序靠後的交易會被拒絕并剔除出區塊。

這些區塊按時間排成一個線性序列。這也正是“區塊鍊”這個詞的由來。區塊以一個相當規律的時間間隔加入到鍊上。對于以太坊,這個間隔大緻是17秒。

作為“順序選擇機制”(通常稱為“挖礦”)的一部分,一段區塊鍊可能會時不時被復原。但這種情況隻會發生在整條鍊的末端。復原涉及的區塊越多,其發生的機率越小。是以你的交易可能會被復原,甚至會被從區塊鍊中删除。但是你等待的越久,這種情況發生的機率就越小。

以太坊虛拟機

總覽

以太坊虛拟機(EVM)是以太坊中智能合約的運作環境。它不僅被沙箱封裝起來,事實上它被完全隔離,也就是說運作在EVM内部的代碼不能接觸到網絡、檔案系統或者其它程序。甚至智能合約與其它智能合約隻有有限的接觸。

賬戶

以太坊中有兩類賬戶,它們共用同一個位址空間。外部賬戶,該類賬戶被公鑰-私鑰對控制(人類)。合約賬戶,該類賬戶被存儲在賬戶中的代碼控制。

外部賬戶的位址是由公鑰決定的,合約賬戶的位址是在建立改合約時确定的(這個位址由合約建立者的位址和該位址發出過的交易數量計算得到,位址發出過的交易數量也被稱作"nonce")

合約賬戶存儲了代碼,外部賬戶則沒有,除了這點以外,這兩類賬戶對于EVM來說是一樣的。

每個賬戶有一個key-value形式的持久化存儲。其中key和value的長度都是256bit,名字叫做storage.

另外,每個賬戶都有一個以太币餘額(機關是“Wei"),該賬戶餘額可以通過向它發送帶有以太币的交易來改變。

交易

一筆交易是一條消息,從一個賬戶發送到另一個賬戶(可能是相同的賬戶或者零賬戶,見下文)。交易可以包含二進制資料(payload)和以太币。

如果目标賬戶包含代碼,該代碼會執行,payload就是輸入資料。

如果目标賬戶是零賬戶(賬戶位址是0),交易将建立一個新合約。正如上文所講,這個合約位址不是零位址,而是由合約建立者的位址和該位址發出過的交易數量(被稱為nonce)計算得到。建立合約交易的payload被當作EVM位元組碼執行。執行的輸出做為合約代碼被永久存儲。這意味着,為了建立一個合約,你不需要向合約發送真正的合約代碼,而是發送能夠傳回真正代碼的代碼。

Gas

以太坊上的每筆交易都會被收取一定數量的gas,gas的目的是限制執行交易所需的工作量,同時為執行支付費用。當EVM執行交易時,gas将按照特定規則被逐漸消耗。

gas price(gas價格,以太币計)是由交易建立者設定的,發送賬戶需要預付的交易費用 = gas price * gas amount。 如果執行結束還有gas剩餘,這些gas将被返還給發送賬戶。

無論執行到什麼位置,一旦gas被耗盡(比如降為負值),将會觸發一個out-of-gas異常。目前調用幀所做的所有狀态修改都将被復原。

存儲,主存和棧

每個賬戶有一塊持久化記憶體區域被稱為存儲。其形式為key-value,key和value的長度均為256比特。在合約裡,不能周遊賬戶的存儲。相對于另外兩種,存儲的讀操作相對來說開銷較大,修改存儲更甚。一個合約隻能對它自己的存儲進行讀寫。

第二個記憶體區被稱為主存。合約執行每次消息調用時,都有一塊新的,被清除過的主存。主存可以以位元組粒度尋址,但是讀寫粒度為32位元組(256比特)。操作主存的開銷随着其增長而變大(平方級别)。

EVM不是基于寄存器,而是基于棧的虛拟機。是以所有的計算都在一個被稱為棧的區域執行。棧最大有1024個元素,每個元素256比特。對棧的通路隻限于其頂端,方式為:允許拷貝最頂端的16個元素中的一個到棧頂,或者是交換棧頂元素和下面16個元素中的一個。所有其他操作都隻能取最頂的兩個(或一個,或更多,取決于具體的操作)元素,并把結果壓在棧頂。當然可以把棧上的元素放到存儲或者主存中。但是無法隻通路棧上指定深度的那個元素,在那之前必須要把指定深度之上的所有元素都從棧中移除才行。

指令集

EVM的指令集被刻意保持在最小規模,以盡可能避免可能導緻共識問題的錯誤實作。所有的指令都是針對256比特這個基本的資料類型的操作。具備常用的算術,位,邏輯和比較操作。也可以做到條件和無條件跳轉。此外,合約可以通路目前區塊的相關屬性,比如它的編号和時間戳。

消息調用

合約可以通過消息調用的方式來調用其它合約或者發送以太币到非合約賬戶。消息調用和交易非常類似,它們都有一個源,一個目标,資料負載,以太币,gas和傳回資料。事實上每個交易都可以被認為是一個頂層消息調用,這個消息調用會依次産生更多的消息調用。

一個合約可以決定剩餘gas的配置設定。比如内部消息調用時使用多少gas,或者期望保留多少gas。如果在内部消息調用時發生了out-of-gas異常(或者其他異常),合約将會得到通知,一個錯誤碼被壓在棧上。這種情況隻是内部消息調用的gas耗盡。在solidity中,這種情況下發起調用的合約預設會觸發一個人工異常。這個異常會列印出調用棧。

就像之前說過的,被調用的合約(發起調用的合約也一樣)會擁有嶄新的主存并能夠通路調用的負載。調用負載被存儲在一個單獨的被稱為calldata的區域。調用執行結束後,傳回資料将被存放在調用方預先配置設定好的一塊記憶體中。

調用層數被限制為1024,是以對于更加複雜的操作,我們應該使用循環而不是遞歸。

代碼調用和庫

存在一種特殊類型的消息調用,被稱為callcode。它跟消息調用幾乎完全一樣,隻是加載自目标位址的代碼将在發起調用的合約上下文中運作。

這意味着一個合約可以在運作時從另外一個位址動态加載代碼。存儲,目前位址和餘額都指向發起調用的合約,隻有代碼是從被調用位址擷取的。

這使得Solidity可以實作”庫“。可複用的庫代碼可以應用在一個合約的存儲上,可以用來實作複雜的資料結構。

日志

在區塊層面,可以用一種特殊的可索引的資料結構來存儲資料。這個特性被稱為日志,Solidity用它來實作事件。合約建立之後就無法通路日志資料,但是這些資料可以從區塊鍊外高效的通路。因為部分日志資料被存儲在布隆過濾器(Bloom filter) 中,我們可以高效并且安全的搜尋日志,是以那些沒有下載下傳整個區塊鍊的網絡節點(輕用戶端)也可以找到這些日志。

建立

合約甚至可以通過一個特殊的指令來建立其他合約(不是簡單的向零位址發起調用)。建立合約的調用跟普通的消息調用的差別在于,負載資料執行的結果被當作代碼,調用者/建立者在棧上得到新合約的位址。

自毀

隻有在某個位址上的合約執行自毀操作時,合約代碼才會從區塊鍊上移除。合約位址上剩餘的以太币會發送給指定的目标,然後其存儲和代碼被移除。

注意,即使一個合約的代碼不包含自毀指令,依然可以通過代碼調用(callcode)來執行這個操作。