R-PoS是一種有多項最佳化創新的混合共識演算法。Ultrain透過吸收改良VRF隨機演算法,做出大規模節點的委員會成員選舉,並借鑑了PoS的Stake機制來增強整個共識演算法的安全性和穩定性,同時結合了BFT具備快速最終確定特性的共識演算法,針對BFT進行了大量工程上的最佳化,最終研發出R-PoS共識機制。
主側鏈隨機排程技術
企業客戶透過使用信任計算服務,可以大幅降低其商業環境中的信任成本,重構其原有的商業模式,實現收入的高速增長。從全球範圍來看,Ultrain是唯一一個可以為企業客戶提供一站式信任計算商業服務的專案,也是唯一一個同時解決了高TPS、高成本和資料安全問題的專案。
Ultrain透過領先的技術研發與生態拓展能力打造可持續的、良性健康的商業模式,形成閉環。
三、環境搭建
Ultrain平臺環境可以分為線下開發環境、線上測試網環境與線上主網環境。
其中,線上主網環境需要購買資源套餐後方可使用,線上測試網環境可經水龍頭程式自行充值後使用,線下開發環境是指在本地自行構建的網路共識環境。以下篇幅重點介紹線下開發環境的搭建流程。
· 系統推薦
我們推薦首選的作業系統為MacOS,你將獲得最佳的開發體驗,其次是Linux與Windows。如果你在Windows下開發遇到一些相容性問題,請參考Windows下Dapp開發攻略 (https://developer.ultrain.io/tutorial/windows_develop)。
· Longclaw
Ultrain線下開發環境藉助於Docker來構建,所以需要你在本機上提前安裝並啟動Docker。關於Docker安裝及使用可以參考阮一峰的Docker入門教程 (http://www.ruanyifeng.com/blog/2018/02/docker-tutorial.html)。
我們使用整合工具Longclaw來構建本地共識網路環境。首先在開發者網站上下載相應系統的Longclaw
(https://developer.ultrain.io/tools),安裝並啟動它。
注意:Longclaw第一次初始化環境可能要花費幾分鐘,需要你耐心等待,當出現以下介面,則說明Longclaw已經成功幫你構建了共識網路環境,也就是本地開發環境。
· 測試賬號
Longclaw為開發者預設建立了八個測試賬號,這些賬號擁有無限的資源使用權。當然如果你透過程式介面自行建立了賬號,則預設是沒有可用資源的,需要呼叫購買資源套餐的介面購買資源。相關文件參考與鏈互動一章中U3.js的對應介面用法說明。
注意:如果在Linux與Window下,Longclaw因不相容問題,導致不能正常啟動,建議直接連線線上測試網環境進行開發,相關配置可參考教程測試公鏈開發配置指南 (https://developer.ultrain.io/tutorial/testnet_guide)
四、工具使用
當有了可用的開發環境後,接下來就可以使用Robin framework建立一個DApp了。Robin framework是一個NodeJS編寫的全域性命令列程式介面。
它提供以下服務:
· 一鍵式合約初始化、編譯與部署;
· 自動化合約測試與開發;
· 友好的程式碼審查與錯誤提示;
· 大量的合約模板與示例參考;
· 指令碼式與可配置化部署流程;
· 互動式合約日誌控制檯輸出;
要求
NodeJS 8.0+
Linux、MacOSX、Windows
安裝
sudo npm install -g robin-framework
建立工程
執行robinorrobin -h來檢視所有的robin子命令
要啟動專案,首先需要建立一個新的空目錄,然後進入目錄:
mkdir testing cd testing
然後初始化一個專案。使用-cor--contract來指定名稱。此時,你有多個模板可以選擇,預設的是純合約專案,其餘的是帶介面的DApp框架。
robin init
合約專案目錄結構:
|--build // built WebAssembly targets
|--contract // contract source files
|--migrations // assign built files location and account who will deploy the contract
|--templates // some contract templates that will guide you
|--test // test files
|--config.js // configuration
...
語法檢查
在robin-lint的幫助下,藉助定製的tslint專案,您將找到錯誤和警告,然後快速修復它們。 只需進入專案的根目錄並執行:robin lint
編譯合約
依賴於ultrascript,合約原始檔將會被編譯為WebAsssembly目標檔案: *.abi, *.wast, *.wasm. 只需進入專案的根目錄並執行:robin build
部署合約
更新配置檔案config.js和migrate.js, 確保你已正確連線上一個Ultrain節點。如果你正在使用longclaw初始化的本地環境,那麼使用預設配置即可。也可以是你定製的節點。 只需進入專案的根目錄並執行:robin deploy
測試合約
參考測試目錄下*.spec.js檔案, 編寫測試用例來覆蓋你的合約中的所有用例場景。Robin提供給你一些測試工具類,比如mocha,chai,u3.jsandu3-utils, 尤其是用在處理非同步測試,只需進入專案的根目錄並執行:robin test
整合UI
如果你想將一個合約專案升級為帶介面的DAPP專案, 使用UI子命令。你有多個框架可以選擇,它們分別是vue-boilerplate、react-boilerplate和react-native-boilerplate。只需進入專案的根目錄並執行:robin ui
五、與鏈互動
U3.js是用JavaScript封裝的負責與鏈互動的通用庫。而Robin framework引用了U3.js,藉助它的介面實現了合約的deploy上鍊。
應用環境
瀏覽器(ES6)或 NodeJS
如果你想整合u3.js到react native環境中,有一個可行的方法,藉助rn-nodeify實現,參考示例U3RNDemo(https://github.com/benyasin/U3RNDemo)
使用方法
一、如果是在瀏覽器中使用u3,請參考以下用法:
test
二、 如果是在NodeJS環境中使用u3,請參照以下用法:
· 安裝u3
npm install u3.js或yarn add u3.js
· 初始化
const { createU3 } = require('u3.js/src');
let config = {
httpEndpoint: 'http://127.0.0.1:8888',
httpEndpoint_history: 'http://127.0.0.1:3000',
chainId: '0eaaff4003d4e08a541332c62827c0ac5d96766c712316afe7ade6f99b8d70fe',
keyProvider: ['PrivateKeys...'],
broadcast: true,
sign: true
}
let u3 = createU3(config);
u3.getChainInfo((err, info) =>{
if (err) {throw err;}
console.log(info);
});
配置
全域性配置
· httpEndpoint string - 鏈實時API的http或https地址。如果是在瀏覽器環境中使用u3,請注意配置相同的域名。
· httpEndpoint_history string - 鏈歷史API的http或https地址。如果是在瀏覽器環境中使用u3,請注意配置相同的域名。
· chainId 鏈唯一的ID。鏈ID可以透過 [httpEndpoint]/v1/chain/get_chain_info獲得。
· keyProvider [array|string|function] - 提供私鑰用來簽名交易,提供用於簽名事務的私鑰。 如果提供了多個私鑰,不能確定使用哪個私鑰,可以使用呼叫get_required_keysAPI 獲取要使用簽名的金鑰。如果是函式,那麼每一個交易都將會使用該函式。如果這裡不提供keyProvider,那麼它可能會Options配置項提供在每一個action或每一個transaction中。
· expireInSeconds number - 事務到期前的秒數,時間基於nodultrain的時間。
· broadcast [boolean=true] - 預設是true。使用true將交易釋出到區塊鏈,使用false將獲取簽名的事務。
· verbose [boolean=false] - 預設是false。詳細日誌記錄。
· debug [boolean=false] - 預設是false。低階除錯日誌記錄。
· sign [boolean=true] - 預設是true。使用私鑰簽名交易,保留未簽名的交易避免了提供私鑰的需要。
· logger
· 預設日誌配置。
logger: {
log: config.verbose ? console.log : null, // 如果值為null,則禁用日誌
error: config.verbose ? console.error : null,
}
Options配置項
Options可以在方法引數之後新增,Authorization應用於單獨的actions。比如:
options = {
authorization: 'admin@chaindaily',
broadcast: true,
sign: true
}
u3.transfer('alice', 'bob', '1.0000 UGAS', '', options)
· authorization [array|auth] - 指明賬號和許可權,典型地應用於多重簽名的配置中。Authorization必須是一個字串格式,形如admin@chaindaily。
· broadcast [boolean=true] - 預設是true。使用true將交易釋出到區塊鏈,使用false將獲取簽名的事務。
· sign [boolean=true] - 預設是true。使用私鑰簽名交易。保留未簽名的交易避免了提供私鑰的需要。
· keyProvider [array|string|function] - 就像global配置項中的keyProvider一樣,這裡的配置可以以覆蓋全域性配置的形式為每一個action或每一個transaction提供單獨的私鑰。
await u3.anyAction('args', {keyProvider})
await u3.transaction(tr ={
tr.anyAction()}, {keyProvider}
)
建立賬號
建立賬號需要花費creator賬號的一些代幣,為新賬號抵押部分RAM和頻寬。
const u3 = createU3(config);
const name = 'abcdefg12345';//普通賬號需要滿足規則:必須為12345abcdefghijklmnopqrstuvwxyz中的12位
let params = {
creator: 'ben',
name: name,
owner: pubkey,
active: pubkey,
updateable: 1,//可選,賬號是否可以更新(更新合約)
};
await u3.createUser(params);
轉賬
轉賬方法使用非常頻繁,UGAS的轉賬需要呼叫系統合約utrio.token。
const u3 = createU3(config);
const c = await u3.contract('utrio.token')
// 使用位置引數
await c.transfer('ben', 'bob', '1.2000 UGAS', '')
// 使用名稱引數
await c.transfer({from: 'bob', to: 'ben', quantity: '1.3000 UGAS', memo: ''})
簽名
使用{ sign: false, broadcast: false }建立一個u3例項並且做一些action, 然後將未簽名的交易傳送到錢包中。
const u3_offline = createU3({ sign: false, broadcast: false });
const c = u3_offline.contract('utrio.token');
let unsigned_transaction = await c.transfer('ultrainio', 'ben', '1 UGAS', 'uu');
在錢包中你可以提供私鑰或助記詞來簽名,並將簽名後的交易傳送到鏈上。
const u3_online = createU3();
let signature = await u3_online.sign(unsigned_transaction, privateKeyOrMnemonic, chainId);
if (signature) {
let signedTransaction = Object.assign({}, unsigned_transaction.transaction, { signatures: [signature] });
let processedTransaction = await u3_online.pushTx(signedTransaction);
}
資源
呼叫合約只會消耗合約Owner的資源,所以如果你想部署一個合約,請先購買一些資源。
· resourcelease(payer,receiver,slot,days)
const u3 = createU3(config);
const c = await u3.contract('ultrainio')
await c.resourcelease('ben', 'bob', 1, 10);// 1 slot for 10 days
透過以下方法查詢資源詳情。
const resource = await u3.queryResource('abcdefg12345');
console.log(resource)
合約
部署合約
部署合約需要提供包含目標檔案為.abi,.wast,*.wasm 的三個檔案的資料夾.
· deploy(contracts_files_path, deploy_account) 第一個引數為合約目標檔案的絕對路徑,第二個合約部署者賬號。
const u3 = createU3(config);
await u3.deploy(path.resolve(__dirname, '../contracts/token/token'), 'bob');
呼叫合約
const u3 = createU3(config);
const c = await u3.contract('ben');
await c.transfer('bob', 'ben', '1.0000 UGAS','');
//或者像這樣呼叫
await u3.contract('ben').then(sm =>
sm.transfer('bob', 'ben', '1.0000 UGAS','')
)
// 一筆交易也可以包含多個合約中的多個action
await u3.transaction(['ben', 'bob'], ({sm1, sm2}) =>{
sm1.myaction(..)
sm2.myaction(..)
})
發行代幣
const u3 = createU3(config);
const account = 'bob';
await u3.transaction(account, token =>{
token.create(account, '10000000.0000 DDD');
token.issue(account, '10000000.0000 DDD', 'issue');
});
const balance = await u3.getCurrencyBalance(account, account, 'DDD')
console.log('currency balance', balance)
事件
Ultrain提供了一個事件註冊監聽機制用來解決非同步場景下業務需求.客戶端首先訂閱一個事件,提供一個用來接收訊息的地址,當合約中的某個方法觸發時,該地址會收到來自鏈的推送訊息。
訂閱/取消訂閱
· registerEvent(deployer, listen_url)
· unregisterEvent(deployer, listen_url)
deployer: 合約的部署者賬號
listen_url: 接收訊息的地址
注意: 如果你是在本地docker環境中使用改機制,請確認接收地址是一個可以從docker訪問到的本地宿主地址.
const u3 = createU3(config);
const subscribe = await u3.registerEvent('ben', 'http://192.168.1.5:3002');
//or
const unsubscribe = await u3.unregisterEvent('ben', 'http://192.168.1.5:3002');
監聽
const { createU3, listener } = require('u3.js/src');
listener(function(data) {
// do callback logic
console.log(data);
});
U3Utils.test.wait(2000);
//must call listener function before emit event
const contract = await u3.contract(account);
contract.hi('ben', 30, 'It is a test', { authorization: [`admin@chaindaily`] });
六、合約編寫
Ultrain使用類JavaScript的語言來編寫智慧合約,這個類JavaScript的語言以TypeScript為原型,透過擴充套件的資料型別標誌符,來達到強型別語言的程式設計語法。
系統內建的方法
function NAME(str: string): u64 : 方法**NAME()**用來將一個string轉成一個account_name型別.str的字元長度不超過12個字元, 內容只能包括以下字元(不能以.結尾):.012345abcdefghijklmnopqrstuvwxyz
function RNAME(account: account_name): string : 方法**RNAME()用來將一個account_name型別轉為string型別, 它是NAME()**方法的反向方法。
function ACTION(str: string): Action : 方法**ACTION()**將一個string型別轉為Action型別,str的長度不超過21個字元, 內容只能包括以下字元(不能包含.):._0-9a-zA-Z. Action類封裝了action相關的資訊。
Action.sender : 當前transaction的發起者, account_name型別。
Action.receiver : 當前transaction的接收者, 即合約帳戶, account_name型別。
Block.number : head block的塊高。
Block.id : head block的id,sha256的hash值。
Block.timestamp : head block的時間戳,從EPOCH開始的秒數。
編寫第一個合約Hello world
import { NAME, RNAME } from "ultrain-ts-lib/src/account";
import { Log } from "ultrain-ts-lib/src/log";
import { Contract } from "ultrain-ts-lib/lib/contract";
class HelloWorld extends Contract {
@action
hi(name: account_name, age: u32, msg: string): void {
Log.s("hi: name = ").s(RNAME(name)).s(" age = ").i(age, 10).s(" msg = ").s(msg).flush();
}
}
我們以上程式碼做如下說明:
1. import: 用來引入其它檔案中定義的類和方法,詳細用法可參考 typescript(https://www.tutorialspoint.com/typescript/index.htm)的說明。
2. extends Contract: 合約都需要派生自Contract,而且一個專案中只能有一個Contract。
3. @action: 申明一個合約方法。只有@action標誌的方法,才能被呼叫。
4. Log: 列印Log。需要在config.ini檔案中配置 contracts-console = true 才能列印到終端。
在Action中Return資訊
為了便於在呼叫方與節點中傳遞部分執行狀態資訊,引入Return模組,Return模組返回的資料會附加在http的response中,呼叫方可以透過分析response得到Return的資訊。
需要強調的是,Return的資訊僅僅是在一個節點(host_url )上預執行的結果,並非區塊鏈網路共識的結果。也就是說, Return返回的結果, 並不是最終交易執行的結果。
Return的資訊只供參考,它可能與區塊鏈網路共識結果不一致。
要Return資訊,可以在action呼叫中,透過Return,ReturnArray方法來完成。Return資訊有以下需要注意的點:
NOTICE
1. Return的message是有長度限制的,預設的message長度為128個character。(int型資料會轉成對應的string)。如果是在側鏈中使用,可以在config.ini檔案中配置 contract-return-string-length 來擴充套件長度限制。
2. 只支援Return基本資料型別int和string, 以及int[]和string[]。
3. 可以呼叫Return或ReturnArray多次,資訊將被concat。
4. 超出長度限制的資訊,會直接丟棄,不會丟擲異常。
Return資訊的示例
class HelloContract extends Contract{
@action
on_hi(name: u64, age: u32, msg: string): void {
Return("call hi() succeed.");
ReturnArray([1,2,3]);
}
}
執行正常的情況下,Return的結果是call hi() succeed.123
資產查詢和轉移
在合約中,可以查詢一個帳號在ultrainio.token合約中的資產,即ultrain平臺資產。查詢資產使用Asset.balanceOf(who: account_name): Asset方法。 轉移ultrain平臺資產,可以使用Asset.transfer(from: account_name, to: account_name, val: Asset, memo: string): void方法。
使用詳情請參考示例balance (https://github.com/ultrain-os/ultrain-ts-lib/blob/master/example/balance/balance.ts)。
import "allocator/arena";
import { Contract } from "ultrain-ts-lib/src/contract";
import { Asset } from "ultrain-ts-lib/src/asset";
import { ultrain_assert } from "ultrain-ts-lib/src/utils";
class BalanceContract extends Contract {
@action
transfer(from: account_name, to: account_name, bet: Asset): void {
let balance = Asset.balanceOf(from);
ultrain_assert(balance.gte(bet), "your balance is not enough.");
balance.prints("banalce from: ");
Asset.transfer(from, to, bet, "this is a transfer test");
}
}
NOTICE 使用Asset.transfer命令轉移資產時,需要保證from的許可權已經授權給了utrio.code,在使用命令列的情況下,可以透過以下命令來授權:
clutrain set account permission $from active '{"threshold": 1, "keys":[{"key":"$PubKey_of_from", "wieght": 1}], "accounts": [{"permission": {"actor": "$from", "permission": "utrio.code"}, "weight": 1]}' owner -p $from
$from是需要授權的帳號。
持久化儲存
Ultrain的智慧合約提供了DBManager來儲存合約資料到資料庫中。不同於以太坊會自動儲存資料,Ultrain需要明確的呼叫API來儲存、讀取資料。
Serializable介面
Serializable是一個Interface, 定義以下三個方法:
import {DataStream} from "ultrain-ts-lib/src/datastream";
export interface Serializable {
deserialize(ds: DataStream): void;
serialize(ds : DataStream) : void;
primaryKey(): u64;
}
deserialize(ds: DataStream): void;
· 方法用來做反序列化工作,從DataStream的位元組流中讀取資料進行初始化工作。
· serialize(ds: DataStream): void; 方法用來做序列化工作,將class的資料寫入到位元組流中。
· primaryKey(): u64; 標誌一個primary key。如果這個class將作為一條獨立的記錄寫入資料庫,那primaryKey()返回的資料將成為資料庫中的primary key.
NOTICE
1. 一個實現了ISerialzable介面的class,編譯器將自動實現以上三個方法,並將class中的成員變數都序列化/反序列化。如果需要單獨override某一個/全部方法,則可以手動實現對應的方法。
2. 如果要排除某個成員變數,以避免序列化和反序列化,可以使用 @ignore 註解。
3. 如果要指定某個成員變數為primaryKey,可以使用 @primaryid 註解。需要注意的是,被註解為@primaryid的變數必須是u64型別,如果沒有變數被註解為@primaryid,則primaryKey()方法預設使用0 作為返回值。
4. 如果使用了@註解,同時又override了serialize()、deserialize()、primaryKey()方法中的某一個(或全部),編譯器將優先使用override的方法。
對於Serializable介面的使用,舉例如下:
class Person implements Serializable{
name: string;
age: u32;
sex: string;
salary: u32;
@ignore
address: string; // 被忽略,不序列化和反序列化
constructor() {
this.name = "xx";
//...
}
// 重寫primaryKey()方法,返回Person的id
primaryKey(): u64 {
return NAME(this.name);
}
}
可序列化儲存的資料
儲存到資料庫中的資料,必須是能夠序列化和反序列化。可以序列化儲存的資料有以下幾類:
1. 內建基本資料型別: u8/i8, u16/i16, u32/i32, u64/i64, boolean, string。 有一些型別其實也是基本資料型別的別名,如account_name。
2. 基本資料型別的一維陣列: u8[], i8[], ..., string[]
3. 實現了Serializable介面的類, 如上的Person。
4. 實現了Serializable介面的類的一維陣列,如Person[]。
宣告合約中DB的table資訊
如果合約中需要使用到DB進行資料存取,則需要在具體的Contract類中註解說明table的資訊。 如下簡單的一份虛擬碼:
class Person implements Serializable {
name: string;
sex: string;
}
class Car implements Serializable {
model: string;
power: u32;
color: string;
}
@database(Person, "persons")
@database(Car, "cars")
// @database() if any more
clas MyContract extends Contract {
//...
// your logic here
}
上述程式碼將會生成兩張表格: "persons"和"cars"。 需要注意的是,@database註解中的Person和Car兩個類,必須實現Serializable介面。
資料庫讀寫
Contract中資料存取要透過DBManager來管理。
DBManager的定義:
export class DBManager{
constructor(tblname: u64, owner: u64, scope: u64) {}
public cursor(): Cursor{}
public emplace(payer: u64, obj: T): void {}
public modify(payer: u64, newobj: T): void {}
public exists(primary: u64): boolean {}
public get(primary: u64, out: T): boolean { }
public erase(obj: T): void {}
}
constructor()方法接收三個引數,
· tblname: u64表示表名; owner:u64表示這個表在哪個合約中,一般的,owner和該合約的receiver是一樣的。 scope: u64表示表中的一個上下文。
· cursor()方法讀取資料表中的所有記錄。
· emplace()方法向表中加入一條記錄。 payer表示這個帳號將為資料儲存付費, obj是一個Serializable的物件,將資料存入DB。
· modify()方法更新表中的資料。 payer表示這條記錄的建立者、付費方; newobj是更新後的資料,newobj的primaryKey對應的物件會被更新。
· exists()方法判斷一個primaryKey是否存在。
· get()方法從DB中讀取primary對應的記錄,並反序列化到out中。
· erase()方法用來刪除一條記錄,obj的primaryKey對應的記錄如果存在,將被刪掉。
NOTICE table沒有方法可以顯式刪除,只有當table中的記錄都刪掉時,table會自動被刪除。
使用Cursor遍歷所有記錄
我們提供了cursor來遍歷所有的記錄,但是必須明白,這個操作非常非常低效,因為在當呼叫cursor()方法時,會將所有的表中的資料都載入到記憶體裡面。如果表中的資料很多的話,那這個交易將會被cursor方法阻塞,從而導致交易超時失敗。 如下示例演示了怎樣使用cursor:
let cursor = this.db.cursor();
Log.s("cursor.count =").i(cursor.count).flush();
while(cursor.hasNext()) {
let p: Person = cursor.get();
p.prints();
cursor.next();
}
table裡面scope和primary key的關係
table中的資料,可以按scope來分類,也可以透過primary key來分類。儘管它們都可以達到分類資料的效果,但是在table中,scope和primary key是兩個不同的維度,它們之間的關係,大概可用下面的結構來表示:
|--table
|----scope1
|--------primaryKey_1
|--------primaryKey_2
|--------........
|----scope2
|--------primaryKey_x
|--------primaryKey_y
|--------.......
在不同的scope下面,primary key可以取相同的值。
使用示例
DB的讀寫操作,請參考示例Person (https://github.com/ultrain-os/ultrain-ts-lib/blob/master/example/person/person.ts)。
import "allocator/arena";
import { Contract } from "ultrain-ts-lib/src/contract";
import { Log } from "ultrain-ts-lib/src/log";
import { ultrain_assert } from "ultrain-ts-lib/src/utils";
import { DBManager } from "ultrain-ts-lib/src/dbmanager";
import { NAME } from "ultrain-ts-lib/src/account";
class Person implements Serializable {
// name: string;
name: string
age: u32;
salary: u32;
primaryKey(): u64 { return NAME(this.name); }
prints(): void {
Log.s("name = ").s(this.name).s(", age = ").i(this.age).s(", salary = ").i(this.salary).flush();
}
}
const tblname = "humans";
const scope = "dept.sales";
@database(Person, "humans")
// @database(SomeMoreRecordStruct, "other_table")
class PersonContract extends Contract {
db: DBManager;
public onInit(): void {
this.db = new DBManager(NAME(tblname), this.receiver, NAME(scope));
}
public onStop(): void {
}
constructor(code: u64) {
super(code);
this._receiver = code;
this.onInit();
}
@action
add(name: string, age: u32, salary: u32): void {
let