1. 程式人生 > 其它 >火幣智慧鏈合約開發教程

火幣智慧鏈合約開發教程

技術標籤:火幣智慧鏈火幣智慧鏈火幣智慧合約開發火幣鏈智慧合約從以太坊轉到火幣只能鏈火幣erc20代幣開發

RISC-V C 合約開發教程

目前測試鏈對於部署合約做了白名單控制,使用者如想體驗,可以在本地部署一條私有測試鏈進行測試

概述

RISC-V service 為 Huobi Chain 提供了一個支援 RISC-V 指令集的虛擬機器服務。 使用者可以通過該服務自行部署和執行合約,實現強大的自定義功能。

一個 RISC-V 合約的本質是一個 Linux 的 ELF 可執行檔案,使用虛擬機器執行該合約等同於 Linux 環境下在單核 CPU 下執行這個可執行檔案。

理論上任何提供了 RISC-V 後端的語言均可以用來開發合約。就生成程式碼的體積和質量(執行過程中 cycle 的消耗)而言,目前最成熟的工具是 riscv-gcc。

本文將以 C 語言開發的一個 ERC20 Token 和 Bank 合約為例,為你展示如何編寫、部署、呼叫、測試一個 RISC-V 合約。

Echo 合約示例

我們首先來看一個簡單的 echo 合約:

#include <pvm.h>

int main() {
    char args[100] = {0};
    uint64_t args_len = 0;
    pvm_load_args(args, &args_len);

    pvm_ret(args, args_len);
	return 0;
}

該合約的作用是將引數的內容原樣返回。

將該 C 程式碼通過 riscv-gcc 編譯生成的二進位制檔案即為我們的合約。

執行合約時,從 main 函式開始。當 main 函式返回值為 0 時,認為合約執行成功,否則合約執行失敗。

注意,這個合約中我們引入了pvm.h,使用了其中的pvm_load_argspvm_ret函式。pvm.h這個檔案中包含了我們與鏈互動所需要的所有函式。這些函式是通過系統呼叫實現的,我們將在下節進行詳細講解。

pvm

pvm 支援合約向 ckb-vm 外部發起呼叫的系統函式集合。在 ckb-vm 內部,你無法直接向鏈發起任何呼叫,包括獲取 Block,Receipt 這樣常用的方法。 所有的對鏈請求,都必須經過 ckb-vm 的 ecall 指令集完成。pvm 替開發者封裝了所有對鏈請求的邏輯,匯出為 C 函式,供合約呼叫

系統呼叫

由於 CKB-VM 只是一個 RISC-V 指令集直譯器。要實現合約的複雜邏輯,必然要與鏈進行互動,如解析引數,返回結果,獲取鏈/交易上下文,操作合約狀態等。因此我們在 risc-v service 中用系統呼叫實現了這些互動功能。

下面是pvm_load_args函式的實現。呼叫該函式時,虛擬機器會根據暫存器的狀態,呼叫虛擬機器外部對應功能的實現函式,並將實現結果通過暫存器和記憶體寫回虛擬機器。例如下面的pvm_load_args呼叫時,會將交易中的合約呼叫引數寫到虛擬機器內部的記憶體,然後將記憶體起始地址和引數長度寫到對應的暫存器。

static inline long
__internal_syscall(long n, long _a0, long _a1, long _a2, long _a3, long _a4, long _a5)
{
    register long a0 asm("a0") = _a0;
    register long a1 asm("a1") = _a1;
    register long a2 asm("a2") = _a2;
    register long a3 asm("a3") = _a3;
    register long a4 asm("a4") = _a4;
    register long a5 asm("a5") = _a5;
    register long syscall_id asm("a7") = n;
    asm volatile ("scall": "+r"(a0) : "r"(a1), "r"(a2), "r"(a3), "r"(a4), "r"(a5), "r"(syscall_id));
    return a0;
}

#define syscall(n, a, b, c, d, e, f) \
    __internal_syscall(n, (long)(a), (long)(b), (long)(c), (long)(d), (long)(e), (long)(f))

int pvm_load_args(uint8_t *data, uint64_t *len)
{
    return syscall(SYSCODE_LOAD_ARGS, data, len, 0, 0, 0, 0);
}

RISC-V service 中提供的系統呼叫有 4 類:

  • debug 工具。pvm_debug提供了虛擬機器內部的 debug 工具,使用者可以列印一段任意的 bytes,方便合約進行除錯
  • 入參出參。pvm_load_argspvm_ret分別提供了合約的入參和出參功能,使用者通過前者獲取合約呼叫引數,通過後者返回合約執行結果。合約的入參和出參均為任意的 bytes
  • 獲取交易上下文。例如通過pvm_caller獲取呼叫該函式的地址,pvm_block_height獲取當前塊高度。
  • 鏈操作:
    • pvm_set_storagepvm_get_storage可以用來操作合約的狀態空間。每個合約擁有獨立的地址,在該地址下擁有獨立的狀態空間,可以把這個狀態空間理解成一個 kv 資料庫,使用者可以在裡面儲存任意的資料。合約只能訪問和修改自己狀態空間內的資料。可以把這個狀態空間類比理解成以太坊的 contract storage。
    • pvm_contract_call可以用來呼叫其它 RISC-V 合約,pvm_service_call可以用來呼叫 huobi-chain 的其它 build-in service。

所有的系統呼叫函式都在pvm.h檔案中,裡面有詳細的函式文件,讀者可以自行查閱。

ERC20 和 Bank 合約程式碼

理解了系統呼叫後,我們來看一個真實的 ERC20 合約和 Bank 合約的例子。

我們將原始碼放到了 GitHub 上,讀者可以將示例程式碼下載到本地進行檢視和互動。

$ git clone https://github.com/nervosnetwork/riscv-contract-tutorials.git
$ cd riscv-contract-tutorials/bank

ERC20 合約是一個符合 ERC20 標準的 token 合約。本合約僅作為說明合約功能之用,讀者如有發行自定義資產的需求,建議使用 huobi-chain 的原生資產模組,asset service。

Bank 合約實現了存錢、取錢、查餘額等功能,展示了合約如何與其它合約相互操作(例如存錢時需要呼叫 ERC20 合約的 transfer_from 功能),該合約本身並沒有什麼現實意義,但可以很容易擴充套件成一個類似 EtherDelta 這樣的 DEX 合約。

合約說明:

  • 序列化:由於合約的入參出參均為任意的 bytes,對於功能複雜的合約,我們可能需要引入一些序列化方法。在上述的程式碼中,我們使用的是 JSON 格式,因為 JSON 使用廣泛,無 schema 限制,且可讀性較好。使用者也可以根據自己的需求(速度、可讀性、大小),選擇適合的序列化方案,如 rlp,protobuf,thrift,msgpack 等。
  • 函式分發:合約執行的統一入口為 main 函式,使用者如想在一個合約中實現許多不同的功能,可以自行在 main 函式中進行函式分發。上述示例中,即是通過 method 欄位的內容來路由到不同的函式進行處理。
  • 除了系統合約外,我們還用到了很多其他的 C 語言庫來幫助開發,具體內容可以參見 deps 資料夾。

編譯

我們使用 riscv-gcc 來將 C 原始碼編譯成二進位制檔案。由於 riscv-gcc 工具編譯較為複雜,我們提供了打包好的 docker 映象供讀者使用。編譯示例如下:

讀者可以在 bank 資料夾中執行:

$ make bin_docker

命令來使用 docker 進行編譯,在 bin 資料夾下得到的兩個二進位制檔案即為我們的合約。

具體的編譯可以參考專案的 Makefile:

TARGET := riscv64-unknown-elf
CC := $(TARGET)-gcc
LD := $(TARGET)-gcc
CFLAGS := -Os -DCKB_NO_MMU -D__riscv_soft_float -D__riscv_float_abi_soft
LDFLAGS := -lm -Wl,-static -fdata-sections -ffunction-sections -Wl,--gc-sections -Wl,-s
CURRENT_DIR := $(shell pwd)
DOCKER_BUILD := docker run --rm -it -v $(CURRENT_DIR):/src nervos/ckb-riscv-gnu-toolchain:xenial bash -c
DEPS := $(CURRENT_DIR)/deps
MID := $(CURRENT_DIR)/middle
BIN := $(CURRENT_DIR)/bin

libpvm.a:
	$(CC) -I$(DEPS) -c $(DEPS)/pvm.c -o $(MID)/pvm.o
	$(CC) -I$(DEPS) -c $(DEPS)/UsefulBuf.c -o $(MID)/UsefulBuf.o
	$(CC) -I$(DEPS) -c $(DEPS)/pvm_structs.c -o $(MID)/pvm_structs.o
	ar rcs $(MID)/libpvm.a $(MID)/UsefulBuf.o $(MID)/pvm_structs.o $(MID)/pvm.o

bin: libpvm.a
	$(CC) -I$(DEPS) $(DEPS)/cJSON.h $(DEPS)/cJSON.c erc20.c $(MID)/libpvm.a $(LDFLAGS) -o $(BIN)/erc20.bin
	$(CC) -I$(DEPS) $(DEPS)/cJSON.h $(DEPS)/cJSON.c bank.c $(MID)/libpvm.a $(LDFLAGS) -o $(BIN)/bank.bin

bin_docker:
	$(DOCKER_BUILD) "cd /src && make bin"

clear:
	@rm -rf middle/*

部署

RISC-V service 的部署合約介面簽名如下:

pub enum InterpreterType {
    Binary = 1,
}

pub struct DeployPayload {
    pub code:      String,
    pub intp_type: InterpreterType,
    pub init_args: String,
}

pub struct DeployResp {
    pub address:  Address,
    pub init_ret: String,
}

其中:

  • 引數
    • code:合約程式碼,使用 hex 編碼
    • intp_type:生產環境目前僅支援Binary,即 ELF 二進位制檔案格式
    • init_args:初始化引數
  • 返回值
    • address:合約地址
    • init_ret:初始化函式呼叫返回值

我們使用 muta-client 來進行操作:

# 部署前請先在本地起一條單節點的鏈

$ muta-cli repl
# 由於部署合約耗費 cycle 較大,調大 client 的預設 cycleslimit
> client.options.defaultCyclesLimit = '0xffffff'

> const fs = require('fs')
> const erc20 = fs.readFileSync('bin/erc20.bin')

> const tx = await client.composeTransaction({ method: 'deploy', payload: { intp_type: 'Binary', init_args: '{"method":"init","name":"bitcoin","symbol":"BTC","supply":1000000000}', code: erc20.toString('hex') }, serviceName: 'riscv' })

> const account = accounts[0]
> txHash = await client.sendTransaction(account.signTransaction(tx))
'4a8baf53d59bed2ef016526030203a0d15ed838e0dfcc717495b56142cc0c77a'

> receipt = await client.getReceipt(txHash)
{ txHash:
   '4a8baf53d59bed2ef016526030203a0d15ed838e0dfcc717495b56142cc0c77a',
  height: '0x0000000000000510',
  cyclesUsed: '0x0000000000024e50',
  events: [],
  stateRoot:
   '0x64b18096bf322b74d3f6ae206d5fe3154fa19a876fe849130f9b5f70d627c850',
  response:
   { serviceName: 'riscv',
     method: 'deploy',
     response:{
        code:'0x00',
        succeed_data:'{"address":"7598a35834c5c1f544edd9ba48013c361f71bf3b","init_ret":""}',
        error_message:''
     }
    }
}

互動

RISC-V service 提供了兩種execcall兩個互動介面。前者為寫介面,可以操作鏈上資料,需要通過發交易,打包執行,後者為查詢介面,可以通過鏈的 query 功能直接呼叫。

示例(繼續使用剛才的 client):

# 查詢介面
> await client.queryService({ serviceName: 'riscv', method: 'call', payload: JSON.stringify({ address, args: JSON.stringify({method: 'total_supply'}) })})
{ isError: false, ret: '"1000000000"' }

# 發交易
> payload = JSON.stringify({ address, args: JSON.stringify({method: 'transfer', recipient: '0000000000000000000000000000000000000000', amount: 100})})
> tx = await client.composeTransaction({ method: 'exec', payload, serviceName: 'riscv' })
> txHash = await client.sendTransaction(account.signTransaction(tx))
> receipt = await client.getReceipt(txHash)
{ txHash:
   '9f18a395972012817c68611e86e182af72f33964ec86c629c8727a9ec1a79daa',
  height: '0000000000000386',
  cyclesUsed: '0000000000072306',
  events: [],
  stateRoot:
   '7f95c8a6d338fbd64de764cdf1870cc60696c4e69af1656de279d7cded6026ed',
  response:
   { serviceName: 'riscv',
     method: 'exec',
     response:{
             code:'0x00',
             succeed_data:'',
             error_message:''
     }
   }
 }

作者v_x+