區塊鏈技術:智慧合約入門

梅芬發表於2018-02-25
目錄

什麼是智慧合約

一個智慧合約是一套以數字形式定義的承諾(promises) ,包括合約參與方可以在上面執行這些承諾的協議。一個合約由一組程式碼(合約的函式)和資料(合約的狀態)組成,並且執行在以太坊虛擬機器上.

以太坊虛擬機器(EVM)使用了256位元長度的機器碼,是一種基於堆疊的虛擬機器,用於執行以太坊智慧合約 。由於EVM是針對以太坊體系設計的,因此使用了以太坊賬戶模型(Account Model)進行價值傳輸

合約的程式碼具有什麼能力:
讀取交易資料。
讀取或寫入合約自己的儲存空間。
讀取環境變數【塊高,雜湊值,gas】
向另一個合約傳送一個“內部交易”。
複製程式碼
在區塊鏈平臺的架構

區塊鏈平臺的架構

1. 什麼是solidity

Solidity是一種智慧合約高階語言,執行在Ethereum虛擬機器(EVM)之上。

solidity 語言特點

它的語法接近於Javascript,是一種物件導向的語言。但作為一種真正意義上執行在網路上的去中心合約,它有很多的不同點:

  • 異常機制,類似於事務的原子性。一旦出現異常,所有的執行都將會被回撤,這主要是為了保證合約執行的原子性,以避免中間狀態出現的資料不一致。
  • 執行環境是在去中心化的網路上,會比較強調合約或函式執行的呼叫的方式。因為原來一個簡單的函式呼叫變為了一個網路上的節點中的程式碼執行
  • 儲存是使用網路上的區塊鏈,資料的每一個狀態都可以永久儲存。

2. 開發的工具

3 快速入門

3.1 舉個例子

完整的步驟:

1. 寫合約
2. 編譯合約
3. 部署合約
4. 測試合約
複製程式碼

獲取例子

參考操作步驟
$ git clone "https://github.com/cristicmf/bcos-qucik-start-demo"
$ cd startDemo
$ npm install
$ babel-node index.js
複製程式碼
檔案結構說明
startDemo
├── README.md
├── SimpleStartDemo.sol  # 合約程式碼
├── codeUtils.js
├── config.js  # 配置檔案
├── index.js   # 部署合約和測試合約
├── output     # abi/bin/address的輸出
│   ├── StartDemo.abi
│   ├── StartDemo.address
│   └── StartDemo.bin
├── package.json
├── sha3.js
└── web3sync.js
複製程式碼

詳細程式碼


    pragma solidity ^0.4.2;
    
    contract SimpleStartDemo {
        int256 storedData;
        event AddMsg(address indexed sender, bytes32 msg);
    
        modifier only_with_at_least(int x) {
           if (x >= 5) {
             x = x+10;
              _;
           }
        } 
        function SimpleStartDemo() {
            storedData = 2;
        }
    
        function setData(int256 x) public only_with_at_least(x){
            storedData = x;
            AddMsg(msg.sender, "[in the set() method]");
        }
    
        function getData() constant public returns (int256 _ret) {
            AddMsg(msg.sender, "[in the get() method]");
            return _ret = storedData;
        }
    }

複製程式碼

3.2 部署合約

舉個例子get demo

$ babel-node index.js
複製程式碼

1. 編譯合約

    execSync("solc --abi  --bin   --overwrite -o " + config.Ouputpath + "  " + filename + ".sol");
複製程式碼

2. 部署合約到區塊鏈上

   var Contract = await web3sync.rawDeploy(config.account, config.privKey, filename);
複製程式碼

3. 對合約進行讀寫

    var address = fs.readFileSync(config.Ouputpath + filename + '.address', 'utf-8');
    var abi = JSON.parse(fs.readFileSync(config.Ouputpath /*+filename+".sol:"*/ + filename + '.abi', 'utf-8'));
    var contract = web3.eth.contract(abi);
    var instance = contract.at(address);
    //獲取鏈上資料
    var data = instance.getData();
    //修改鏈上資料
    var func = "setData(int256)";
    var params = [10];
    var receipt = await web3sync.sendRawTransaction(config.account, config.privKey, address, func, params);
複製程式碼

3.2.1 引入概念:

address:以太坊地址的長度,大小20個位元組,160位,所以可以用一個uint160編碼。地址是所有合約的基礎,所有的合約都會繼承地址物件,也可以隨時將一個地址串,得到對應的程式碼進行呼叫。合約的地址是基於賬號隨機數和交易資料的雜湊計算出來的

ABI:是以太坊的一種合約間呼叫時或訊息傳送時的一個訊息格式。就是定義操作函式簽名,引數編碼,返回結果編碼等。

交易:以太坊中“交易”是指儲存從外部賬戶發出的訊息的簽名資料包。

簡單理解是:只要對區塊鏈進行寫操作,一定會發生交易。

交易回執:

發生交易後的返回值

3.2.2 擴充套件閱讀:

3.3 合約檔案結構簡介

1. 版本宣告

pragma solidity ^0.4.10;
複製程式碼

1. 引用其它原始檔

import “filename”;//全域性引入
複製程式碼

1. 狀態變數(State Variables)

    int256 storedData;
複製程式碼

詳細說明見下文

2. 函式(Functions)

   function setData(int256 x) public {
         storedData = x;
         AddMsg(msg.sender, "[in the set() method]");
     }
   
    function getData() constant public returns (int256 _ret) {
        return _ret = storedData;
     }
複製程式碼

3. 事件(Events)

   //事件的宣告
   event AddMsg(address indexed sender, bytes32 msg);
   //事件的使用
   function setData(int256 x) public {
         storedData = x;
         AddMsg(msg.sender, "in the set() method");
     }
複製程式碼

4. 結構型別(Structs Types)

   contract Contract {
     struct Data {
       uint deadline;
       uint amount;
     }
     Data data;
     function set(uint id, uint deadline, uint amount) {
       data.deadline = deadline;
       data.amount = amount;
     }
   }
複製程式碼

5. 函式修飾符(Function Modifiers)

類似於hook modifier only_with_at_least(int x) { if (x >= 5) { x = x+10; _; } }

4. 合約程式設計模式COP

面向條件的程式設計(COP)是面向合約程式設計的一個子域,作為一種面向函式和指令式程式設計的混合模式。COP解決了這個問題,通過需要程式設計師顯示地列舉所有的條件。邏輯變得扁平,沒有條件的狀態變化。條件片段可以被正確的文件化,複用,可以根據需求和實現來推斷。重要的是,COP在程式設計中把預先條件當作為一等公民。這樣的模式規範能保證合約的安全。

4.1 FEATURES

  • 函式主體沒有條件判斷

例子:

contract Token {
    // The balance of everyone
    mapping (address => uint) public balances;
    // Constructor - we're a millionaire!
    function Token() {
        balances[msg.sender] = 1000000;
    }
    // Transfer `_amount` tokens of ours to `_dest`.
    function transfer(uint _amount, address _dest) {
        balances[msg.sender] -= _amount;
        balances[_dest] += _amount;
    }
}
複製程式碼

改進後:

function transfer(uint _amount, address _dest) {
    if (balances[msg.sender] < _amount)
        return;
    balances[msg.sender] -= _amount;
    balances[_dest] += _amount;
}
複製程式碼

COP的風格

modifier only_with_at_least(uint x) {
    if (balances[msg.sender] >= x) _;
}

function transfer(uint _amount, address _dest)
only_with_at_least(_amount) {
    balances[msg.sender] -= _amount;
    balances[_dest] += _amount;
}
複製程式碼

擴充套件閱讀:

5. 語法介紹

基礎語法見官方API

5.1 值型別


  • 布林(Booleans) true false 支援的運算子

    !邏輯非 && 邏輯與 || 邏輯或 == 等於 != 不等於

  • 整型(Integer) int/uint:變長的有符號或無符號整型。變數支援的步長以8遞增,支援從uint8到uint256,以及int8到int256。需要注意的是,uint和int預設代表的是uint256和int256

  • 地址(Address): 以太坊地址的長度,大小20個位元組,160位,所以可以用一個uint160編碼。地址是所有合約的基礎,所有的合約都會繼承地址物件,也可以隨時將一個地址串,得到對應的程式碼進行呼叫

  • 定長位元組陣列(fixed byte arrays)

  • 有理數和整型(Rational and Integer Literals,String literals)

  • 列舉型別(Enums)

  • 函式(Function Types)

5.2 引用型別(Reference Types)

  • 不定長位元組陣列(bytes)
  • 字串(string) bytes3 a = "123";
  • 陣列(Array)
  • 結構體(Struts)

6. 重要概念

6.1 Solidity的資料位置

資料位置的型別

變數的儲存位置屬性。有三種型別,memory,storage和calldata。

  • memory儲存位置同我們普通程式的記憶體類似。即分配,即使用,越過作用域即不可被訪問,等待被回收-
  • storage的變數,資料將永遠存在於區塊鏈上。
  • calldata 資料位置比較特殊,一般只有外部函式的引數(不包括返回引數)被強制指定為calldata

Storage - 狀態變數的儲存模型

大小固定的變數(除了對映,變長陣列以外的所有型別)在儲存(storage)中是依次連續從位置0開始排列的。如果多個變數佔用的大小少於32位元組,會盡可能的打包到單個storage槽位裡,具體規則如下:

  • 在storage槽中第一項是按低位對齊儲存(lower-order aligned)
  • 基本型別儲存時僅佔用其實際需要的位元組。
  • 如果基本型別不能放入某個槽位餘下的空間,它將被放入下一個槽位。
  • 結構體和陣列總是使用一個全新的槽位,並佔用整個槽(但在結構體內或陣列內的每個項仍遵從上述規則)

優化建議:

為了方便EVM進行優化,嘗試有意識排序storage的變數和結構體的成員,從而讓他們能打包得更緊密。比如,按這樣的順序定義,uint128, uint128, uint256,而不是uint128, uint256, uint128。因為後一種會佔用三個槽位。

Memory - 記憶體變數的佈局(Layout in Memory)

Solidity預留了3個32位元組大小的槽位:

0-64:雜湊方法的暫存空間(scratch space)

64-96:當前已分配記憶體大小(也稱空閒記憶體指標(free memory pointer))

暫存空間可在語句之間使用(如在內聯編譯時使用)

Solidity總是在空閒記憶體指標所在位置建立一個新物件,且對應的記憶體永遠不會被釋放(也許未來會改變這種做法)。

有一些在Solidity中的操作需要超過64位元組的臨時空間,這樣就會超過預留的暫存空間。他們就將會分配到空閒記憶體指標所在的地方,但由於他們自身的特點,生命週期相對較短,且指標本身不能更新,記憶體也許會,也許不會被清零(zerod out)。因此,大家不應該認為空閒的記憶體一定已經是清零(zeroed out)的。

例子

6.2 address

以太坊地址的長度,大小20個位元組,160位,所以可以用一個uint160編碼。地址是所有合約的基礎,所有的合約都會繼承地址物件,也可以隨時將一個地址串,得到對應的程式碼進行呼叫

6.3 event

event AddMsg(address indexed sender, bytes32 msg);

  • 這行程式碼宣告瞭一個“事件”。客戶端(服務端應用也適用)可以以很低的開銷來監聽這些由區塊鏈觸發的事件

事件是使用EVM日誌內建功能的方便工具,在DAPP的介面中,它可以反過來呼叫Javascript的監聽事件的回撥。

var event = instance.AddMsg({}, function(error, result) {
        if (!error) {
            var msg = "AddMsg: " + utils.hex2a(result.args.msg) + " from "
            console.log(msg);
            return;
        } else {
            console.log('it error')
        }
    });
複製程式碼
  • 事件在合約中可被繼承。當被呼叫時,會觸發引數儲存到交易的日誌中(一種區塊鏈上的特殊資料結構)。這些日誌與合約的地址關聯,併合併到區塊鏈中,只要區塊可以訪問就一直存在(至少Frontier,Homestead是這樣,但Serenity也許也是這樣)。日誌和事件在合約內不可直接被訪問,即使是建立日誌的合約。
  • 日誌位置在nodedir0/log 裡面,可以打出特殊的型別進行驗證

6.4 陣列

陣列是定長或者是變長陣列。有length屬性,表示當前的陣列長度。

  1. bytes:類似於byte[], 動態長度的位元組陣列

  2. string:類似於bytes,動態長度的UTF-8編碼的字元型別

  3. bytes1~bytes32

    一般使用定長的 bytes1~bytes32。在知道字串長度的情況下,指定長度時,更加節省空間。

6.4.1 建立陣列

  1. 字面量 uint[] memory a = []

  2. new uint[] memory a = new uint; 例子

    pragma solidity ^0.4.0;
    
    contract SimpleStartDemo{
      uint[] stateVar;
    
      function f(){
       //定義一個變長陣列
        uint[] memory memVar;
    
        //不能在使用new初始化以前使用
        //VM Exception: invalid opcode
        //memVar [0] = 100;
    
        //通過new初始化一個memory的變長陣列
        memVar = new uint[](2);
        
        //不能在使用new初始化以前使用
        //VM Exception: invalid opcode
        //stateVar[0] = 1;
        
        //通過new初始化一個storage的變長陣列
        stateVar = new uint[](2);
        stateVar[0] = 1;
      }
    }
    複製程式碼

6.4.2 陣列的屬性和方法

length屬性


storage變長陣列是可以修改length

memory變長陣列是不可以修改length

複製程式碼

push方法



storage變長陣列可以使用push方法

bytes可以使用push方法

複製程式碼

例子

pragma solidity ^0.4.2;

contract SimpleStartDemo {
  uint[] stateVar;

  function f() returns (uint){
    //在元素初始化前使用
    stateVar.push(1);

    stateVar = new uint[](1);
    stateVar[0] = 0;
    //自動擴充長度
     uint pusharr = stateVar.push(1);
     uint len = stateVar.length;
    //不支援memory
    //Member "push" is not available in uint256[] memory outside of storage.
    //uint[] memory memVar = new uint[](1);
    //memVar.push(1);

    return len;
  }
}
複製程式碼

下標:和其他語言類似

6.4.3 Memory陣列

  1. 如果Memory陣列作為函式的引數傳遞,只能支援ABI能支援的型別型別。

  2. Memory陣列是不能修改修改陣列大小的屬性 例子

    pragma solidity ^0.4.2;

    contract SimpleStartDemo { function f() { //建立一個memory的陣列 uint[] memory a = new uint;

         //不能修改長度
         //Error: Expression has to be an lvalue.
         //a.length = 100;
     }
     
     //storage
     uint[] b;
     
     function g(){
         b = new uint[](7);
         //可以修改storage的陣列
         b.length = 10;
         b[9] = 100;
     }
    複製程式碼

    }

EVM的限制

由於EVM的限制,不能通過外部函式直接返回動態陣列和多維陣列

  1. 將stroage陣列不能直接返回,需要轉換成memory型別的返回
      //Data層資料
      struct Rate {
      		int key1;
            int unit;
            uint[3] exDataArr;
            bytes32[3] exDataStr;
        }
    
        mapping(int =>Rate) Rates;
     function getRate(int key1) public constant returns(int,uint[3],bytes32[3]) {
            uint[3] memory exDataInt = Rates[key1].exDataArr;
            bytes32[3] memory exDataStr = Rates[key1].exDataStr;
            return (Rates[key1].unit,exDataInt,exDataStr);
        }
複製程式碼

業務場景

6.5 函式

function () {internal(預設)|external} constant [returns ()]

6.5.1 函式的internal與external

例子

pragma solidity ^0.4.5;

contract FuntionTest{
    function internalFunc() internal{}

    function externalFunc() external{}

    function callFunc(){
        //直接使用內部的方式呼叫
        internalFunc();

        //不能在內部呼叫一個外部函式,會報編譯錯誤。
        //Error: Undeclared identifier.
        //externalFunc();

        //不能通過`external`的方式呼叫一個`internal`
        //Member "internalFunc" not found or not visible after argument-dependent lookup in contract FuntionTest
        //this.internalFunc();

        //使用`this`以`external`的方式呼叫一個外部函式
        this.externalFunc();
    }
}
contract FunctionTest1{
    function externalCall(FuntionTest ft){
        //呼叫另一個合約的外部函式
        ft.externalFunc();
        
        //不能呼叫另一個合約的內部函式
        //Error: Member "internalFunc" not found or not visible after argument-dependent lookup in contract FuntionTest
        //ft.internalFunc();
    }
}
複製程式碼

訪問函式有外部(external)可見性。如果通過內部(internal)的方式訪問,比如直接訪問,你可以直接把它當一個變數進行使用,但如果使用外部(external)的方式來訪問,如通過this.,那麼它必須通過函式的方式來呼叫。

例子

    pragma solidity ^0.4.2;
    
    contract SimpleStartDemo {
        uint public c = 10;
        
        function accessInternal() returns (uint){
            return c;
        }
        
        function accessExternal() returns (uint){
            return this.c();
        }
    }
複製程式碼

6.5.2 函式呼叫

  • 內部呼叫,不會建立一個EVM呼叫,也叫訊息呼叫
  • 外部呼叫,建立EVM呼叫,會發起訊息呼叫

6.5.3 函式修改器(Function Modifiers)

修改器(Modifiers)可以用來輕易的改變一個函式的行為。比如用於在函式執行前檢查某種前置條件。修改器是一種合約屬性,可被繼承,同時還可被派生的合約重寫(override)

例子

    pragma solidity ^0.4.2;
    
    contract SimpleStartDemo {
        int256 storedData;
        event AddMsg(address indexed sender, bytes32 msg);
    
        modifier only_with_at_least(int x) {
           if (x >= 5) {
             x = x+10;
              _;
           }
        } 
        function setData(int256 x) public only_with_at_least(x){
            storedData = x;
            AddMsg(msg.sender, "[in the set() method]");
        }
    }

複製程式碼

6.5.4合約建構函式 同名函式

  • 可選
  • 僅能有一個構造器
  • 不支援過載

6.6 Constant

函式也可被宣告為常量,這類函式將承諾自己不修改區塊鏈上任何狀態。

一般從鏈上獲取資料時,get函式都會加上constant

6.7 繼承(Inheritance)

Solidity通過複製包括多型的程式碼來支援多重繼承。

父類

    pragma solidity ^0.4.4;
    
    contract Meta {
        string  public name;
        string  public abi;
        address metaAddress;
    
        function Meta(string n,string a){
            name=n;
            abi=a;
        }
       
        function getMeta()public constant returns(string,string,address){
            return (name,abi,metaAddress);
        }
       
        function setMetaAddress(address meta) public {
            metaAddress=meta;
        }    
    }
複製程式碼

子類

    pragma solidity ^0.4.4;
    
    import "Meta.sol";
    contract Demo is Meta{
        bytes32 public orgID; 
    
        function Demo (string n,string abi,bytes32 id) Meta(n,abi)
        {
        	orgID = id;
        }
    }
複製程式碼
最簡單的合約架構

7. 限制

基於EVM的限制,不能通過外部函式返回動態的內容
複製程式碼

please keep in mind

- Fail as early and loudly as possible
- Favor pull over push payments
- Order your function code: conditions, actions, interactions
- Be aware of platform limits
- Write tests
- Fault tolerance and Automatic bug bounties
- Limit the amount of funds deposited
- Write simple and modular code
- Don’t write all your code from scratch
- Timestamp dependency: Do not use timestamps in critical parts of the code, because miners can manipulate them
- Call stack depth limit: Don’t use recursion, and be aware that any call can fail if stack depth limit is reached
- Reentrancy: Do not perform external calls in contracts. If you do, ensure that they are the very last thing you do
複製程式碼

8. 語言本身存在的痛點

1. ABI支援的型別有限,難以返回複雜的結構體型別。
2. Deep Stack的問題
3. 難以除錯,只能靠event log ,進行合約的除錯
4. 合約呼叫合約只能使用定長陣列
複製程式碼

9. 合約架構

合約架構分層

最簡單的架構

1->1合約架構圖

合約的架構分兩層資料合約和邏輯合約
model:資料合約
controller:邏輯合約
這樣分層的原因,是方便後期合約的升級。
複製程式碼

獲取更多合約架構詳情

truffle框架

優勢

大家都用它,簡單易用,生態相對於其他合約框架更加全面

功能

  • 一鍵初始化開發合約的專案(包含配置)
  • 合約編譯
  • 合約部署
  • 合約測試
  • 合約debug【可借鑑】

upgrade smart contract

10. 參考資料

11.相關名詞解釋:

  1. 以太坊合約的程式碼是使用低階的基於堆疊的位元組碼的語言寫成的,被稱為“以太坊虛擬機器程式碼”或者“EVM程式碼”。程式碼由一系列位元組構成,每一個位元組代表一種操作。
  2. UTXO:比特幣系統的“狀態”是所有已經被挖出的、沒有花費的比特幣(技術上稱為“未花費的交易輸出,unspent transaction outputs 或UTXO”)的集合。每個UTXO都有一個面值和所有者(由20個位元組的本質上是密碼學公鑰的地址所定義[1])。一筆交易包括一個或多個輸入和一個或多個輸出。每個輸入包含一個對現有UTXO的引用和由與所有者地址相對應的私鑰建立的密碼學簽名。每個輸出包含一個新的加入到狀態中的UTXO。
  3. 區塊鏈:區塊鏈起源於中本聰的比特幣,作為比特幣的底層技術,本質上是一個去中心化的資料庫。是指通過去中心化和去信任的方式集體維護一個可靠資料庫的技術方案。

相關文章