寫在前面:HiBlock區塊鏈社區成立了翻譯小組(以太坊中文社區),翻譯區塊鏈相關的技術文檔及資料,本文爲solidity官方文檔翻譯的第一部分《智能合約概述》,特發佈出來邀請solidity愛好者、開發者作公開的審校,您能夠添加微信baobaotalk_com,驗證輸入「solidity」,而後將您的意見和建議發送給咱們,也能夠在文末「留言」區留言,有效的建議咱們會採納及合併進下一版本,同時將送一份小禮物給您以示感謝。html
讓咱們先看一下最基本的例子。如今就算你都不理解也沒關係,後面咱們會有更深刻的講解。程序員
存儲數據庫
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 ,傳入不一樣的值,覆蓋你的數字,可是這個數字仍會被存儲在區塊鏈的歷史記錄中。隨後,咱們會看到怎樣施加訪問限制,以確保只有你才能改變這個數字。
子貨幣(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 函數可被任何人用於向他人發送幣 (固然,前提是發送者擁有這些幣)。記住,若是你使用合約發送幣給一個地址,當你在區塊鏈瀏覽器上查看該地址時是看不到任何相關信息的。由於,實際上你發送幣和更改餘額的信息僅僅存儲在特定合約的數據存儲器中。經過使用事件,你能夠很是簡單地爲你的新幣建立一個「區塊鏈瀏覽器」來追蹤交易和餘額。
對於程序員來講,區塊鏈這個概念並不難理解,這是由於大多數難懂的東西 (挖礦, 哈希 ,橢圓曲線密碼學 ,點對點網絡(P2P) 等) 都只是用於提供特定的功能和承諾。你只需接受這些既有的特性功能,沒必要關心底層技術,好比,難道你必須知道亞馬遜的 AWS 內部原理,你才能使用它嗎?
交易/事務
區塊鏈是全球共享的事務性數據庫,這意味着每一個人均可加入網絡來閱讀數據庫中的記錄。若是你想改變數據庫中的某些東西,你必須建立一個被全部其餘人所接受的事務。事務一詞意味着你想作的(假設您想要同時更改兩個值),要麼一點沒作,要麼所有完成。此外,當你的事務被應用到數據庫時,其餘事務不能修改數據庫。
舉個例子,設想一張表,列出電子貨幣中全部帳戶的餘額。若是請求從一個帳戶轉移到另外一個帳戶,數據庫的事務特性確保了若是從一個帳戶扣除金額,它總被添加到另外一個帳戶。若是因爲某些緣由,沒法添加金額到目標帳戶時,源帳戶也不會發生任何變化。
此外,交易老是由發送人(建立者)簽名。
這樣,就可很是簡單地爲數據庫的特定修改增長訪問保護機制。 在電子貨幣的例子中,一個簡單的檢查能夠確保只有持有帳戶密鑰的人才能從中轉帳。
區塊
在比特幣中,要解決的一個主要難題,被稱爲「雙花攻擊 (double-spend attack)」:若是網絡存在兩筆交易,都想花光同一個帳戶的錢時(即所謂的衝突)會發生什麼狀況?交易互相沖突?
簡單的回答是你沒必要在意此問題。網絡會爲你自動選擇一條交易序列,並打包到所謂的「區塊」中,而後它們將在全部參與節點中執行和分發。若是兩筆交易互相矛盾,那麼最終被確認爲後發生的交易將被拒絕,不會被包含到區塊中。
這些塊按時間造成了一個線性序列,這正是「區塊鏈」這個詞的來源。區塊以必定的時間間隔添加到鏈上 —— 對於以太坊,這間隔大約是17秒。
做爲「順序選擇機制」(也就是所謂的「挖礦」)的一部分,可能會發生這樣的狀況:塊不時地被回滾,但只發生在區塊鏈的「末端」。在末端涉及回滾區塊越多,其發生的機率越小。因此你的交易有可能被回滾,甚至從區塊鏈中抹除,但你等待的時間越長,這種狀況發生的機率就越小。
概述
以太坊虛擬機 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 異常。當前調用幀所作的全部狀態修改都將被回滾。
存儲,內存和棧
每一個帳戶有一塊持久化內存區被稱爲 存儲。 存儲是一個 key-value 的鍵值對 ,其存儲着一個由256位的鍵到256位的值的映射. 在合約中,不能枚舉戶中的存儲,且存儲的讀操做相對開銷高,修改存儲開銷更高。一個合約只能對它本身的存儲進行讀寫。
第二個內存區稱爲 內存,合約會試圖爲每一次消息調用獲取一塊被從新擦拭乾淨的內存實例。 內存是線性的,可按字節級尋址,但讀的長度被限制爲256位,而寫的長度能夠是8位或256位。當訪問(不管是讀仍是寫)以前從未訪問過的內存字(word)時(不管是偏移到該字內的任何位置),內存將按字進行擴展(每一個字是256 bit)。擴容也將消耗必定的gas。 內存越大,費用就越高(平方級別)。
EVM 不是基於寄存器的,而是基於棧的,所以全部的計算都在一個被稱爲 stack 的區域執行。 棧最大有1024個元素,每一個元素長度是一個字(256 bit)。對棧的訪問只限於其頂端,限制方式爲:容許拷貝最頂端的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 。合約帳戶上剩餘的以太幣會發送給指定的目標,而後其存儲和代碼從狀態中被移除。
注:本文爲solidity翻譯的第一部分《智能合約概述》,特發佈出來邀請solidity愛好者、開發者作公開的審校,您能夠添加微信baobaotalk_com,驗證輸入「solidity」,而後將您的意見和建議發送給咱們,也可在文末「留言」區留言,或經過原文連接訪問咱們的Github。有效的建議咱們會收納並及時改進,同時將送一份小禮物給您以示感謝。
點擊「閱讀原文」便可查看翻譯原文