Solidity編程 一 之智能合約的介紹


寫在前面:

  最新在學習以太坊相關的東西,Solidity是基礎,所以對 http://solidity.readthedocs.io/en/latest/installing-solidity.html 里的文章進行了翻譯。爭取這篇文檔都能完成翻譯。由於自己的英語水平有限,如果在翻譯的過程中有什么錯誤的,請留言,非常感謝!  

1. 一個智能合約的例子

   我們從一個基礎的solidity例子開始。開始的時候,你可能看不懂每一行具體的意思,但是沒關系,我們會在后續的講解中介紹每一個細節

 

   

Storage

pragma solidity ^0.4.0;
 
contract SimpleStorage {
    uint storedData;
 
    function set(uint x) {
        storedData = x;
    }
 
    function get() constant returns (uint) {
        return storedData;
    }
}

      第一行告訴該合約用的是0.4.0版本的solidity編寫,並且這些代碼具有向上兼容性。保證不會在不同solidity編譯版本下編譯會出現不同的行為。

  從Solidity角度來看,合約就是存在於以太坊區塊鏈中的一個特定地址中的代碼和數據集合。uint storedData 聲明了一個類型為 uint(256位的無符號整型)的變量,變量名稱為 storedData。你可以把它想象為數據庫中的一個字段,該字段是可以被數據庫中的方法進行查詢,修改。在以太坊中,這個字段是屬於一個合約字段。在這個例子中,該變量可以通過提供的get,set方法進行獲取或是修改。Solidity中,訪問一個變量是不需要通過this來引用的。

  這個合約很簡單,只是允許以太坊上的任何人來存儲一個數據到某個節點,同時把這個操作發布到以太坊中,當然,以太坊上的其他節點同樣可以通過調用set方法來修改你已經存儲好的值。雖然有被修改,但是對該值操作的任何歷史記錄都是保存在以太坊中的。不用擔心你的存儲記錄或是修改記錄會丟失。后面我們會將到如何對合約進行限制,只允許你一個人修改這個數據

 

2. 子貨幣例子

 

  下面的例子將實現一個簡單的加密貨幣例子。無中生幣不在是夢想,當然只有合約的創建人才有這個特權。此外,任何人只要有一個以太坊密鑰對就可以進行貨幣交易,根本不需要注冊用戶名和密碼。

 

  

pragma solidity ^0.4.0;
 
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; 

   聲明了一個public,類型為address的狀態變量。Address類型是一個160位的值,不允許任何的算術操作。它適合於存儲合約地址或是其他人的密鑰對。Public關鍵字會自動產生用於外部訪問該變量值的方法。如果不聲明public,其他的合約是無法訪問該變量的。自動產生的方法類似於:

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

   當然如果你增加了一個和上面完全一樣的方法是沒有任何作用的,我們需要變量和產生的方法名完全一致。這塊其實編譯器會幫助我們完成,不需要我們自己動手編寫,我們只要知道這個概念就可以。

 

  mapping (address => uint) public balances;

   還是創建了一個公有狀態變量,這是一個比address更復雜的數據類型,類似java里的Map<address,uint>,它描述了一個地址和一個uinit數據類型的map關系。Mappings的關系可以看成是一個hash表,所有可能的key都對應了一個0的值。當然在map里不存在只有key值或是只有value值的情況。所以我們需要記住添加了一個什么樣的map關系或是像這個例子一樣,如何使用它。因為這是個public變量,所以系統會自動為它生成一個get方法,類似於:

   function balances(address _account) returns (uint) {

        return balances[_account];

    }

 

  通過上面的方法我們可以很容易的查詢一個賬號的余額。

 

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

   這一行創建了一個名為event 的事件。該事件會在該示例的最后一行被觸發。用戶或是server應用可以花很低的代價(后面會講代價是什么)來監聽事件的觸發。一旦這個事件被觸發了,監聽者接收到三個參數: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是值該方法調用者的地址。

最后,真正完成合約功能的,並且被其他用戶調用的是 mintsend方法。如果mint是被不是創建該合約的賬號調用,不會起任何作用。但是,send可以被任何賬號(必須有以太幣的賬號)調用並發送以太幣給另外一個賬號。注意,如果你用合約發送以太幣到另外一個賬號,通過區塊鏈瀏覽器查看是查看不到任何變化的,因為發送以太幣的過程和金額的變化都被存儲在了特殊的以太幣合約里。而不是體現在賬號上。通過使用事件,可以很容易的創建一個區塊鏈瀏覽器,用來查看交易和賬號余額。

 

3. 區塊鏈基礎

 

   對於編程者來說,區塊鏈不是一個很難理解的概念。因為最難懂的那部分(包括挖礦,哈希,橢圓加密,p2p網絡)都只是提供了一系列的特性和約束。一旦你知道了這些特性和約束,你不必去理解這些特性或是約束背后的實現原理。

 

  交易

     區塊鏈是全局共享,交易數據庫。這就意味着任何人只要參與到這個網絡中就可以訪問到這個數據庫。如果你要修改數據庫中的數據,你需要創建一個被其他所有在這個網絡里的人所認可的交易。交易說明對數據的修改要么沒有任何進行要么就完全完成,不會出現部分完成,部分未完成的情況。而且一旦交易完成,被記錄在數據庫中,誰也無法修改這個交易。

     舉個例子,想象在一個電子貨幣里,用表列舉出所有賬號余額。當進行一個賬號和另外一個賬號進行交易,交易數據庫要確保交易金額要從交易發送方減去,並且在交易接收方增加同樣的交易金額。如果交易過程中出現了任何原因導致交易失敗,交易發送方增加金額的行為失敗,那接收方的金額也不應該發生變化。

     而且發送方都會對發起的交易進行簽名加密。這直接地保證了數據庫只能被指定的修改所修改。在電子貨幣的例子中,簡單的檢查能夠確保只有持有這個賬號的秘鑰者才能對金額進行轉移。

 

  區塊

     在比特幣術語中,一個比較難理解的是雙花攻擊問題:當網絡中的兩筆交易想同時清空同一個賬號的余額會發生什么?一個沖突?

     理論上,你不需要去關心這種情況。系統會為你選擇一個交易,這些交易會綁定在一個叫做區塊里,然后這些交易被執行並且分發到所有參與的節點。如果兩筆交易相互沖突,后面執行的交易會被拒絕,不會成為區塊的一部分。

      每一個區塊都會及時的用線性組織起來,這就是區塊鏈的最初來源。每一個區塊都在一個固定時間加入到區塊鏈中,對於以太坊來說,間隔為17秒。作為序列化選擇機制(被稱做挖礦)的一部分,可能會發生區塊反復回滾情況,但是這種情況只是出現在區塊鏈的末端。越多的區塊被加入到末端,回滾操作就會越少。所以你的交易可能被回滾或是從區塊鏈上被刪除,但是你只要等的時間越長,這種情況發生的就會越少。

 

4.  以太坊虛擬機

 

  概述

     以太坊虛擬機(EVM)是以太網上智能合約的運行環境。這不僅僅是個沙盒,更確實的是一個完全獨立的環境,也就是說代碼運行在EVM里是沒有網絡,文件系統或是其他進程的。智能合約甚至被限制訪問其他的智能合約

 

  賬號 

     在以太坊中有兩種賬號共享地址空間:外部賬號和合約賬號。外部賬號是由公鑰和私鑰控制的(如人),合約賬號是由賬號存儲的代碼所控制。

外部賬號的地址是由公鑰決定的,而合約地址是在智能合約被創建的時候決定的(這個地址由創建者的地址和發送方發送過來的交易數字衍生而來,這個數字通常被叫做“nonce”

    不管是否賬號存有代碼(合約賬號存儲了代碼,而外部賬號沒有),對於EVM來說這兩種賬號是相等的。

每一個賬號都有持久化存儲一個key和value長度都為256位字的鍵值對,被稱為“storage”

而且,在以太坊中,每個賬號都有一個余額(確切的是用“Wei”來作為基本單位)  ,該余額可以被發送方發送過來帶有以太幣的交易所更改。

 

  交易 

   交易是一個賬號和另外一個賬號之間的信息交換。它包含了二進制數據(消費數據)和以太數據。如果目標賬號包含了代碼,這個代碼一旦被執行,那么它的消費數據就會作為一個輸入數據。如果目標賬號是一個0賬號(地址為0的賬號),交易會生成一個新的合約。這個合約的地址不為0,但是是來源於發送方,之后這個賬號的交易數據會被發送。這個合約消費會被編譯為EVM的二進制代碼,並執行。這次的執行會被作為這個合約的代碼持久化。這就是說:為了創建一個合約,你不需要發送真正的代碼到這個合約上,事實上是代碼的返回作為合約代碼。

 

  Gas

      以太坊上的每筆進行一筆交易都會被收取一定數量的Gas.這是為了限制交易的數量,同時對每一筆交易的進行支付額外費用。當EVM執行一個交易,交易發起方就會根據定義的規則消耗對應的Gas。

  交易的創造者定義了的Gas 價格。所以交易發起方每次需要支付 gas_price * gas 。如果有gas在執行后有剩余,會以同樣的方法返回給交易發起方。

  如果gas在任何時候消耗完,out-of-gas 異常會被拋出,那當前的這邊交易所執行的后的狀態全部會被回滾到初始狀態。

 

  存儲,主存和棧

       每個賬號都有持久化的內存空間叫做存儲. 存儲是一個key和value長度都為256位的key-value鍵值對。從一個合約里列舉存儲是不大可能的。讀取存儲里的內容是需要一定的代價的,修改storage里的內容代價則會更大。一個合約只能讀取或是修改自己的存儲內容。

  第二內存區域叫做主存。系統會為每個消息的調用分配一個新的,被清空的主存空間。主存是線性並且以字節粒度尋址。讀的粒度為32字節(256位),寫可以是1個字節(8位)或是32個字節(256字節)。當訪問一個字(256位)內存時,主存會按照字的大小來擴展。主存擴展時候,消耗Gas也必須要支付,主存的開銷會隨着其增長而增大(指數增長)。

      EVM不是一個基於寄存器,而是基於棧的。所以所有的計算都是在棧中執行。最大的size為1024個元素,每個元素為256位的字。棧的訪問限於頂端,按照如下方式:允許拷貝最上面的16個元素中的一個到棧頂或是棧頂和它下面的16個元素中的一個進行交換。所有其他操作會從棧中取出兩個(有可能是1個,多個,取決於操作)元素,把操作結果在放回棧中。當然也有可能把棧中元素放入到存儲或是主存中,但是不可能在沒有移除上層元素的時候,隨意訪問下層元素。

 

  指令集

    為了避免錯誤的實現而導致的一致性問題,EVM的指令集保留最小集合。所有的指令操作都是基於256位的字。包含有常用的算術,位操作,邏輯操作和比較操作。條件跳轉或是非條件跳轉都是允許的。而且合約可以訪問當前區塊的相關屬性比如編號和時間戳。

 

  消息調用

        合約可以通過消息調用來實現調用其他合約或是發送以太幣到非合約賬號。消息調用和交易類似,他們都有一個源,一個目標,數據負載,以太幣,gas和返回的數據。事實上,每個交易都包含有一個頂層消息調用,這個頂層消息可以依次創建更多的消息調用。

 

       一個合約可以定義內部消息調用需要消耗多少gas,多少gas需要被保留。如果在內部消息調用中出現out-of-gas異常,合約會被通知,會在棧里用一個錯誤值來標記。這種情況只是這次調用的gas被消耗完。在Solidity,這種情況下調用合約會引起一個人為異常,這種異常會拋出棧的信息。

 

      上面提到,調用合約會被分配到一個新的,並且是清空的主存,並能訪問調用的負載。調用負載時被稱為calldata的一個獨立區域。調用結束后,返回一個存儲在調用主存空間里的數據。這個存儲空間是被調用者預先分配好的。調用限制的深度為1024.對於更加復雜的操作,我們更傾向於使用循環而不是遞歸。

 

  代理調用/ 代碼調用和庫

    存在一種特殊的消息調用,叫做代理調用。除了目標地址的代碼在調用方的上下文中被執行,而且msg.sendermsg.value不會改變他們的值,其他都和消息調用一樣。這就意味着合約可以在運行時動態的加載其他地址的代碼。存儲,當前地址,余額都和調用合約有關系。只有代碼是從被調用方中獲取。這就使得我們可以在Solidity中使用庫。比如為了實現復雜的數據結構,可重用的代碼可以應用於合約存儲中。

 

  日志

   我們可以把數據存儲在一個特殊索引的數據結構中。這個結構映射到區塊層面的各個地方。為了實現這個事件,在Solidity把這個特性稱為日志。合約在被創建出來后是不可以訪問日志數據的。但是他們可以從區塊鏈外面有效的訪問這些數據。因為日志的部分數據是存儲在bloom filters上。我們可以用有效並且安全加密的方式來查詢這些數據。即使不用下載整個區塊鏈數據(輕客戶端)也能找到這些日志

 

  創建

   合約可以通過特殊的指令來創建其他合約。這些創建調用指令和普通的消息調用唯一區別是:負載數據被執行,結果作為代碼被存儲,調用者在棧里收到了新合約的地址。

 

  自毀

    從區塊鏈中移除代碼的唯一方法是合約在它的地址上執行了selfdestruct操作。這個賬號下剩余的以太幣會發送給指定的目標,存儲和代碼從棧中刪除。

    

 


 

 歡迎大家關注微信號:蝸牛講技術。掃下面的二維碼

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM