什么是智能合約
一個智能合約是一套以數字形式定義的承諾(promises) ,包括合約參與方可以在上面執行這些承諾的協議。一個合約由一組代碼(合約的函數)和數據(合約的狀態)組成,並且運行在以太坊虛擬機上.
以太坊虛擬機(EVM)使用了256比特長度的機器碼,是一種基於堆棧的虛擬機,用於執行以太坊智能合約 。由於EVM是針對以太坊體系設計的,因此使用了以太坊賬戶模型(Account Model)進行價值傳輸。
合約的代碼具有什么能力:
讀取交易數據。
讀取或寫入合約自己的存儲空間。
讀取環境變量(塊高,哈希值,gas) 向另一個合約發送一個“內部交易”。
1. 什么是solidity
Solidity是一種智能合約高級語言,運行在Ethereum虛擬機(EVM)之上。
solidity 語言特點
它的語法接近於Javascript,是一種面向對象的語言。但作為一種真正意義上運行在網絡上的去中心合約,它有很多的不同點:
- 異常機制,類似於事務的原子性。一旦出現異常,所有的執行都將會被回撤,這主要是為了保證合約執行的原子性,以避免中間狀態出現的數據不一致。
- 運行環境是在去中心化的網絡上,會比較強調合約或函數執行的調用的方式。因為原來一個簡單的函數調用變為了一個網絡上的節點中的代碼執行
- 存儲是使用網絡上的區塊鏈,數據的每一個狀態都可以永久存儲。
2. 開發的工具
- 在線編譯器Remix
- Visual Studio Code + solidity 插件
- WeBase 開發工具
3 快速入門
准備工作
3.1 開發合約
合約開發步驟:
1. 寫合約 2. 編譯合約 3. 部署合約 4. 測試合約 5. 生成java文件
說明:WeBase
幫助用戶開發、測試和生成對應的Java類,用戶獲取java類,可以直接進行業務開發,加快開發進度和效率。
3.1.1. 獲取合約例子
pragma solidity ^0.4.24; contract HelloWorld { string name; function HelloWorld() { name = "Hello, World!"; } function get()constant returns(string) { return name; } function set(string n) { name = n; } }
3.1.2. 部署合約到區塊鏈上
- 編譯合約
- 編譯合約
- 調用get方法
- 調用set方法
- 檢查調用情況
3.1.4. 生成Java
3.1.5 應用開發
-
下載應用腳手架
$ git clone https://github.com/FISCO-BCOS/spring-boot-starter.git
- 導入文件進行配置開始開發
[更多工具和例子]()
3.2.1 引入概念:
address:以太坊地址的長度,大小20個字節,160位,所以可以用一個uint160編碼。地址是所有合約的基礎,所有的合約都會繼承地址對象,也可以隨時將一個地址串,得到對應的代碼進行調用。合約的地址是基於賬號隨機數和交易數據的哈希計算出來的
ABI:是以太坊的一種合約間調用時或消息發送時的一個消息格式。就是定義操作函數簽名,參數編碼,返回結果編碼等。
交易:以太坊中“交易”是指存儲從外部賬戶發出的消息的簽名數據包。
簡單理解是:只要對區塊鏈進行寫操作,一定會發生交易。
交易回執:發生交易后的返回值
3.2.2 擴展閱讀:
3.3 合約文件結構簡介
版本聲明
pragma solidity ^0.4.24;
狀態變量(State Variables)
string name;
詳細說明見下文
函數(Functions)
function get()constant returns(string) { return name; } function set(string n) { name = n; }
事件(Events)
//事件的聲明 event AddMsg(address indexed sender, bytes32 msg); //事件的使用 function setData(int256 x) public { storedData = x; AddMsg(msg.sender, "in the set() method"); }
結構類型(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; } }
函數修飾符(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. 語法介紹
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)
- 數組(Array)
- 結構體(Struts)
更多詳情見官方API
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屬性,表示當前的數組長度。
- bytes:類似於byte[], 動態長度的字節數組
- string:類似於bytes,動態長度的UTF-8編碼的字符類型
- bytes1~bytes32
一般使用定長的 bytes1~bytes32。在知道字符串長度的情況下,指定長度時,更加節省空間。
6.4.1 創建數組
- 字面量 uint[] memory a = []
-
new uint[] memory a = new uint[](7);
例子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數組
- 如果Memory數組作為函數的參數傳遞,只能支持ABI能支持的類型類型。
-
Memory數組是不能修改修改數組大小的屬性
例子pragma solidity ^0.4.2;
contract SimpleStartDemo {
function f() { //創建一個memory的數組 uint[] memory a = new uint[](7); //不能修改長度 //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的限制,不能通過外部函數直接返回動態數組和多維數組
- 將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 (<parameter types>) {internal(默認)|external} constant [returns (<return types>)]
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. 語言本身存在的痛點
- ABI支持的類型有限,難以返回復雜的結構體類型。
- Deep Stack的問題
- 難以調試,只能靠event log ,進行合約的調試
- 合約調用合約只能使用定長數組
9. 合約架構
9.1. 合約架構分層
實用架構示意圖
合約的架構分兩層數據合約和邏輯合約,方便后期合約的升級。更多詳情,請參見淺談以太坊智能合約的設計模式與升級方法 。