XuperChain文档¶
简介¶
XuperChain是超级链体系下的第一个开源项目,是构建超级联盟网络的底层方案。
其主要特点是高性能,通过原创的XuperModel模型,真正实现了智能合约的并行执行和验证,通过自研的WASM虚拟机,做到了指令集级别的极致优化。
在架构方面,其可插拔、插件化的设计使得用户可以方便选择适合自己业务场景的解决方案,通过独有的XuperBridge技术,可插拔多语言虚拟机,从而支持丰富的合约开发语言。
在网络能力方面,XuperChain具备全球化部署能力,节点通信基于加密的P2P网络,支持广域网超大规模节点,且底层账本支持分叉管理,自动收敛一致性,TDPOS算法确保了大规模节点下的快速共识。在账号安全方面,XuperChain内置了多私钥保护的账号体系,支持权重累计、集合运算等灵活的策略。

XuperChain架构
模块¶
模块 | 特性 |
---|---|
存储 | XuperChain的底层存储基于KV数据库,存储的数据包括区块数据、交易数据、账号 余额、DPOS投票数据、合约账号数据、智能合约数据等,上链的数据全部持久化到底 层存储。不同的链,存储独立。底层存储支持可插拔,从而可以满足不同的业务场景 |
网络 | 负责交易数据的网络传播和广播、节点发现和维护。以P2P通信为基础,实现全分布式 结构化拓扑网络结构,数据传输全程加密。局域网穿透技术采用NAT方案,同一条流保 持长连接且复用。多条链复用同一个p2p网络 |
共识 | 共识模块用于解决交易上链顺序问题,过滤无效交易并达成全网一致。XuperChain实 现了更加高效的DPOS共识算法。支持可插拔,从而可以支持不同的业务场景 |
密码学 | 用于构造和验证区块、交易的完整性,采用非对称加密算法生成公私钥、地址。匿名性 较好。支持可插拔,从而可以支持不同的业务场景 |
智能合约 | 自研并实现了一套智能合约虚拟机XVM,支持丰富的开发语言,智能合约之间并发执行, 支持执行消耗资源,避免恶意攻击 |
提案 | 一种解决系统升级问题的机制。比如修改区块大小,升级共识算法。提案整个过程涉及 到发起提案、参与投票、投票生效三个阶段 |
账号与权限 | 为了满足合约调用的权限控制,保证XuperChain网络的健康运转,自研并实现了一套 基于账号的去中心化的合约权限系统。支持权重累计、集合运算等灵活的策略,可以满足 不同的业务场景 |
核心数据结构¶
背景¶
众所周知,程序=数据结构+算法,了解一个程序的数据结构有助于掌握一个程序的关键设计。本文从背景、功能以及各个字段的用意来剖析XuperChain底层核心数据结构,从而方便XuperChain开发者以及用户更深入地了解XuperChain底层框架的核心数据结构的设计缘由,有助于提高XuperChain开发者更高效的开发,有助于XuperChain用户更好的使用XuperChain来服务自己的业务。
核心数据结构¶
涉及到的核心数据结构包括:区块、交易、UTXO、读写集。
区块¶
- 背景:所谓区块链,简单来说就是不同的区块以DAG方式链接起来形成的链。因此,区块是区块链的基本单元。
- 功能:区块是区块链的基本单元,通常为了提高区块链网络的吞吐,矿工会在一个区块中打包若干个交易。一个区块通常由区块头以及区块体组成。

- 代码:区块的Proto如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | message InternalBlock {
// block version
// 区块版本
int32 version = 1;
// Random number used to avoid replay attacks
// 随机数,用来避免重放攻击
int32 nonce = 2;
// blockid generate the hash sign of the block used by sha256
// 区块的唯一标识
bytes blockid = 3;
// pre_hash is the parent blockid of the block
// 区块的前置依赖区块ID
bytes pre_hash = 4;
// The miner id
// 矿工ID
bytes proposer = 5;
// 矿工对区块的签名
// The sign which miner signed: blockid + nonce + timestamp
bytes sign = 6;
// The pk of the miner
// 矿工公钥
bytes pubkey = 7;
// The Merkle Tree root
// 默克尔树树根
bytes merkle_root = 8;
// The height of the blockchain
// 区块所在高度
int64 height = 9;
// Timestamp of the block
// 打包区块的时间戳
int64 timestamp = 10;
// Transactions of the block, only txid stored on kv, the detail information
// stored in another table
// 交易内容
repeated Transaction transactions = 11;
// The transaction count of the block
// 区块中包含的交易数量
int32 tx_count = 12;
// 所有交易hash的merkle tree
repeated bytes merkle_tree = 13;
// 采用DPOS共识算法时,当前是第几轮
int64 curTerm = 16;
int64 curBlockNum = 17;
// 区块中执行失败的交易以及对应的失败原因
map<string, string> failed_txs = 18; // txid -> failed reason
// 采用POW共识算法时,对应的挖矿难度值
int32 targetBits = 19;
// 下面的属性会动态变化
// If the block is on the trunk
// 该区块是否在主干上
bool in_trunk = 14;
// Next next block which on trunk
// 当前区块的后继区块ID
bytes next_hash = 15;
}
|
交易¶
- 背景:区块链网络中的每个节点都是一个状态机,为了给每个节点传递状态,系统引入了交易,作为区块链网络状态更改的最小操作单元。
- 功能:通常表现为普通转账以及智能合约调用。
- 代码:交易的Proto如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | message Transaction {
// txid is the id of this transaction
// 交易的唯一标识
bytes txid = 1;
// the blockid the transaction belong to
// 交易被打包在哪个区块中
bytes blockid = 2;
// Transaction input list
// UTXO来源
repeated TxInput tx_inputs = 3;
// Transaction output list
// UTXO去处
repeated TxOutput tx_outputs = 4;
// Transaction description or system contract
// 交易内容描述或系统合约
bytes desc = 6;
// Mining rewards
// 矿工奖励
bool coinbase = 7;
// Random number used to avoid replay attacks
// 随机数
string nonce = 8;
// Timestamp to launch the transaction
// 发起交易的时间戳
int64 timestamp = 9;
// tx format version; tx格式版本号
int32 version = 10;
// auto generated tx
// 该交易是否属于系统自动生成的交易
bool autogen = 11;
// 读写集中的读集
repeated TxInputExt tx_inputs_ext = 23;
// 读写集中的写集
repeated TxOutputExt tx_outputs_ext = 24;
// 该交易包含的合约调用请求
repeated InvokeRequest contract_requests = 25;
// 权限系统新增字段
// 交易发起者, 可以是一个Address或者一个Account
string initiator = 26;
// 交易发起需要被收集签名的AddressURL集合信息,包括用于utxo转账和用于合约调用
repeated string auth_require = 27;
// 交易发起者对交易元数据签名,签名的内容包括auth_require字段
repeated SignatureInfo initiator_signs = 28;
// 收集到的签名
repeated SignatureInfo auth_require_signs = 29;
// 节点收到tx的时间戳,不参与签名
int64 received_timestamp = 30;
// 统一签名(支持多重签名/环签名等,与initiator_signs/auth_require_signs不同时使用)
XuperSignature xuper_sign = 31;
}
|
UTXO¶
- 背景:区块链中比较常见的两种操作,包括普通转账以及合约调用,这两种操作都涉及到了数据状态的引用以及更新。为了描述普通转账涉及到的数据状态的引用以及更新,引入了UTXO(Unspent Transaction Output)。
- 功能:一种记账方式,用来描述普通转账时涉及到的数据状态的引用以及更新。通常由转账来源数据(UtxoInput)以及转账去处数据(UtxoOutput)组成。

- 代码:UTXO的Proto如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | message Utxo {
// 转账数量
bytes amount = 1;
// 转给谁
bytes toAddr = 2;
// 转给谁的公钥
bytes toPubkey = 3;
// 该Utxo属于哪一个交易
bytes refTxid = 4;
// 该Utxo数据哪一个交易的哪一个offset
int32 refOffset = 5;
}
// UtxoInput query info to query utxos
// UTXO的转账来源
message UtxoInput {
Header header = 1;
// which bcname to select
// UTXO来源属于哪一条链
string bcname = 2;
// address to select
// UTXO来源属于哪个address
string address = 3;
// publickey of the address
// UTXO来源对应的公钥
string publickey = 4;
// totalNeed refer the total need utxos to select
// 需要的UTXO总额
string totalNeed = 5;
// userSign of input
// UTXO来源的签名
bytes userSign = 7;
// need lock
// 该UTXO是否需要锁定(内存级别锁定)
bool needLock = 8;
}
// UtxoOutput query results
// UTXO的转账去处
message UtxoOutput {
Header header = 1;
// utxo list
// UTXO去处
repeated Utxo utxoList = 2;
// total selected amount
// UTXO去处总额
string totalSelected = 3;
}
|
读写集¶
- 背景:区块链中比较常见的两种操作,包括普通转账以及合约调用,这两种操作都涉及到了数据状态的引用以及更新。为了描述合约调用涉及到的数据状态的引用以及更新,引入了读写集。
- 功能:一种用来描述合约调用时涉及到的数据状态的引用以及更新的技术。通常由读集(TxInputExt)以及写集(TxOutputExt)组成。

- 代码:读写集的Proto如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // 扩展输入
message TxInputExt {
// 读集属于哪一个bucket
string bucket = 1;
// 读集对应的key
bytes key = 2;
// 读集属于哪一个txid
bytes ref_txid = 3;
// 读集属于哪一个txid的哪一个offset
int32 ref_offset = 4;
}
// 扩展输出
message TxOutputExt {
// 写集属于哪一个bucket
string bucket = 1;
// 写集对应的key
bytes key = 2;
// 写集对应的value
bytes value = 3;
}
|
智能合约¶
自研并实现了一套智能合约虚拟机XVM。特点如下:
- 合约状态数据与合约代码运行环境分离,从而能够支持多语言虚拟机且各种合约虚拟机只需要做纯粹的无状态合约代码执行;
- 支持执行消耗资源,避免恶意攻击;
- 支持丰富的智能合约开发语言,比如go,Solitidy,C/C++,Java等;
- 利用读写集确保普通合约调用支持并发执行,充分利用计算机多核特性;
权限系统¶
实现一个去中心化,区块链内置的合约账号权限系统。特点如下:
- 支持多种权限模型,比如背书数、背书率、AK集合、CA鉴权、社区治理等;
- 支持完善的账号权限管理,比如账号的创建、添加和删除AK、设置AK权重、权限模型;
- 支持设置合约调用权限,添加和删除AK、设置AK权重、权限模型;
隐私和保密¶
XuperChain支持多种隐私保护和保密机制,包括但不限于:
- 数据在p2p网络中采用ECDH加密传输,保障区块链数据的安全性;
- 通过助记词技术,在用户私钥丢失的情况下可以恢复;
- 多私钥保护的账号体系;
- 基于椭圆曲线算法的公私钥加密和签名体系;
总结¶
XuperChain是百度自研的一套区块链解决方案,采用经典的UTXO记账模式,并且支持丰富的智能合约开发语言,交易处理支持并发执行,拥有完善的账号与权限体系,采用DPOS作为共识算法,交易处理速度可达到9万TPS。
本章节将指导您获取XuperChain的代码并部署一个基础的可用环境,还会展示一些基本操作
XuperChain环境部署¶
编译XuperChain¶
使用git下载源码到本地
执行命令
1 2 | cd src/github.com/xuperchain/xuperchain
make
|
- 在output目录得到产出xchain和xchain-cli
Note
可能需要配置go语言环境变量($GOROOT, $PATH)
- GOPATH问题报错(go1.11版本之后无需关注)
- 在1.11版本之前需要配置。配置成以下形式:
- export GOPATH=xxx/github.com/xuperchain/xuperchain
GCC版本需要升级到4或5以上
XuperChain基本操作¶
在output下,主要目录有data, logs, conf, plugins等, 二进制文件有xchain,xchain-cli
各目录的功能如下表:
目录名 | 功能 |
---|---|
output/ | 节点根目录 |
├─ conf | xchain.yaml: xchain服务的配置信息(注意端口冲突) plugins.conf: 插件的配置信息 |
├─ data | 数据的存放目录,创世块信息,以及共识和合约的样例 |
│ ··· ├─ blockchain | 账本目录 |
│ ··· ├─ keys | 此节点的地址,具有全局唯一性 |
│ ··· ├─ netkeys | 此节点的网络标识ID,具有全局唯一性 |
│ ··· └─ config | 包括创始的共识,初始的资源数,矿工奖励机制等 |
├─ logs | 程序日志目录 |
├─ plugins | so扩展的存放目录 |
├─ xchain | xchain服务的二进制文件 |
├─ xchain-cli | xchain客户端工具 |
└─ wasm2c | wasm工具(智能合约会用到) |
部署xchain服务¶
创建链¶
在启动xchain服务之前,我们首先需要创建一条链(创世区块),xchain客户端工具提供了此功能
1 2 | # 创建xuper链
./xchain-cli createChain
|

创建链
这样我们就使用 config/xuper.json 中的配置创建了一条链(此时 data/blockchain 中会生成 xuper 目录,里面即是我们创建的链的账本等文件)
确认服务状态¶
按照默认配置,xchain服务会监听37101端口,可以使用如下命令查看xchain服务的运行状态
1 2 | # check服务运行状况
./xchain-cli status -H 127.0.0.1:37101
|

查看运行状态
基本功能的使用¶
创建新账号¶
xchain中,账号分为普通账号和“合约账号”,这里先介绍普通账号的创建,命令如下
1 2 3 | # 创建普通用户, 包含地址,公钥,私钥
./xchain-cli account newkeys --output data/bob
# 在bob目录下会看到文件address,publickey,privatekey生成
|

创建账号
查询资源余额¶
对于普通账号,可使用如下命令查询账号资源余额,其中 -H 参数为xchain服务的地址
1 | ./xchain-cli account balance --keys data/keys -H 127.0.0.1:37101
|

查询余额
转账¶
转账操作需要提供源账号的私钥目录,也就类似“1.2.4.1”中生成的目录,这里注意到并不需要提供目标账号的任何密钥,只需要提供地址即可
1 2 | # --keys 从此地址 转给 --to地址 --amount 钱
./xchain-cli transfer --to czojZcZ6cHSiDVJ4jFoZMB1PjKnfUiuFQ --amount 10 --keys data/keys/ -H 127.0.0.1:37101
|
命令执行的返回是转账操作的交易id(txid)

普通转账操作
查询交易信息¶
通过以下命令可以查询交易的信息,包括交易状态、交易的源和目标账号、交易的金额、所在的区块(如果已上链)等内容
1 2 | # 可查询上一步生成的txid的交易信息
./xchain-cli tx query cbbda2606837c950160e99480049e2aec3e60689a280b68a2d253fdd8a6ce931 -H 127.0.0.1:37101
|

查询交易
查询block信息¶
通过blockid可以查询区块的相关信息,包括区块内打包的交易、所在链的高度、前驱/后继区块的id等内容
1 2 | # 可查询上一步交易所在的block id信息
./xchain-cli block 0354240c8335e10d8b48d76c0584e29ab604cfdb7b421d973f01a2a49bb67fee -H 127.0.0.1:37101
|

查询区块
发起多重签名交易¶
对于需要多个账号签名才可以生效的交易,需要先发起多重签名交易,收集需要的签名,然后在发出
对需要收集签名的账号地址,需要事先维护在一个文件中(假定名为addr_list),每个地址一行
1 2 | YDYBchKWXpG7HSkHy4YoyzTJnd3hTFBgG
ZAmWoJViiNn5pKz32m2MVgmPnSpgLia7z
|
假设要发起一笔转账操作
1 2 3 4 | # 从账号发起
./xchain-cli multisig gen --to czojZcZ6cHSiDVJ4jFoZMB1PjKnfUiuFQ --amount 100 -A addr_list
# 从合约账号发起
./xchain-cli multisig gen --to czojZcZ6cHSiDVJ4jFoZMB1PjKnfUiuFQ --amount 100 -A addr_list --from XC11111111111111@xuper
|
这样会生成一个tx.out文件,包含了需发起的交易内容
1 2 | # 各方在签名之前可以check 原始交易是否ok,查看visual.out
./xchain-cli multisig check --input tx.out --output visual.out
|
然后收集需要的签名
1 2 3 4 5 | # 首先需要发起者自己的签名
./xchain-cli multisig sign --tx tx.out --output my.sign
# 假设addr_list中的地址对应的私钥存放在alice、bob中
./xchain-cli multisig sign --keys data/account/alice --tx tx.out --output alice.sign
./xchain-cli multisig sign --keys data/account/bob --tx tx.out --output bob.sign
|
最后将交易和收集好的签名发出
1 2 | # send后第一个参数是发起者的签名文件,第二个参数是需要收集的签名文件,均为逗号分割
./xchain-cli multisig send --tx tx.out my.sign alice.sign,bob.sign
|
XuperModel¶
XuperChain能够支持合约链内并行的很大的原因是由于其底层自研的XuperModel数据模型。
XuperModel是一个带版本的存储模型,支持读写集生成。该模型是比特币utxo模型的一个演变。在比特币的utxo模型中,每个交易都需要在输入字段中引用早期交易的输出,以证明资金来源。同样,在XuperModel中,每个事务读取的数据需要引用上一个事务写入的数据。在XuperModel中,事务的输入表示在执行智能合约期间读取的数据源,即事务的输出来源。事务的输出表示事务写入状态数据库的数据,这些数据在未来事务执行智能合约时将被引用,如下图所示:

XuperModel事务
为了在运行时获取合约的读写集,在预执行每个合约时XuperModel为其提供智能缓存。该缓存对状态数据库是只读的,它可以为合约的预执行生成读写集和结果。验证合约时,验证节点根据事务内容初始化缓存实例。节点将再次执行一次合约,但此时合约只能从读集读取数据。同样,写入数据也会在写入集中生效。当验证完生成的写集和事务携带的写集一致时合约验证通过,将事务写入账本,cache的原理如下所示,图中左边部分是合约预执行时的示意图,右边部分是合约验证时的示意图:

XuperModel合约验证
XuperBridge¶
内核调用设计¶
XuperBridge为所有合约提供统一的合约接口,从抽象方式上类似于linux内核对应于应用程序,内核代码是一份,应用程序可以用各种语言实现,比如go,c。类比到合约上就是各种合约的功能,如KV访问,QueryBlock, QueryTx等,这些请求都会通过跟xchain通信的方式来执行,这样在其上实现的各种合约虚拟机只需要做纯粹的无状态合约代码执行。
合约与xchain进程的双向通信¶
xchain进程需要调用合约虚拟机来执行具体的合约代码,合约虚拟机也需要跟xchain进程通信来进行具体的系统调用,如KV获取等,这是一个双向通信的过程。
这种双向通信在不同虚拟机里面有不同的实现,
- 在native合约里面由于合约是跑在docker容器里面的独立进程,因此牵扯到跨进程通信,这里选用了unix socket作为跨进程通信的传输层,xchain在启动合约进程的时候把syscall的socket地址以及合约进程的socket地址传递给合约进程,合约进程一方面监听在unix socket上等待xchain调用自己运行合约代码,另一方面通过xchain的unix socket创建一个指向xchain syscall服务的grpc客户端来进行系统调用。
- 在WASM虚拟机里面情况有所不同,WASM虚拟机是以library的方式链接到xchain二进制里面,所以虚拟机和xchain在一个进程空间,通信是在xchain和WASM虚拟机之间进行的,这里牵扯到xchain的数据跟虚拟机里面数据的交换,在实现上是通过WASM自己的模块机制实现的,xchain实现了一个虚拟的WASM模块,合约代码执行到外部模块调用的时候就转到对应的xchain函数调用,由于xchain和合约代码的地址空间不一样,还是牵扯到序列化和反序列化的动作。
PB接口¶
合约暴露的代码接口
1 2 3 | service NativeCode {
rpc Call(CallRequest) returns (CallResponse);
}
|
xchain暴露的syscall接口
1 2 3 4 5 6 7 8 9 10 11 12 | service Syscall {
// KV service
rpc PutObject(PutRequest) returns (PutResponse);
rpc GetObject(GetRequest) returns (GetResponse);
rpc DeleteObject(DeleteRequest) returns (DeleteResponse);
rpc NewIterator(IteratorRequest) returns (IteratorResponse);
// Chain service
rpc QueryTx(QueryTxRequest) returns (QueryTxResponse);
rpc QueryBlock(QueryBlockRequest) returns (QueryBlockResponse);
rpc Transfer(TransferRequest) returns (TransferResponse);
}
|
KV接口与读写集¶
合约每次执行的产出为一系列KV操作的读写集,读写集的概念详细见 XuperModel。
KV的接口:
- GetObject(key)
- PutObject(key, value)
- DeleteObject(key)
- NewIterator(start, limit)
各个接口对读写集的影响:
- Get会生成一个读请求
- Put会产生一个读加一个写
- Delete会产生一个读加一个特殊的写(TODO)
- Iterator会对迭代的key产生读
效果:
- 读请求不会读到最新的其他tx带来的变更
- 读请求会读到最新的自己的写请求(包括删除)的变更
- 写请求在提交前不会被其他合约读到
- 新写入的会被迭代器读到
合约上下文¶
每次合约运行都会有一个伴随合约执行的上下文(context)对象,context里面保存了合约的kv cache对象,运行参数,输出结果等,context用于隔离多个合约的执行,也便于合约的并发执行。
Context的创建和销毁¶
context在合约虚拟机每次执行合约的时候创建。 每个context都有一个context id,这个id由合约虚拟机维护,在xchain启动的时候置0,每次创建一个context对象加1,合约虚拟机保存了context id到context对象的映射。 context id会传递给合约虚拟机,在Docker里面即是合约进程,在之后的合约发起KV调用过程中需要带上这个context id来标识本次合约调用以找到对应的context对象。
context的销毁时机比较重要,因为我们还需要从context对象里面获取合约执行过程中的Response以及读写集,因此有两种解决方案,一种是由调用合约的地方管理,这个是xuper3里面做的,一种是统一销毁,这个是目前的做法,在打包成块结束调用Finalize的时候统一销毁所有在这个块里面的合约context对象。
合约上下文的操作¶
- NewContext,创建一个context,需要合约的参数等信息。
- Invoke,运行一个context,这一步是执行合约的过程,合约执行的结果会存储在context里面。
- Release,销毁context,context持有的所有资源得到释放。
XVM虚拟机¶
背景¶
XVM为合约提供一个稳定的沙盒运行环境,有如下目标:
- 隔离性,合约运行环境和xchain运行环境互不影响,合约的崩溃不影响xchain。
- 确定性,合约可以访问链上资源,但不能访问宿主机资源,保证在确定的输入下有确定的输出
- 可停止性,设置资源quota,合约对资源的使用超quta自动停止
- 可以统计合约的资源使用情况,如CPU,内存等
- 运行速度尽量快。
WASM简介¶
WASM是WebAssembly的缩写,是一种运行在浏览器上的字节码,用于解决js在浏览器上的性能不足的问题。 WASM的指令跟机器码很相似,因此很多高级语言如C,C++,Go,rust等都可以编译成WASM字节码从而可以运行在浏览器上。 很多性能相关的模块可以通过用C/C++来编写,再编译成WASM来提高性能,如视频解码器,运行在网页的游戏引擎,React的虚拟Dom渲染算法等。
WASM本身只是一个指令集,并没有限定运行环境,因此只要实现相应的解释器,WASM也可以运行在非浏览器环境。 xchain的WASM合约正是这样的应用场景,通过用C++,go等高级语言来编写智能合约,再编译成WASM字节码,最后由XVM虚拟机来运行。 XVM虚拟机在这里就提供了一个WASM的运行环境。
WASM字节码编译加载流程¶
WASM字节码的运行有两种方式,一种是解释执行,一种是编译成本地指令后再运行。 前者针对每条指令挨个解释执行,后者通过把WASM指令映射到本地指令如(x86)来执行,解释执行有点是启动快,缺点是运行慢,编译执行由于有一个预先编译的过程因此启动速度比较慢,但运行速度很快。
XVM选用的是编译执行模式。
字节码编译¶
用户通过c++编写智能合约,通过emcc编译器生成wasm字节码,xvm加载字节码,生成加入了指令资源统计的代码以及一些运行时库符号查找的机制,最后编译成本地指令来运行。
c++合约代码
1 2 3 | int add(int a, int b) {
return a + b;
}
|
编译后的WASM文本表示
1 2 3 4 5 6 | (module
(func $add (param i32 i32) (result i32)
local.get 0
local.get 1
i32.add)
(export "_add" (func $add)))
|
XVM编译WASM到c,最后再生成动态链接库。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | static u32 _add(wasm_rt_handle_t* h, u32 p0, u32 p1) {
FUNC_PROLOGUE;
u32 i0, i1;
ADD_AND_CHECK_GAS(3);
i0 = p0;
i1 = p1;
i0 += i1;
FUNC_EPILOGUE;
return i0;
}
/* export: '_add' */
u32 (*export__add)(wasm_rt_handle_t*, u32, u32);
static void init_exports(wasm_rt_handle_t* h) {
/* export: '_add' */
export__add = (&_add);
}
|
加载运行¶
在了解如何加载运行之前先看下如何使用xvm来发起对合约的调用,首先生成Code对象,Code对象管理静态的指令代码以及合约所需要的符号解析器Resolver。 之后就可以通过实例化Context对象来发起一次合约调用,GasLimit等参数就是在这里传入的。Code和Context的关系类似Docker里面的镜像和容器的关系, 一个是静态的,一个是动态的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | func run(modulePath string, method string, args []string) error {
code, err := exec.NewCode(modulePath, emscripten.NewResolver())
if err != nil {
return err
}
defer code.Release()
ctx, err := exec.NewContext(code, exec.DefaultContextConfig())
if err != nil {
return err
}
ret, err := ctx.Exec(method, []int64{int64(argc), int64(argv)})
fmt.Println(ret)
return err
}
|
转换后的c代码最终会编译成一个动态链接库来给XVM运行时来使用,在每个生成的动态链接库里面都有如下初始化函数。 这个初始化函数会自动对wasm里面的各个模块进行初始化,包括全局变量、内存、table、外部符号解析等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | typedef struct {
void* user_ctx;
wasm_rt_gas_t gas;
u32 g0;
uint32_t call_stack_depth;
}wasm_rt_handle_t;
void* new_handle(void* user_ctx) {
wasm_rt_handle_t* h = (*g_rt_ops.wasm_rt_malloc)(user_ctx, sizeof(wasm_rt_handle_t));
(h->user_ctx) = user_ctx;
init_globals(h);
init_memory(h);
init_table(h);
return h;
}
|
语言运行环境¶
c++运行环境¶
c++因为没有runtime,因此运行环境相对比较简单,只需要设置基础的堆栈分布以及一些系统函数还有emscripten的运行时函数即可。
c++合约的内存分布
普通调用如何在xvm解释
go运行环境¶
XuperBridge对接¶
XVM跟XuperBridge对接主要靠两个函数
- call_method,这个函数向Bridge传递需要调用的方法和参数
- fetch_response,这个函数向Bridge获取上次调用的结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | extern "C" uint32_t call_method(const char* method, uint32_t method_len,
const char* request, uint32_t request_len);
extern "C" uint32_t fetch_response(char* response, uint32_t response_len);
static bool syscall_raw(const std::string& method, const std::string& request,
std::string* response) {
uint32_t response_len;
response_len = call_method(method.data(), uint32_t(method.size()),
request.data(), uint32_t(request.size()));
if (response_len <= 0) {
return true;
}
response->resize(response_len + 1, 0);
uint32_t success;
success = fetch_response(&(*response)[0u], response_len);
return success == 1;
}
|
资源消耗统计¶
考虑到大部分指令都是顺序执行的,因此不需要在每个指令后面加上gas统计指令, 只需要在control block最开头加上gas统计指令,所谓control block指的是loop, if等会引起跳转的指令。
c++代码
1 2 3 4 5 6 7 8 9 10 11 12 | extern int get(void);
extern void print(int);
int main() {
int i = get();
int n = get();
if (i < n) {
i += 1;
print(i);
}
print(n);
}
|
编译后生成的wast代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | (func (;2;) (type 1) (result i32)
(local i32 i32)
call 1
local.tee 0
call 1
local.tee 1
i32.lt_s
if ;; label = @1
local.get 0
i32.const 1
i32.add
call 0
end
local.get 1
call 0
i32.const 0)
|
生成的带统计指令的c代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | static u32 wasm__main(wasm_rt_handle_t* h) {
u32 l0 = 0, l1 = 0;
FUNC_PROLOGUE;
u32 i0, i1;
ADD_AND_CHECK_GAS(11);
i0 = wasm_env__get(h);
l0 = i0;
i1 = wasm_env__get(h);
l1 = i1;
i0 = (u32)((s32)i0 < (s32)i1);
if (i0) {
ADD_AND_CHECK_GAS(6);
i0 = l0;
i1 = 1u;
i0 += i1;
wasm_env__print(h, i0);
}
ADD_AND_CHECK_GAS(5);
i0 = l1;
wasm_env__print(h, i0);
i0 = 0u;
FUNC_EPILOGUE;
return i0;
}
|
账号权限控制模型¶
背景¶
超级链需要一套去中心化的,内置的权限系统 为了实现这个目标,我们借鉴了业界很多现有系统如Ethereum、EOS、Fabric 的优点,设计一个基于账号的合约权限系统
名词解释¶
AK(Access Key)
:具体的一个address,由密码学算法生成一组公私钥对,然后将公钥用指定编码方式压缩为一个地址。账号(Account)
: 在超级链上部署合约需要有账号, 账号可以绑定一组AK(公钥),并且AK可以有不同的权重。 账号的名字具有唯一性。合约 (Contract)
: 一段部署在区块链上的可执行字节码,合约的运行会更新区块链的状态。我们允许一个账号部署多个合约。合约的名字具有唯一性。
模型简介¶
系统会首先识别用户,然后根据被操作对象的ACL的信息来决定用户能否对其进行哪些操作

ACL简介
- 个人账号AK:是指一个具体的地址Addr
- 用户的创建是离线的行为,可以通过命令行工具或者API进行创建
- 合约账号:超级链智能合约的管理单元。
- 账号的创建:
- 任何账号或者AK都可以调用系统级智能合约创建账号
- 创建账号需要指定账号对应的拥有者的地址集,如果一个账号中只有一个地址, 那么这个Addr对账号完全控制;
- 创建账号需要指定ACL控制策略,用于账号其他管理动作的权限控制;
- 创建账号需要消耗账号资源;
- 账号名命名规则;
- 合约账号由三部分组成,分为前缀,中间部分,后缀。
- 前缀为XC,后缀为@链名
- 中间部分为16个数字组成。
- 在创建合约账号的时候,只需要传入16位数字,在使用合约账号的时候,使用完整的账号。
- 账号管理:依地址集合据创建时指定的地址集和权限策略,管理账号其他操作
- 账号股东剔除和加入
- 账号资产转账
- 创建合约,创建智能合约需要消耗账号资源,先将utxo资源打到账号下,通过消耗账号的utxo资源创建合约,验证的逻辑需要走账号的ACL控制
- 合约Method权限模型管理
- 智能合约:超级链中的一个具体的合约,属于某个账号
- 账号所属人员允许在账号内部署合约
- 账号所属人员可以定义合约管理的权限模型
- 设置合约方法的权限模型,合约内有一个权限表,记录:{ contract.method,permission_model}
- 合约命名规则:长度为4~16个字符(包括4和16),首字母可选项为[a-ZA-Z_],末尾字符可选项为[a-zA-Z0-9_],中间部分的字符可选项为[a-zA-Z_.]
实现功能¶
主要有两个功能:账号权限管理、合约权限管理
- 账号权限管理 账号的创建、添加和删除AK、设置AK权重、权限模型
- 合约权限管理 设置合约调用权限,支持2种权限模型:
- 背书阈值:在名单中的AK或Account签名且他们的权重值加起来超过一定阈值,就可以调用合约
- AK集合: 定义多组AK集合,集合内的AK需要全部签名,集合间只要有一个集合有全部签名即可
系统设计¶

ACL架构
ACL数据结构说明¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | // -------- Account and Permission Section -------
enum PermissionRule {
NULL = 0; // 无权限控制
SIGN_THRESHOLD = 1; // 签名阈值策略
SIGN_AKSET = 2; // AKSet签名策略
SIGN_RATE = 3; // 签名率策略
SIGN_SUM = 4; // 签名个数策略
CA_SERVER = 5; // CA服务器鉴权
COMMUNITY_VOTE = 6; // 社区治理
}
message PermissionModel {
PermissionRule rule = 1;
double acceptValue = 2; // 取决于用哪种rule, 可以表示签名率,签名数或权重阈值
}
// AK集的表示方法
message AkSet {
repeated string aks = 1; //一堆公钥
}
message AkSets {
map<string, AkSet> sets = 1; // 公钥or账号名集
string expression = 2; // 表达式,一期不支持表达式,默认集合内是and,集合间是or
}
// Acl实际使用的结构
message Acl {
PermissionModel pm = 1; // 采用的权限模型
map<string, double> aksWeight = 2; // 公钥or账号名 -> 权重
AkSets akSets = 3;
}
|
签名阈值策略: Sum{Weight(AK_i) , if sign_ok(AK_i)} >= acceptValue
系统合约接口¶
合约接口 | 用途 |
---|---|
NewAccountMethod | 创建新的账号 |
SetAccountACLMethod | 更新账号的ACL |
SetMethodACLMethod | 更新合约Method的ACL |
样例¶
acl模型如下:
1 2 3 4 5 6 7 8 9 10 11 | {
"pm": {
"rule": 1,
"acceptValue": 1.0
},
"aksWeight": {
"AK1": 1.0,
"AK2": 1.0
}
}
|
- 其中rule=1表示签名阈值策略,rule=2表示AKSet签名策略
- 签名的ak对应的weight值加起来>acceptValue,则符合要求
超级链p2p网络¶
p2p网络概述¶
依据p2p网络中节点相互之间如何联系,可以将p2p网络简单区分为无结构和结构化两大类:
- 非结构化p2p网络:这种p2p网络是最普通的,没有对结构做特别的设计。优点在于结构简单易于组件,网络局部区域内个体可以任意分布。对于节点的加入和离开网络也表现地非常稳定,比特币网络使用的就是无结构化的网络。但是这种网络主要有3个缺点,一是公网网络拥塞时传输效率低,二是存在泛洪循环,三是消息风暴问题。
- 结构化p2p网络:这种p2p网络的结构经过精心设计,目的是为了增加路由效率,提高查询数据的效率,结构化p2p最普遍的实现方案是使用分布式哈希表(DHT),以太坊网络中使用的就是结构化的网络。
互联网的发展速度远远超过人们的预期,人们在制定网络协议之初没有考虑过网络规模会获得如此迅速的增长,导致ip地址的短缺。NAT技术通过将局域网内的主机地址映射为互联网上的有效ip地址,实现了网络地址的复用,从而部分解决了ip地址短缺的问题。网络中大部分用户处于各类NAT设备之后,导致在p2p网络中两个节点之间直接建立udp或者tcp链接难度比较大,应运而生的是NAT穿透技术。目前主要有两种途径,一种称为打洞,即UDP Punch技术;另一种是利用NAT设备的管理接口,称为UPnP技术。
超级链p2p网络¶
超级链采用libp2p作为网络的基础设施,使用KAD进行节点的路由管理,支持NAT穿透。超级链的定义了自己的协议类型 XuperProtocolID = “/xuper/2.0.0” ,所有的超级链网络节点除了基础的消息类型外还会监听并处理这个协议的网络消息。
超级链p2p消息¶
超级链消息采用Protobuf定义,整个消息包括2部分,分别是消息头 MessageHeader
和消息体 MessageData
,具体如下所示:

其proto消息定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // XuperMessage is the message of Xuper p2p server
message XuperMessage {
// MessageHeader is the message header of Xuper p2p server
message MessageHeader {
string version = 1;
// dataCheckSum is the message data checksum, it can be used check where the message have been received
string logid = 2;
string from = 3;
string bcname = 4;
MessageType type = 5;
uint32 dataCheckSum = 6;
ErrorType errorType = 7;
}
// MessageData is the message data of Xuper p2p server
message MessageData {
// msgInfo is the message infomation, use protobuf coding style
bytes msgInfo = 3;
}
MessageHeader Header = 1;
MessageData Data = 2;
}
|
模块交互图¶
超级链p2p网络模块与其他模块的交互如图所示:

上图左边是Xuper的启动流程,其中InitP2PServer的流程为启动P2P的核心流程,如右半部分所示,右半部分主要包括4个阶段,分别为:
- InitP2pInstance:创建libp2p host实例
- SetXuperStreamHandler:初始化p2p通信消息protocols,XuperProtocol为Xuper节点之间进行消息通信和消息处理的核心逻辑。
- InitKadDht: 初始化libp2p KadDht,通过设置的bootstrap节点,建立自己的kad dht。
- InitStreams: 前一步已经建立了自己的kad dht,下一步就是与这些邻近的节点之间建立通信流,通过libp2p的NewStream接口实现通信流建立。
至此,Xuper的p2p连接建立完毕。
身份认证¶
背景¶
Xuperchain节点之间存在双重身份:P2P节点ID和Xuperchain address,为了解决节点间的身份互信,防止中间人攻击和消息篡改,节点间需要一种身份认证机制,可以证明对称节点声明的XChain address是真实有效的
名词解释¶
- Xuperchain address:当前节点的address,一般为data/keys/address
- P2P节点ID:当前节点P2P的peer.ID
P2P建立连接过程¶

连接建立时序
实现过程¶
- 新建的net.Stream连接,已经完成了ECDH密钥协商流程,因此此时节点间已经是加密连接。
- 连接建立后,增加一步身份认证流程,如果通过,则stream建立成功,加入到streamPool中
其中,身份认证流程如下:
- 身份认证流程通过开关控制,可开启和关闭 DefaultIsAuthentication: true or false
- 身份验证支持XChain address的验证方式
- 如果开启身份验证,则身份验证不通过的Stream直接关闭
- 身份验证是使用XChain的私钥对PeerID+XChain地址的SHA256哈希值进行签名,并将PeerID、Xuperchain公钥、Xuperchain地址、签名数据一起传递给对方进行验证
主要结构修改点¶
1 2 3 4 5 6 | // stream 增加authenticate接口
func (s *Stream) Authenticate() error {}
// 收到身份验证消息后的回调处理函数接口
func (p *P2PServerV2) handleGetAuthentication(ctx context.Context, msg *xuper_p2p.XuperMessage) (*xuper_p2p.XuperMessage, error) {}
|
提案和投票机制¶

图1:提案和投票机制示意图
提案和投票机制是区块链系统实现自我进化的关键。系统首次上线后难免遇到很多问题,我们提供提案/投票机制为区块链的社区治理提供便利的工具,以保证未来系统的可持续发展。具体实现方法如下:
Step1:提案者(proposer) 通过发起一个事务声明一个可调用的合约,并约定提案的投票截止高度,生效高度; Step2:投票者(voter) 通过发起一个事务来对提案投票,当达到系统约定的投票率并且账本达到合约的生效高度后,合约就会自动被调用; Step3:为了防止机制被滥用,被投票的事务的需要冻结参与者的一笔燃料,直到合约生效后解冻。
共识可升级¶

图2:XuperChain提案机制进行共识升级
XuperChain提供可插拔共识机制,通过提案和投票机制,升级共识算法或者参数。图2简要说明了如何使用XuperChain的提案机制进行共识升级。
系统参数可升级¶
通过提案和投票机制,区块链自身的运行参数也是可升级的。包括:block大小、交易大小、挖矿奖励金额和衰减速度等。
下面通过一个例子来说明,假设一条链,最开始用的是POW共识,创始块如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | {
"version" : "1",
"predistribution": [
{}
],
"maxblocksize" : "128",
"award" : "1000000",
"decimals" : "8",
"award_decay": {
"height_gap": 31536000,
"ratio": 0.5
},
"genesis_consensus": {
"name": "pow",
"config": {
"defaultTarget": "19", # 默认难度19个0 bits前缀
"adjustHeightGap": "10", # 每10个区块调整一次难度
"expectedPeriod": "15", # 期望15秒一个区块
"maxTarget": "22"
}
}
}
|
然后,我们想将其共识切换到TDPOS共识。
步骤1:由提案者发起提案,提案没有额外的代价,通过命令行的desc选项指向提案用的json即可。 提案json的内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | {
"module": "proposal",
"method": "Propose",
"args" : {
"min_vote_percent": 51, # 当投票者冻结的资产占全链的51%以上时生效提案
"stop_vote_height": 120 # 停止计票的高度是:120
},
"trigger": {
"height": 130, # 提案生效高度是:130
"module": "consensus",
"method": "update_consensus",
"args" : {
"name": "tdpos",
"config": {
"proposer_num":"3",
"period":"3000",
"term_gap":"60000",
"alternate_interval": "3000",
"term_interval": "6000",
"block_num":"10",
"vote_unit_price":"1",
"init_proposer": {
"1": ["dpzuVdosQrF2kmzumhVeFQZa1aYcdgFpN", "f3prTg9itaZY6m48wXXikXdcxiByW7zgk", "U9sKwFmgJVfzgWcfAG47dKn1kLQTqeZN3"]
}
}
}
}
}
|
把上面的json保存在myprop.json, 然后运行:
1 | ./xchain-cli transfer --to `cat data/keys/address` --desc ./myprop.json --amount 1
|
得到一个txid,此处为 67cc7cd23b7fcbe0a4919d5c581b3fda759da13cdd97414afa7539e221727594
然后,通过
1 | ./xchain-cli tx query 67cc7cd23b7fcbe0a4919d5c581b3fda759da13cdd97414afa7539e221727594
|
确认该交易已经上链(标志是blockid不为空了)
步骤2:可以对这个提案投票。投票需要冻结自己资产,并且冻结高度必须大于停止计票的高度。
1 | ./xchain-cli vote –amount 100000000 –frozen 121 67cc7cd23b7fcbe0a4919d5c581b3fda759da13cdd97414afa7539e221727594
|
Note
注意:冻结高度121需要大于提案停止计票高度120, 否则是无效投票。
另外,累计投票金额数量必须大于全链总量的51% (51%是提案json中指定的,但是最小不能少于50%)
1 2 | ./xchain-cli account balance –Z # 可以查看自己被冻结的资产总量。
./xchain-cli status --host localhost:37301 | grep -i total # 查询全链的资产总量。
|
步骤3:最后,等到当前生效高度到达,会发现共识已经切换到TDPOS了。
1 | ./xchain-cli tdpos status
|
此命令可以查看tdpos状态。
密码学和隐私保护¶
背景¶
密码学技术是区块链的核心基础技术之一,承担着区块链不可篡改和去中心化验证等特性的底层支撑。在超级链中,密码学技术广泛应用在账户体系、交易签名、数据隐私保护等方面,主要以ECC(椭圆曲线密码体系)以及多种Hash散列算法为基础,发展出的一个单独的模块。
密码学基础¶
哈希函数¶
加密哈希函数(Hash Function)
是适用于密码学的哈希散列函数,是现代密码学的基本工具。它是一种数学算法,将任意大小的数据(通常称为“消息”)映射到固定大小的二进制串(称之为“散列值”,“散列”或“消息摘要”),并且是单向的功能,即一种实际上不可逆转的功能。理想情况下,查找生成给定哈希的消息的唯一方法是尝试对可能的输入进行暴力搜索,以查看它们是否产生匹配,或使用匹配哈希的彩虹表。
MD5
: 摘要长度为128bit,由于容易受到碰撞攻击,目前使用越来越少。SHA256
: SHA系列哈希算法由美国国家安全局制定,具有多个hash算法标准,可以产生160~512bit不等的哈希摘要。目前在区块链中使用较多的是SHA256,摘要长度为256bit,具有较高的抗碰撞攻击安全性。RIPEMD-160
: 产生长度为160bit的摘要串。 相比于美国国家安全局设计的SHA-1和SHA-2算法,RIPEMD-160的设计原理是开放的。
关于一些典型的Hash算法的对比,可以参考 这里 。
ECC¶
构建区块链的去中心化交易,需要一种加密算法,使交易发起人使用持有的密钥对交易数据进行数字签名,而交易验证者只需要知道交易发起人的公开信息,即可对交易有效性进行验证,确定该交易确实来自交易发起者。这种场景在密码学中称之为`公开密钥加密`,也称之为非对称密钥加密。
常见的公开密钥算法如RSA、ECC(Elliptic Curve Cryptography,缩写为 ECC)等,RSA起步较早,此前在非对称加密领域使用范围最广,例如目前的SSL证书大多采用RSA算法。而在ECC算法问世后,由于在抗攻击性、资源消耗等方面相比RSA具有更好的表现,其使用也越来越广泛。
公钥密码算法一般都基于一个数学难题,比如RSA的依据是给定两个数p, q很容易相乘得到N, 当N足够大时,对N进行因式分解则相对困难的多。ECC是建立在基于椭圆曲线的离散对数问题上的密码体制,给定椭圆曲线上的一个点P,一个整数k,求解Q=kP很容易;给定一个点P、Q,知道Q=kP,求整数k却是一个难题。具体的理论知识可以参考 椭圆曲线密码学。
椭圆曲线密码学包含了多种密码学算法,下面列出在超级链中涉及到的一些算法:
ECIES
(Elliptic Curve Integrated Encryption Scheme): 椭圆曲线集成加密算法,主要用于基于椭圆曲线的数据加解密。ECDH
(Elliptic Curve Diffie–Hellman): 基于Diffie–Hellman算法的一种密钥协商算法,定义了双方如何安全的生成和交换密钥。ECDSA
(Elliptic Curve Digital Signature Algorithm): 是使用椭圆曲线密码学实现的DSA(数字签名算法),一般发起人对消息摘要使用私钥签名,验证者可以通过公钥对签名有效性进行验证。
椭圆曲线算法由于采用的椭圆曲线的不同,具有多种不同的算法标准,典型的如:
- NIST标准,典型的曲线如P-256/P-384/P-521等;
- SECG标准,典型的如Secp256k1/Secp256r1/ secp192k1/ secp192r1等;
- ECC25519,主要指Ed25519数字签名和Curve25519密钥协商标准等;
- 国产密码算法,中国国家密码局制定的密码学算法标准,典型的如SM2/3/4等。
多重签名和环签名¶
多重签名是指在数字签名中,有时需要多个用户对同一个交易进行签名和认证,例如某些合约账户下的数据需要多个人授权才能修改或转账。
在密码学中,通过多重签名可以将多个用户的授权签名信息压缩在同一个签名中,这样相比于每个用户产生一个签名的数据体量会小很多,因此其验签计算、网络传输的资源开销也会少很多。
环签名是一种数字签名技术,环签名的一个安全属性是无法通过计算还原出一组用户中具体使用私钥签名的用户。也就是说,使用环签名技术可以使一组用户中的某一个人对消息进行签名,而并不会泄露签名者是这组用户中的哪个人。环签名与组签名类似,但在两个关键方面有所不同:第一,单个签名具有匿名性; 第二,任何一批用户都可以作为一个组使用,无需额外设置。
在实际使用中,多重签名主要用作多人实名授权的交易,通过产生更小的签名数据提升网络传输和计算效率,而环签名则主要用于对交易隐私保护和匿名性有要求的交易场景中。
超级链中密码学的使用¶
密码学作为区块链系统的底层基础技术,在很多方面都会使用到。这里介绍几个超级链中几个密码学典型的使用场景。
用户公私钥账户¶
超级链的用户账户体系基于非对称公私钥对,每个用户账户对应这一组公私钥对,并采用一定的哈希算法将公钥摘要成一个字符串作为用户账户地址(address)。

超级链中公私钥对使用椭圆曲线算法生成,用户账户地址主要使用SHA256和RIPEMD-160哈希算法生成。
考虑到密钥不具备可读性,为了帮助用户保存密钥,超级链实现了BIP39提案的助记词技术。
- 助记词的生成过程:首先生成一个长度在128~256bit之间的随机熵,由此在助记词表中选出对应的单词列表,形成助记词。
- 助记词产生私钥:使用基于口令的密钥派生算法PBKDF2,将上述生成的助记词和用户指定的密钥作为密钥派生算法参数,生成长度为512bit的种子,以此种子作为生成密钥的随机参数,便产生了了从助记词生成的私钥。
- 通过助记词恢复密钥:由于用户持有生成密钥的助记词和口令,因此在用户私钥遗忘或丢失时,可以通过同样的助记词和口令,执行助记词产生私钥的过程,从而恢复出账户密钥。
数据签名¶
超级链中,每个交易都需要交易发起人以及交易背书人的签名;在每个块生成时,也需要加上打包块的节点的签名。
- 交易签名: 基于交易数据摘要,会包含交易输入输出、合约调用、合约读写集、发起人和背书人信息等,并将交易数据序列化后的字节数组使用双重SHA256得到摘要数据,最后对摘要数据用ECDSA或其他数字签名算法产生交易签名。
- 块签名:基于区块数据摘要,会包含区块元信息如前序块Hash值、交易Merkle树根、打包时间、出块节点等数据,并在序列化后使用双重SHA256得到摘要数据,最后对摘要数据用ECDSA或其他数字签名算法产生区块签名。

通信加密¶
超级链底层采用P2P网络传播交易和区块数据,在一些许可区块链网络场景中,需要对节点间的数据传输进行加密提升安全和隐私性,因此超级链的P2P连接支持基于ECDH的密钥交换算法的TLS连接。
ECDH的原理是交换双方可以在不共享任何秘密的情况下协商出一个密钥,双方只要知道对方的公钥,就能和自己的私钥通过计算得出同一份数据,而这份数据就可以作为双方接下来对称加密的密钥。

超级链P2P网络通过ECDH建立通信加密通道的过程如上图所示:
- 第一阶段是Propose阶段,这一阶段,对等节点间互相交换双方永久公钥PK。
- 第二阶段是Exchange阶段,本质是基于ECDH的密钥交换。双方通过ECC算法随机生成一组临时密钥对(tempPK, tempSK),然后用自己的永久私钥对临时公钥tempPK进行签名并交换。这时,双方可以通过第一步的公钥进行验签,同时拿到供本次会话使用的临时公钥。使用临时公钥的好处是一话一密,即使本次会话密钥泄露也不会导致以前的加密数据被破解。ECDH算法使得双方通过对方的公钥和自己的私钥,可以获得一致的共享密钥SharedKey。
- 第三阶段是Verify阶段。双方使用ShareKey产生两组密钥Key1,Key2分别作为读写密钥,并使用支持的对称加密算法(AES/blowfish)加密传输第一步中发送给对方的Nonce,而接收方则使用刚才协商的密钥对数据解密,并验证Nonce是不是等于第一步中自己发送给对方的值。
通过这三次握手,双方建立了加密通信通道,并且节点间通信加密信道满足一话一密的高安全等级。
密码学模块¶
Crypto Provider Interface¶
密码学作为区块链系统的底层技术,相对比较独立。考虑到超级链作为区块链底层系统方案的模块化目标,我们将密码学相关的功能设计成一个单独的模块,并通过插件化技术实现了模块可插拔、插件可替换。
因此,超级链首先抽象出了统一的密码学相关的功能,并在此之上定义了统一的密码学接口,我们称之为Crypto Provider Interface,并通过CryptoClient接口向上层区块链系统系统密码学功能。CryptoClient目前由一组接口构成:
1 2 3 4 5 6 7 | // CryptoClient is the interface of all Crypto functions
type CryptoClient interface {
CryptoCore
KeyUtils
AccountUtils
MultiSig
}
|
整个CryptoClient由四部分功能接口组成:
CryptoCore
: 主要提供包括加解密、签名等密码学核心功能;KeyUtils
: 主要提供公私钥相关工具,例如密钥对象和JSON、文件格式之间的转换等;AccountUtils
: 主要提供账户相关的功能接口,例如创建账户、助记词导出私钥等;MultiSig
: 主要提供多重签名、环签名相关功能接口。
密码学插件¶
由于抽象出了统一的密码学模块和接口,在此基础上实现插件化就比较容易。目前超级链已经实现了包括 Nist P256 + ECDSA/Schnorr 以及 国密 等多种密码学插件,其中已经开源了 Nist P256 + ECDSA/Schnorr ,ECDSA和Schnorr签名作为两种可选的签名方案,分别提供了密码学插件。
为了方便框架使用密码学插件,超级链在 crypto/client 包中封装了一层密码学插件管理器,支持创建指定类型的密码学对象,或者通过公私钥自动识别需要加载的插件类型。通过密码学插件管理器,可以支持隔绝框架对密码学插件的感知,对上层框架提供一种无缝的使用体验。
超级链中默认密码学插件使用的是Nist P256 + ECDSA,在不额外指定的情况下,超级链启动后会加载默认密码学插件。
之前说过,通过密码学插件管理器可以按照公私钥自动识别需要加载的插件类型,那么超级链如何根据密钥来判断应该使用哪种密码学插件呢?其实,不同的密码学插件是通过密钥中的曲线类型来确定的,目前系统中定义了三种不同的曲线类型:
P-256
: 使用Nist P256+ECDSA的默认插件;P-256-SN
: 使用Nist P256 + Schnorr签名的插件,可以提供更高的签名验签性能;SM2-P-256
: 使用SM2/3/4的国密插件,符合中国国家密码局制定的密码学标准。
实际使用中,可以通过创建链时的配置中的密码学类型指定使用哪种密码学插件,以schnorr签名为例,在创世块配置中添加下述配置即可:
1 | "crypto": "schnorr"
|
在cli命令行工具中已经支持了通过命令行参数 –cryptotype 指定密码学插件的类型,例如需要创建一个使用Nist P256 + Schnorr的密码学插件的用户账户,可以使用下述命令行:
1 | ./xchain-cli account newkeys --output data/tmpkey --cryptotype schnorr
|
插件机制¶
可插拔架构¶
超级链从设计之初就以高性能、可插拔的区块链底层技术架构为目标,因此整个超级链在模块化、接口化设计上做了很多的抽象工作。而插件化机制就是服务于超级链可插拔的架构目标,使得所有模块具有同样的可插拔机制,并能满足对模块插件的加载、替换等生命周期的管理。
通过插件化机制可以实现如下架构优点:
- 代码解耦 :插件化机制使超级链的架构框架与各个模块的实现相解耦,模块统一抽象出基本数据结构与框架的交互接口,模块只要符合统一接口即可做到插拔替换。
- 高可扩展 :用户可以自己实现符合业务需求的模块插件,直接替换插件配置就可以实现业务扩展。
- 发布灵活 :插件可以单独发布,配合插件生命周期管理甚至可以实现插件的单独更新,而作为插件的开发者也可以自由选择开源发布或者只发布插件二进制文件。
插件框架设计¶
插件框架用以根据需求创建插件实例,考虑到超级链单进程多链、插件多实例多版本等需求,整体设计

模块和插件定义¶
超级链中,一种 模块 是指:包含一组相同数据结构和接口的代码集合,能实现相对独立的功能。
一个模块可以有多种实现,每种实现形成一个 插件 。
模块和插件具有如下约束:
- 同一种模块,需要抽象出公共数据接口和接口方法。
- 该模块的所有插件,需要实现定义的所有公共接口,并不包含定义接口以外的public接口。
- 每个插件需要实现一个
GetInstance
接口,该接口创建并返回一个插件对象引用,该插件对象包含插件定义的所有公共接口。
因此,我们可以在框架中定义一组公共的数据结构和接口,例如:
1 2 3 4 5 6 7 | package kvstore
type KVStore interface {
Init(string)
Get(string) (string, error)
Set(string, string) error
}
|
并在插件代码中,引用并实现该公共接口定义,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | import "framework/kvstore"
type KVMem struct {
meta kvstore.Meta
data map[string]string
rwmutex sync.RWMutex
}
// 每个插件必须包含此方法,返回一个插件对象
func GetInstance() interface{} {
kvmem := KVMem{}
return &kvmem
}
// 插件需要实现接口定义中的所有方法
func (ys *YourKVStore) Init(conf string) {
// Your code here
}
func (ys *YourKVStore) Get(key string) (string, error) {
// Your code here
}
func (ys *YourKVStore) Set(key string, value string) error {
// Your code here
}
|
配置化管理¶
插件通过配置文件组织可选插件以及 插件子类型 、插件路径 、版本 等信息。考虑到同一个链中可能需要创建某个插件的多种实例,因此所有的插件都以数组的方式声明该插件不同的子插件类型对应的链接库地址。
举例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | {
"kvstore":[
{
"subtype": "Memory",
"path": "plugins/kv-memory.so.1.0.1",
"version": "1.0.1",
"ondemand": false
},
{
"subtype": "Json",
"path": "plugins/kv-json.so.1.0.0",
"version": "1.0.0",
"ondemand": false
}
],
"crypto":[
{
"subtype": "GuoMi",
"path": "plugins/crypto/crypto-gm.so.1.1.0",
"version": "1.1.0",
"ondemand": false
},
]
}
|
PluginMgr¶
PluginMgr定义了插件管理的对外接口。
1 2 3 4 5 | // 根据插件配置文件初始化插件管理对象
func CreateMgr(confPath string) (pm *PluginMgr, err error);
// 指定插件名称和插件子类型,获取该插件的一个实例
func (pm *PluginMgr) CreatePluginInstance(name string, subtype string)
|
需要插件功能的主逻辑中,要通过 CreateMgr 创建一个PluginMgr的实例,该实例会根据传入的配置文件创建插件实例。
PluginMgr使用¶
每个模块可以定义自己的实例创建方法,并可以自行确定是否使用默认模块,或使用插件化的模块。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | func NewKVStore(pm *pluginmgr.PluginMgr, subType string) (store KVStore, err error) {
var iface interface{}
iface, err = pm.CreatePluginInstance(KV_PLUGIN_NAME, subType)
if err != nil {
return
}
if iface != nil {
// registered external plugin
store = iface.(KVStore)
} else {
// no plugin registered, use default one
store = new(KVText)
}
return
}
|
超级链的插件¶
目前,插件化机制已经在超级链中应用于包括密码学、共识、KV引擎等多个核心模块中,初步实现了插件的解耦和可扩展性目标。
以密码学为例,通过插件机制,我们可以实现多套不同的密码学算法的封装,目前超级链已经实现了包括Nist P256、Schnorr签名、国密算法等多个不同的密码学插件,并支持代码和二进制产出的独立发布。
当然,目前插件机制是基于go plugin的实现,限于go plugin本身实现上的一些局限性,插件机制也具有如下需要改进的地方:
- 跨平台支持:目前尚不支持Windows系统的插件化,只支持Mac/Linux系统。
- 依赖版本限制:插件的依赖库版本和框架的依赖库版本不能有任何的差别,否则会加载失败。
相信在后续超级链迭代过程中,上述问题也会得到解决。
超级链共识框架¶
区块链共识机制概述¶
区块链系统多数采用去中心化的分布式设计,节点是分散在各处,系统需要设计一套完善的制度,以维护系统的执行顺序与公平性,统一区块链的版本,并奖励提供资源维护区块链的使用者,以及惩罚恶意的危害者。这样的制度,必须依赖某种方式来证明,是由谁取得了一个区块链的打包权(或称记帐权),并且可以获取打包这一个区块的奖励;又或者是谁意图进行危害,就会获得一定的惩罚,这些都是区块链系统的共识机制需要解决的问题。
随着区块链应用落地场景越来越多,很多适应不同应用场景的共识算法先后被提出。但是在当前的技术背景下,功能过于全面的共识算法无法真正可用。在新一代区块链共识机制的设计过程中,根据实际应用场景,有的放矢的选择去中心化、节能、安全等设计原则,对一些原则支持强弱进行取舍,将一定程度上提升系统的整体运行效率。
我们超级链设计上是一个通用的区块链框架,用户可以方便地进行二次开发定制。超级链的共识模块设计上是一个能够复用底层共识安全的共识框架,用户基于这样的框架可以轻松地定义其自己的链,而不需要考虑底层的共识安全和网络安全。
超级链共识框架概览¶

上图是超级链共识模块的整体架构图,自底向上主要包括3层:
超级链共识主流程¶
超级链的是一个多链架构,其中单个链的主要逻辑在 core/xchaincore.go
文件中,其中与共识模块交互的函数主要有2个,分别是 Miner()
和 SendBlock()
:
- Miner(): 这个函数的主要功能有2点,首先判断自己是否为当前的矿工,当判断自己是矿工时需要进行区块打包。
- SendBlock(): 这个函数是节点收到区块时的核心处理逻辑,当节点收到一个区块时会调用共识模块的相关接口进行区块有效性的验证,当验证通过后会将区块写入到账本中。

超级链的共识整体流程如上图所示,主要包括7个步骤:
- 用户提交交易到网络,交易执行完后会进入未确认状态,并记录在交易的未确认列表中TxPool中;
- 节点的Miner流程通过访问Consensus模块判断自己是否为当前的矿工;
- 当节点判断自己是矿工时需要从TxPool中拉取交易并进行区块的打包;
- 当矿工完成打包后会将区块广播给其他的验证节点,同时会通过步骤7将区块写入到账本;
- 如果某个时刻其他节点判断自己是矿工,同样地会按照上述1-5流程进行区块打包,打包完后会将区块广播给该节点;
- 节点收到区块后,会调用consensus模块进行区块的有效性验证;
- 矿工打包完后或者验证节点收到一个有效的区块后,将区块写入账本;
接口介绍¶
整个共识框架主要有2套接口,分别是共识基础接口和共识安全接口,适用的场景不同。
场景一:用户希望定义自己的共识功能并独立负责共识安全;那么用户仅需要实现共识基础接口; 场景二:用户希望定义自己的共识功能,但是希望框架底层能帮助保证共识安全;那么用户需要实现共识基础接口和共识安全接口;
共识基础接口¶
共识基础接口是共识模块的核心接口,是与core模块交互的主要部分。其中最核心的部分主要是 CompeteMaster
和 CheckMinerMatch
两个。 CompeteMaster
是一个节点判断自己是否为主的主要逻辑, CheckMinerMatch
是节点收到一个区块验证其区块有效性的主要逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // consensus/base/consensusinterface.go
type ConsensusInterface interface {
Type() string
Version() int64
InitCurrent(block *pb.InternalBlock) error
Configure(xlog log.Logger, cfg *config.NodeConfig, consCfg map[string]interface{},
extParams map[string]interface{}) error
CompeteMaster(height int64) (bool, bool)
CheckMinerMatch(header *pb.Header, in *pb.InternalBlock) (bool, error)
ProcessBeforeMiner(timestamp int64) (map[string]interface{}, bool)
ProcessConfirmBlock(block *pb.InternalBlock) error
GetCoreMiners() []*MinerInfo
GetStatus() *ConsensusStatus
}
|
共识安全接口¶
共识安全接口是保证底层共识安全的核心接口,共识框架底层支持了 Hotstuff
算法的的高性能的共识安全模块 Chained-BFT
。暴露出了 PacemakerInterface
和 ExternalInterface
接口,其中 PacemakerInterface
是 Chained-BFT
的活性保证,此外为了扩展 Chained-BFT
安全模块能够应用于更多的仲裁类型, 底层 Chained-BFT
设计上不需要理解仲裁的具体内容,通过 ExternalInterface
会与外层的共识进行通信,接口的具体定义如下,更详细的内容可以参见 Chained-BFT
的介绍。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | // consensus/common/chainedbft/liveness/pacemaker_interface.go
// PacemakerInterface is the interface of Pacemaker. It responsible for generating a new round.
// We assume Pacemaker in all correct replicas will have synchronized leadership after GST.
// Safty is entirely decoupled from liveness by any potential instantiation of Packmaker.
// Different consensus have different pacemaker implement
type PacemakerInterface interface {
// NextNewView sends new view msg to next leader
// It used while leader changed.
NextNewView(viewNum int64, proposer, preProposer string) error
// NextNewProposal generate new proposal directly while the leader haven't changed.
NextNewProposal(proposalID []byte, data interface{}) error
// UpdateQCHigh update QuorumCert high of this node.
//UpdateQCHigh() error
// CurretQCHigh return current QuorumCert high of this node.
CurrentQCHigh(proposalID []byte) (*pb.QuorumCert, error)
// CurrentView return current vie of this node.
CurrentView() int64
// UpdateValidatorSet update the validator set of BFT
UpdateValidatorSet(validators []*cons_base.CandidateInfo) error
}
// consensus/common/chainedbft/external/external_interface.go
// ExternalInterface is the interface that chainedbft can communicate with external interface
// external consensus need to implements this.
type ExternalInterface interface {
// CallPreQc call external consensus for the PreQc with the given Qc
// PreQc is the the given QC's ProposalMsg's JustifyQC
CallPreQc(*pb.QuorumCert) (*pb.QuorumCert, error)
// CallProposalMsg call external consensus for the marshal format of proposalMsg's parent block
CallPreProposalMsg([]byte) ([]byte, error)
// CallPrePreProposalMsg call external consensus for the marshal format of proposalMsg's grandpa's block
CallPrePreProposalMsg([]byte) ([]byte, error)
// CallVerifyQc call external consensus for proposalMsg verify with the given QC
CallVerifyQc(*pb.QuorumCert) (bool, error)
// CallProposalMsgWithProposalID call external consensus for proposalMsg with the given ProposalID
CallProposalMsgWithProposalID([]byte) ([]byte, error)
// IsFirstProposal return true if current proposal is the first proposal of bft
// First proposal could have empty or nil PreQC
IsFirstProposal(*pb.QuorumCert) (bool, error)
}
|
Chained-BFT共识公共组件¶
概述¶
在 超级链共识框架 一文中介绍了超级链底层有一个共识的公共组件叫chained-bft,其是Hotstuff算法的实现。HotStuff是一种简洁而优雅的bft改进算法。它具有以下优点:
- 它的设计中将liveness和safty解耦开来,使得非常方便与其他的共识进行扩展;
- 将bft过程拆解成3阶段,每个阶段都是o(n)的通信;
- 它允许一个节点处于不同的view,并且将view的切换与区块结合起来,使得其能够实现异步共识,进一步提升共识的效率。
这样一个chained-bft可以在给定主集合的场景下确保网络的共识安全性,并且通过与外层共识配合工作实现共识的活性保证。
核心数据结构¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | enum QCState {
NEW_VIEW = 0;
PREPARE = 1;
PRE_COMMIT = 2;
COMMIT = 3;
DECIDE = 4;
}
// QuorumCert is a data type that combines a collection of signatures from replicas.
message QuorumCert {
// The id of block this QC certified.
bytes BlockId = 1;
// The current type of this QC certified.
// the type contains `NEW_VIEW`, `PREPARE`,`PRE_COMMIT`, `COMMIT`, `DECIDE`.
State Type = 2;
// The view number of this QC certified.
int64 ViewNumber = 3;
// SignInfos is the signs of the leader gathered from replicas
// of a specifically certType.
QCSignInfos SignInfos = 4;
}
// QCSignInfos is the signs of the leader gathered from replicas of a specifically certType.
// A slice of signs is used at present.
// TODO zq: It will be change to Threshold-Signatures
// after Crypto lib support Threshold-Signatures.
message QCSignInfos {
// QCSignInfos
map<string, SignInfo> QCSignInfos = 1;
}
// SignInfo is the signature information of the
message SignInfo {
string Address = 1;
string PublicKey = 2;
bytes Sign = 3;
}
// ChainedBftMessage is the message of the protocal
// In hotstuff, there are two kinds of messages, "NEW_VIEW_Message" and "QC_Message".
// In XuperChain, there is only one kind of message, "NEW_VIEW. The "QC_Message" is resuded with "BroadcastBlock" message.
message ChainedBftMessage {
// Message Type
QCState Type = 1;
// Justify is the QC of the leader gathered, send to next leader.
QuorumCert Justify = 2;
}
// ChainedBftMessage is the vote message of
message ChainedBftVoteMessage {
// The id of block this message vote for.
bytes BlockId = 1;
// Replica will sign the QCMessage if the QuorumCert if valid.
SignInfo signature = 2;
}
|
整个chained-bft中主要包括三部分,分别是状态机 Smr
、 SafetyRules
和 PacemakerInterface
。
Smr¶
Smr是 chained-bft
的核心实例。他的主要的作用有以下几点:
- 维护节点链的chained-bft共识状态机;
- 在外层共识的驱动下发起
NewView
和NewProposal
等消息并更新本地状态; - 处理其他验证节点的消息并更新本地状态;

Safety Rule¶
Safety Rule 是一个验证节点是否要接受一个新的Proposal的安全性规则,主要有三条:
- 判断当前Proposal的View值是否大于本地locked Proposal的View值;
- 验证当前Proposal中上一个Proposal的投票信息有效性和投票个数是否大于系统矿工数目的2/3;
- 验证当前Proposal的ProposalMsg是否有效;
当一个验证节点收到一个新的提案时,如果满足上述 Safety Rule
的认证,则会给这个提案进行投票,否则拒绝这次提案。
PacemakerInterface¶
Hotstuff算法的一大特点就是将共识的liveness和safety分开。PacemakerInterface是Hotstuff算法Pacemaker的接口定义,外层共识通过实现这些接口,可以推进内层共识的状态轮转。不同的外层共识可以有不同的实现。目前超级链已经实现了DPoS+Hotstuff,具体的方案如下所示:

TDPoS共识¶
介绍¶
TDPoS是超级链的一种改进型的DPoS算法,他是在一段预设的时间长度(一轮区块生产周期)内选择若干个验证节点,同时将这样一轮区块生产周期分为N个时间段, 这若干个候选节点按照约定的时间段协议协同挖矿的一种算法。在选定验证节点集合后,TDPoS通过Chained-BFT算法来保证轮值期间的安全性。 总结一下,整个TDPoS主要包括2大阶段:
- 验证人选举:通过pos相关选举规则选出一个验证者集合;
- 验证人轮值:验证者集合按照约定的协议规则进行区块生产;
候选人选举¶
节点角色

在TDPoS中,网络中的节点有三种角色,分别是“普通选民”、“候选人”、“验证者”:
- 选民:所有节点拥有选民的角色,可以对候选节点进行投票;
- 候选人:需要参与验证人竞选的节点通过注册机制成为候选人,通过注销机制退出验证人竞选;
- 验证人:每轮第一个节点进行检票,检票最高的topK候选人集合成为该轮的验证人,被选举出的每一轮区块生产周期的验证者集合,负责该轮区块的生产和验证,某个时间片内,会有一个矿工进行区块打包,其余的节点会对该区块进行验证。
网络中的三种角色之间是可以相互转换的,转换规则如下:
- 所有地址都具有选民的特性,可以对候选人进行投票;
- 选民经过“候选人提名”提名接口成为候选人,参与竞选;
- 候选人经过“候选人退选”注销接口退出竞选;
- 候选人经过检票产出验证者,得票topK的候选人当选验证者;
- 验证者轮值完恢复候选人或者选民角色;
提名规则
节点想要参与竞选,需要先被提名为候选人,只有被提名的地址才能接受投票。为了收敛候选人集合,并一定程度上增加候选人参与的门槛,提名为候选人会有很多规则,主要有以下几点:
- 提名候选人需要冻结燃料,并且金额不小于系统总金额的十万分之一;
- 该燃料会被一直冻结,直到节点退出竞选;
- 提名支持自提和他提,即允许第三方节点对候选人进行提名;
- 被提名者需要知晓自己被提名,需要对提名交易进行背书;
选举规则
候选人被提名后,会形成一个候选人池子,投票需要针对该池子内部的节点进行。TDPoS的投票也有很多规则,主要有以下几点:
- 任何地址都可以进行投票,投票需要冻结燃料,投票的票数取决于共识配置中每一票的金额,票数 = 冻结金额 / 投票单价;
- 该燃料会被一直冻结,直到该投票被撤销;
- 投票采用博尔达计分法,支持一票多投,每一票最多投给设置的验证者个数,每一票中投给不同候选人的票数相同;
候选人轮值¶
每一轮开始的第一个区块会自动触发检票的交易,该交易会进行下一轮候选人的检票,被选举出的节点会按照既定的时间片协同出块,每一个区块都会请求所有验证节点的验证。TDPoS的时间片切分如下图所示:

为了降低切主时容易造成分叉,TDPoS将出块间隔分成了3个,如上图所示:
- t1:同一轮内同一个矿工的出块间隔;
- t2:同一轮内切换矿工时的出块间隔,需要为t1的整数倍;
- t3:不同轮间切换时的出块间隔,需要为t1的整数倍;
拜占庭容错
TDPoS验证节点轮值过程中,采取了 Chained-Bft 防止矿工节点的作恶。
技术细节¶
TDPoS实现主要在 consensus/tdpos
路径下,其主要是通过智能合约的方式实现的,主要有以下几个合约方法:
1 2 3 4 5 6 7 8 9 | voteMethod = "vote"
// 候选人投票撤销
revokeVoteMethod = "revoke_vote"
// 候选人提名
nominateCandidateMethod = "nominate_candidate"
// 候选人罢黜
revokeCandidateMethod = "revoke_candidate"
// 验证人生成
checkvValidaterMethod = "check_validater"
|
核心接口如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | func (tp *TDpos) runVote(desc *contract.TxDesc, block *pb.InternalBlock) error {
// ......
return nil
}
func (tp *TDpos) runRevokeVote(desc *contract.TxDesc, block *pb.InternalBlock) error {
// ......
return nil
}
func (tp *TDpos) runNominateCandidate(desc *contract.TxDesc, block *pb.InternalBlock) error {
// ......
return nil
}
func (tp *TDpos) runRevokeCandidate(desc *contract.TxDesc, block *pb.InternalBlock) error {
// ......
return nil
}
func (tp *TDpos) runCheckValidater(desc *contract.TxDesc, block *pb.InternalBlock) error {
// ......
return nil
}
|
Single及PoW共识¶
介绍¶
Single以及PoW属于不同类型的区块链共识算法。其中,PoW(Proof Of Work,工作量证明)是通过解决一到特定的问题从而达成共识的区块链共识算法;而Single亦称为授权共识,在一个区块链网络中授权固定的address来记账本。Single一般在测试环境中使用,不适合大规模的应用环境。PoW适用于公有链应用场景。
算法流程¶
Single共识
- 对于矿工:Single是固定 address 周期性出块,因此在调用 CompeteMaster 的时候主要判断当前时间与上一次出块时间间隔是否达到一个周期;
- 对于验证节点:验证节点除了密码学方面必要的验证之外,还会验证矿工与本地记录的矿工是否一致;
Pow共识
- 对于矿工:每次调用 CompeteMaster 都返回 true,表明每次调用 CompeteMaster 的结果都是矿工该出块了;
- 对于验证节点:验证节点除了密码学方面必要的验证之外,还会验证区块的难度值是否符合要求;
在超级链中使用Single或PoW共识¶
只需修改 data/config 中的创世块配置即可指定使用共识
使用Single共识的创世块配置¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | {
"version" : "1",
"consensus" : {
# 共识算法类型
"type" : "single",
# 指定出块的address
"miner" : "dpzuVdosQrF2kmzumhVeFQZa1aYcdgFpN"
},
# 预分配
"predistribution":[
{
"address" : "dpzuVdosQrF2kmzumhVeFQZa1aYcdgFpN",
"quota" : "100000000000000000000"
}
],
# 区块大小限制
"maxblocksize" : "128",
# 出块周期
"period" : "3000",
# 出块奖励
"award" : "428100000000",
# 精度
"decimals" : "8",
# 出块奖励衰减系数
"award_decay": {
"height_gap": 31536000,
"ratio": 1
},
# 系统权限相关配置
"permission": {
"CreateAccount" : { "rule" : "NULL", "acl": {}},
"SetAccountAcl": { "rule" : "NULL", "acl": {}},
"SetContractMethodAcl": { "rule" : "NULL", "acl": {}}
}
}
|
使用PoW共识的创世块配置¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | {
"version" : "1",
# 预分配
"predistribution":[
{
"address" : "Y4TmpfV4pvhYT5W17J7TqHSLo6cqq23x3",
"quota" : "1000000000000000"
}
],
"maxblocksize" : "128",
"award" : "1000000",
"decimals" : "8",
"award_decay": {
"height_gap": 31536000,
"ratio": 0.5
},
"genesis_consensus":{
"name": "pow",
"config": {
# 默认难度值
"defaultTarget": "19",
# 每隔10个区块做一次难度调整
"adjustHeightGap": "10",
"expectedPeriod": "15",
"maxTarget": "22"
}
}
}
|
关键技术¶
Single共识的原理简单,不再赘述。
PoW共识
解决一道难题过程,执行流程如下:
- step1 每隔一个周期判断是否接收到新的区块。若是,跳出解决难题流程,若不是,进行 step2 ;
- step2 判断当前计算难度值是否符合要求。若是,跳出难题解决流程,若不是难度值加1,继续 step1 ;
伪代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | // 在每次挖矿时,设置为true
// StartPowMinning
for {
// 每隔round次数,判断是否接收到新的区块,避免与网络其他节点不同步
if gussCount % round == 0 && !l.IsEnablePowMinning() {
break
}
// 判断当前计算难度值是否符合要求
if valid = IsProofed(block.Blockid, targetBits); !valid {
guessNonce += 1
block.Nonce = guessNonce
block.Blockid, err = MakeBlockID(block)
if err != nil {
return nil, err
}
guessCount++
continue
}
break
}
// valid为false说明还没挖到块
// l.IsEnablePowMinning() == true --> 自己挖出块
// l.IsEnablePowMinning() == false --> 被中断
if !valid && !l.IsEnablePowMinning() {
l.xlog.Debug("I have been interrupted from a remote node, because it has a higher block")
return nil, ErrMinerInterrupt
}
|
计算当前区块难度值过程,执行流程如下:
- step1 判断当前区块所在高度是否比较小。若是,直接复用默认的难度值,跳出计算区块难度值过程,若不是,继续 step2 ;
- step2 获取当前区块的前一个区块的难度值;
- step3 判断当前区块是否在下一个难度调整周期范围内。若是,继续 step4 ;若不是,继续 step5 ;
- step4 获取当前区块的前一个区块的难度值,并计算经历N个区块,预期/实际消耗的时间,并根据公式调整难度值,跳出计算区块难度值过程;
- step5 如果当前区块所在高度在下一次区块难度调整的周期范围内,直接复用前一个区块的难度值,跳出计算区块难度值过程;
伪代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | func (pc *PowConsensus) calDifficulty(curBlock *pb.InternalBlock) int32 {
// 如果当前区块所在高度比较小,直接复用默认的难度值
if curBlock.Height <= int64(pc.config.adjustHeightGap) {
return pc.config.defaultTarget
}
height := curBlock.Height
preBlock, err := pc.getPrevBlock(curBlock, 1)
if err != nil {
pc.log.Warn("query prev block failed", "err", err, "height", height-1)
return pc.config.defaultTarget
}
// 获取当前区块前一个区块的难度值
prevTargetBits := pc.getTargetBitsFromBlock(preBlock)
// 如果当前区块所在高度恰好是难度值调整所在的高度周期
if height%int64(pc.config.adjustHeightGap) == 0 {
farBlock, err := pc.getPrevBlock(curBlock, pc.config.adjustHeightGap)
if err != nil {
pc.log.Warn("query far block failed", "err", err, "height", height-int64(pc.config.adjustHeightGap))
return pc.config.defaultTarget
}
// 经历N个区块,预期消耗的时间
expectedTimeSpan := pc.config.expectedPeriod * (pc.config.adjustHeightGap - 1)
// 经历N个区块,实际消耗的时间
actualTimeSpan := int32((preBlock.Timestamp - farBlock.Timestamp) / 1e9)
pc.log.Info("timespan diff", "expectedTimeSpan", expectedTimeSpan, "actualTimeSpan", actualTimeSpan)
//at most adjust two bits, left or right direction
// 避免难度值调整太快,防止恶意攻击
if actualTimeSpan < expectedTimeSpan/4 {
actualTimeSpan = expectedTimeSpan / 4
}
if actualTimeSpan > expectedTimeSpan*4 {
actualTimeSpan = expectedTimeSpan * 4
}
difficulty := big.NewInt(1)
difficulty.Lsh(difficulty, uint(prevTargetBits))
difficulty.Mul(difficulty, big.NewInt(int64(expectedTimeSpan)))
difficulty.Div(difficulty, big.NewInt(int64(actualTimeSpan)))
newTargetBits := int32(difficulty.BitLen() - 1)
if newTargetBits > pc.config.maxTarget {
pc.log.Info("retarget", "newTargetBits", newTargetBits)
newTargetBits = pc.config.maxTarget
}
pc.log.Info("adjust targetBits", "height", height, "targetBits", newTargetBits, "prevTargetBits", prevTargetBits)
return newTargetBits
} else {
// 如果当前区块所在高度在下一次区块难度调整的周期范围内,直接复用前一个区块的难度值
pc.log.Info("prev targetBits", "prevTargetBits", prevTargetBits)
return prevTargetBits
}
}
|
超级链监管机制¶
监管机制概述¶
超级链是一个具备政府监管能力的区块链系统。在设计上我们需要充分考虑监管和安全问题,做到安全可控。基于此我们超级链底层设计了一个监管合约的机制,通过该机制,超级链具备了对链上用户的实名、交易的安全检查等监管能力。
超级链在初始化时候,可以通过创世块配置的方式,配置这条链是否需要支持监管类型。对于配置了监管合约的链,这个链上所有的事务发起,无论是转账还是合约调用,系统会默认插入监管合约的执行,执行结果体现在读写集中,执行过程不消耗用户资源,执行结果所有节点可验证。
目前超级链支持的监管合约主要有以下几个:
- 实名制合约: identity
- DApp封禁合约: banned
- 合规性检查合约: complianceCheck
- 交易封禁合约: forbidden
下面将会以实名合约为例对监管合约的使用步骤进行说明
监管机制使用说明¶
创世块配置¶
创世块配置新增 reserved_contracts
配置,内容如下:
1 2 3 4 5 6 7 8 | "reserved_contracts": [
{
"module_name": "wasm",
"contract_name": "identity",
"method_name": "verify",
"args":{}
}
]
|
这个配置中配置了 identity
监管合约。
部署Reserved合约¶
1. 编译实名合约
实名合约代码路径如下:core/contractsdk/cpp/reserved/identity.cc
实名合约实名的对象是一个具体的ak。
1 2 3 | cd ./contractsdk/cpp
cp reserved/identity.cc example
./build.sh
|
编译好的产出为 ./build 文件夹下的identity.wasm文件。
2. 创建合约账户
在XuperChain中所有的合约都是部署在具体的某个账户下的,所以,为了部署实名合约,我们需要首先创建一个合约账户,注意,这里账户的拥有者可以修改其内合约Method的ACL权限管理策略,通过这种机制实现对谁可以添加实名状态和删除实名状态的控制。 这里是由超级链的 多节点网络搭建 支持的。
1 2 | # 快速创建合约方式:
./xchain-cli account new --account 1111111111111111
|
3. 部署实名合约
部署合约需要消耗资源,所以先给上述合约账户转移一笔资源,然后在合约内部署上面的合约:
1 2 3 4 5 | # 1 转移资源
./xchain-cli transfer --to XC1111111111111111@xuper --amount 100000
# 2 部署实名合约
# 通过 -a 的creator参数,可以初始化被实名的ak。
./xchain-cli wasm deploy --account XC1111111111111111@xuper --cname identity -H localhost:37101 identity.wasm -a '{"creator":"addr1"}'
|
Note
上述实名合约初始化的被实名的address需要和实名合约添加实名信息保持相同,否则会由于初始实名的ak和添加实名权控不一致而导致系统无法添加新的实名状态。
Reserved合约调用¶
实名合约部署完成后,就可以进行实名合约信息的添加和删除了
1. 添加实名信息
合约调用json文件如下:
1 2 3 4 5 6 7 8 | {
"module_name": "wasm",
"contract_name": "identity",
"method_name": "register_aks",
"args":{
"aks":"ak1,ak2"
}
}
|
具体步骤如下:
1 2 3 4 5 6 | # 1: 生成原始交易
./xchain-cli multisig gen --desc identity_add.json --host localhost:37101 --fee 1000 --output tx_add.out
# 2: 本地签名
./xchain-cli multisig sign --output tx_add_my.sign --tx tx_add.out
# 3: 交易发送
./xchain-cli multisig send tx_add_my.sign --host localhost:37101 --tx tx_add.out
|
2. 删除实名信息
合约调用json文件如下:
1 2 3 4 5 6 7 8 | {
"module_name": "wasm",
"contract_name": "identity",
"method_name": "unregister_aks",
"args":{
"aks":"ak1,ak2"
}
}
|
具体步骤如下:
1 2 3 4 5 6 | # 1: 生成原始交易
./xchain-cli multisig gen --desc identity_del.json --host localhost:37101 --fee 1000 --output tx_del.out
# 2: 本地签名
./xchain-cli multisig sign --output tx_del_my.sign --tx tx_del.out
# 3: 交易发送
./xchain-cli multisig send tx_del_my.sign tx_del_compliance_sign.out --host localhost:37101 --tx tx_del.out
|
3. 实名信息验证
当用户向网络发起事务请求时,网络会验证交易中的 initiator
和 auth_require
字段是否都经过实名,如果都经过实名,则通过,否则,失败。
多盘散列¶
背景¶
区块链中的账本数据通常是只增不减,而单盘存储容量有上限。目前单盘最高容量是14TB左右,需要花费4000块钱;以太坊账本数据已经超过1TB,即使是在区块大小上精打细算的比特币账本也有0.5TB左右。区块链账本数据不断增加,单盘容量上限成为区块链持续发展的天花板。 目前对leveldb的多盘扩展方案,大部分是采用了多个leveldb实例的方式,也就是每个盘一个单独的leveldb实例。这种做法的好处是简单,不需要修改leveldb底层代码,缺点是牺牲了多行原子写入的功能。在区块链的应用场景中,我们是需要这种多个写入操作原子性的。所以选择了改leveldb底层模型的技术路线。
LevelDB数据模型分析¶
- Log文件:写Memtable前会先写Log文件,Log通过append的方式顺序写入。Log的存在使得机器宕机导致的内存数据丢失得以恢复;
- Manifest文件:Manifest文件中记录SST文件在不同Level的分布,单个SST文件的最大最小key,以及其他一些LevelDB需要的元信息;
- Current文件:LevelDB启动时的首要任务就是找到当前的Manifest,而Manifest可能有多个。Current文件简单的记录了当前Manifest的文件名;
以上3种文件可以称之为元数据文件,它们占用的存储空间通常是几十MB,最多不会超过1GB
- SST文件:磁盘数据存储文件。分为Level 0到Level N多层,每一层包含多个SST文件;单个SST文件容量随层次增加成倍增长;文件内数据有序;其中Level0的SST文件由Immutable直接Dump产生,其他Level的SST文件由其上一层的文件和本层文件归并产生;SST文件在归并过程中顺序写生成,生成后仅可能在之后的归并中被删除,而不会有任何的修改操作。

核心改造点¶
Leveldb的数据主要是存储在SST(Sorted String Table)文件中,写放大的产生就是由于compact的时候需要顺序读取Level-N中的sst文件,写出到Level-N+1的sst文件中。 我们将SST文件分散在多块盘上存储,具体的方法是根据sst的编号做取模散列,取模的底数是盘的个数, 理论上数据量和IO压力会均匀分散在多块盘上。
举个例子,假设某sst文件名是12345.ldb,而节点机器有3块盘用于存储(/disk1, /disk2, /disk3),那么就将改sst文件放置在 (12345 % 3) + 1, 也就是disk1盘上

使用方式¶
leveldb.OpenFile 有两个参数,一个是db文件夹路径path,一个是打开参数Options; 如果要使用多盘存储,调用者需要设置 Options.DataPaths 参数,它是一个[]string 数组,声明了各个盘的文件夹路径,可参考 配置多盘存储 。
扩容问题¶
假设本来是N块盘,扩容后是(N+M)块盘。对于已有的sst文件,因为取模的底数变了, 可能会出现按照原有的取模散列不命中的情况。 规则是:
- 对于读Open,先按照 (N+M) 取模去Open,如果不存在,则遍历各盘直到能Open到相应的文件,由于Open并不是频繁操作,代价可接受,且SST的编号是唯一且递增的,所以不存在读取脏数据的问题;
- 对于写Open,就按照 (N+M) 取模,因为写Open一定是生成新的文件。
随着Compact的不断进行,整个数据文件的分布会越来越趋向于均匀分布在 (N+M) 个盘,扩容完成。
实验¶
平行链与群组¶
术语¶
- 平行链 :相对于主链而言,运行在超级链中的用户级区块链实例,用户通过调用主链的智能合约创建。功能与主链无区别,全网节点均可以获取平行链账本数据,实现整体架构水平可扩展。
- 群组 :作用于平行链,具备群组特性的平行链,只有特定节点才拥有该平行链的账本数据。群组具备的特性包括私密性、动态性。
设计思路¶
如何要支持群组,需要在xuper链部署一个系统合约:GroupChain(一个网络有且仅有一个)
这样是为了保证兼容性,如果没有部署这个GroupChain合约,那么行为和旧版本一致。
<平行链名字,IP> → Address
为什么把IP放在Key中,是为了方便做过滤的时候查找更快,直接Get。
平行链名字作为前缀,方便列出这条链的所有合法成员节点。
备注:此处IP是代指一个TCP协议定位符,可以是libp2p风格的URL。
查询特定的链是否具备群组关系
Case1: 部分链希望有群组特性,即只有特定的节点才能同步账本数据;
Case2: 剩下的链还是期望所有节点都参与同步、验证区块;
基于以上两种场景,需要增加一层映射,即<平行链,是否支持群组>
如果每次转发都Lookup数据库过滤IP,性能有影响,可以考虑在p2p中维护一个Cache;
通过这个智能合约接口,可以修改(address, IP)的映射关系
合约的Owner(GroupChain这个合约的Owner)可以添加或删除address
节点也可以后期自己修改IP(节点有权更换自己的IP),合约里面会判断Initiator()字段和address是否一致,确保每个address只能修改自己的IP
平行链中转消息的时候,必须确保目的IP在智能合约的映射表中存在
如果每次转发都Lookup数据库过滤IP,性能有影响,可以考虑在p2p中维护一个Cache;
合约账号¶
访问控制列表(ACL)¶
如果把合约账号当作一家股份制公司,那么ACL便是公司股东投票的机制,ACL可以规定合约账号背后各“股东”账号的权重,只有当“股东”签名的权重之和大于设定阈值时操作才会有效地进行。
超级链中ACL配置格式如下:
1 2 3 4 5 6 7 8 9 10 | {
"pm": {
"rule": 1, # rule=1表示签名阈值策略,rule=2表示AKSet签名策略
"acceptValue": 0.6 # acceptValue为签名需达到的阈值
},
"aksWeight": { # aksWeight里规定了每个地址对应账号签名的权重
"AK1": 0.3,
"AK2": 0.3
}
}
|
了解了访问控制列表的概念,下面我们就来演示一下如何创建一个合约账号
合约账号创建¶
Xchain的客户端工具提供了新建账号的功能,基本用法如下:
1 | xchain-cli account new --desc account.des
|
这里的 account.des 就是创建账号所需要的配置了,内容如下:
1 2 3 4 5 6 7 8 9 | {
"module_name": "xkernel",
"method_name": "NewAccount",
"args" : {
"account_name": "1111111111111111", # 说明:账号名称是16位数字组成的字符串
# acl 中的内容注意转义
"acl": "{\"pm\": {\"rule\": 1,\"acceptValue\": 0.6},\"aksWeight\": {\"AK1\": 0.3,\"AK2\": 0.3}}"
}
}
|
命令运行后就会调用xchain的系统合约功能 NewAccount
创建一个名为 XC1111111111111111@xuper
(如果链名字为xuper)的账号

新建合约账号
除了上述方法,我们还提供了一个比较简易的方式来创建合约账号,命令如下:
1 | xchain-cli account new --account 1111111111111111 # 16位数字组成的字符串
|
上述命令也会创建一个名为 XC1111111111111111@xuper
的账号,由于我们没有制定ACL的具体内容,其ACL被赋值为默认状态,即背后有权限的账号只有当前节点上默认账号一个(地址默认位于 data/keys/address)
Note
创建合约账号的操作需要提供手续费,需要按照命令行运行结果给出的数值,添加一个不小于它的费用(使用 –fee 参数)
合约账号基本操作¶
查询账号ACL¶
XuperChain的客户端工具提供了ACL查询功能,只需如下命令
1 | xchain-cli acl query --account XC1111111111111111@xuper # account参数为合约账号名称
|

查询合约账号ACL
查询账号余额¶
合约账号查询余额和普通账号类似,只是命令行的参数有些许变化
1 | ./xchain-cli account balance XC1111111111111111@xuper -H 127.0.0.1:37101
|
使用此命令即可查询`XC1111111111111111@xuper`的余额

查询合约账号余额
修改账号ACL¶
修改ACL的配置和创建账号的配置类似
1 2 3 4 5 6 7 8 9 | {
"module_name": "xkernel",
"method_name": "SetAccountAcl", # 这里的方法有了变更
"args" : {
"account_name": "1111111111111111",
# acl字段为要修改成的新ACL
"acl": "{\"pm\": {\"rule\": 1,\"acceptValue\": 0.6},\"aksWeight\": {\"AK3\": 0.3,\"AK4\": 0.3}}"
}
}
|
修改ACL的操作,需要符合当前ACL中设置的规则,即需要具有足够权重的账号签名
我们首先生成一个多重签名的交易
1 | ./xchain-cli multisig gen --desc acl_new.json --from XC1111111111111111@xuper
|

成多重签名交易
这样就会生成一个默认为`tx.out`的文件,之后使用原ACL中的账号对其进行签名
1 2 | ./xchain-cli multisig sign --keys data/account/AK1 --output AK1.sign
./xchain-cli multisig sign --keys data/account/AK2 --output AK2.sign
|

签名交易
最后把生成的`tx.out`发出去
1 | ./xchain-cli multisig send --tx tx.out AK1.sign,AK2.sign AK1.sign,AK2.sign
|

发送交易
至此便完成了ACL的修改
多节点部署¶
在阅读本节前,请先阅读“快速入门”,当中介绍了创建单节点网络的创建,在该基础上,搭建一个SINGLE共识的多节点网络,其他节点只要新增p2p网络bootNodes配置即可。如果你想搭建一个TDPoS共识的链,仅需要修改创世块参数中“genesis_consensus”配置参数即可。下面将详细介绍相关操作步骤。
p2p网络配置¶
我们以搭建3个节点的网络为例来说明(其实搭建更多节点的原理是一致的),首先需要有一个节点作为“bootNode”,其他节点启动前都配置这个“bootNode”的地址即可实现
对于bootNode节点,我们需要先获取它的netUrl,具体命令如下:
1 | ./xchain-cli netUrl get -H 127.0.0.1:37101
|
如果不是以默认配置启动的,我们需要先生成它的netUrl,然后再获取
1 | ./xchain-cli netUrl gen -H 127.0.0.1:37101
|
如此我们会获得一个类似于
/ip4/127.0.0.1/tcp/47101/p2p/QmVxeNubpg1ZQjQT8W5yZC9fD7ZB1ViArwvyGUB53sqf8e
样式的返回
对其他的节点,我们需要修改其服务配置 conf/xchain.yaml 中p2pv2一节
1 2 3 4 5 6 7 | p2pV2:
// port是节点p2p网络监听的默认端口,如果在一台机器上部署注意端口配置不要冲突,
// node1配置的是47101,node2和node3可以分别设置为47102和47103
port: 47102
// 节点加入网络所连接的种子节点的链接信息,
bootNodes:
- "/ip4/127.0.0.1/tcp/47101/p2p/QmVxeNubpg1ZQjQT8W5yZC9fD7ZB1ViArwvyGUB53sqf8e"
|
Note
需要注意的是,如果节点分布在不同的机器之上,需要把netUrl中的本地ip改为机器的实际ip
修改完配置后,即可在每一个节点使用相同配置创建链,然后分别启动bootNode和其他节点,即完成了多节点环境的部署
这里可以使用系统状态的命令检查环境是否正常
1 | ./xchain-cli status -H 127.0.0.1:37101
|
通过变更 -H 参数,查看每个节点的状态,若所有节点高度都是一致变化的,则证明环境部署成功
搭建TDPoS共识网络¶
XuperChain系统支持可插拔共识,通过修改创世块的参数,可以创建一个以TDPoS为共识的链。
下面创世块配置(一般位于 core/data/config/xuper.json)和单节点创世块配置的区别在于创世共识参数genesis_consensus的config配置,各个配置参数详解配置说明如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | {
"version" : "1",
"predistribution":[
{
"address" : "mahtKhdV5SZP4FveEBzX7j6FgUGfBS9om",
"quota" : "100000000000000000000"
}
],
"maxblocksize" : "128",
"award" : "1000000",
"decimals" : "8",
"award_decay": {
"height_gap": 31536000,
"ratio": 1
},
"genesis_consensus": {
"name": "tdpos",
"config": {
# tdpos共识初始时间,声明tdpos共识的起始时间戳,建议设置为一个刚过去不旧的时间戳
"timestamp": "1548123921000000000",
# 每一轮选举出的矿工数,如果某一轮的投票不足以选出足够的矿工数则默认复用前一轮的矿工
"proposer_num":"3",
# 每个矿工连续出块的出块间隔
"period":"3000",
# 每一轮内切换矿工时的时间间隔,需要为period的整数倍
"alternate_interval":"6000",
# 切换轮时的出块间隔,即下一轮第一个矿工出第一个块距离上一轮矿工出最后一个块的时间间隔,需要为period的整数配
"term_interval":"9000",
# 每一轮内每个矿工轮值任期内连续出块的个数
"block_num":"200",
# 为被提名的候选人投票时,每一票单价,即一票等于多少Xuper
"vote_unit_price":"1",
# 指定第一轮初始矿工,矿工个数需要符合proposer_num指定的个数,所指定的初始矿工需要在网络中存在,不然系统轮到该节点出块时会没有节点出块
"init_proposer": {
"1":["RU7Qv3CrecW5waKc1ZWYnEuTdJNjHc43u","XpQXiBNo1eHRQpD9UbzBisTPXojpyzkxn","SDCBba3GVYU7s2VYQVrhMGLet6bobNzbM"]
}
}
}
}
|
修改完每个节点的创世块配置后,需要确认各节点的 data/blockchain 目录下内容为空。然后重新按照上一节的步骤,在各节点上创建链,启动所有节点,即完成TDPoS共识的环境部署。
选举TDPOS候选人¶
选举候选人包括提名和投票两个环节,具体操作和 发起提案 类似
提名候选人¶
首先需要准备一个提名的配置,json格式
1 2 3 4 5 6 7 8 9 10 | {
"module": "tdpos",
"method": "nominate_candidate",
"args": {
# 此字段为要提名的候选人的地址
"candidate": "kJFcY3FjmNU8xk6cRzHvTPmChUQ3SBGVE",
# 此字段为候选人节点的netURL
"neturl": "/ip4/10.0.4.6/tcp/47101/p2p/QmRmdBSyHpKPvhsvmyys8f1jDM4x1S9cbCwZaBMqMKjwhV"
}
}
|
然后将这个json文件(假定文件名为nominate.json)通过多重签名命令发出。提名候选人的操作需要提名者和被提名候选人的两个签名(如果是自己提名自己,那么就只需要一个签名了)
首先要准备一个需收集签名的地址列表,可以参考 发起多重签名交易
1 2 | YDYBchKWXpG7HSkHy4YoyzTJnd3hTFBgG
kJFcY3FjmNU8xk6cRzHvTPmChUQ3SBGVE
|
然后生成一个提名交易,超级链上进行候选人提名需要冻结大于链上资产总量的十万分之一的utxo(当前的总资产可以通过 status查询命令 查看结果的utxoTotal字段)
1 2 | # 这里转账的目标地址可以任意,转给自己也可以,注意冻结参数为-1,表示永久冻结
./xchain-cli multisig gen --to=dpzuVdosQrF2kmzumhVeFQZa1aYcdgFpN --desc=nominate.json --amount=10000000000000000 --frozen -1 -A addr_list --output nominate.tx
|
命令会生成交易内容,然后对其进行签名
1 2 3 4 | # 提名者签名
./xchain-cli multisig sign --tx nominate.tx --output nominate.sign --keys path/to/nominate
# 候选人签名
./xchain-cli multisig sign --tx nominate.tx --output candidate.sign --keys path/to/candidate
|
然后将生成的交易发送
1 2 | # send 后面的签名有两个参数,第一个为发起方的签名,第二个为需要收集的签名(列表逗号分隔)
./xchain-cli multisig send --tx nominate.tx nominate.sign nominate.sign,candidate.sign
|
发送交易会返回一个txid,这里需要记录下来,后面可能会用到
投票¶
投票的配置也是一个json格式
1 2 3 4 5 6 7 8 | {
"module": "tdpos",
"method": "vote",
"args": {
# 提名过的address
"candidates":["RU7Qv3CrecW5waKc1ZWYnEuTdJNjHc43u"]
}
}
|
同样使用转账的命令发出,注意投票的utxo需要永久冻结。
1 2 | # 同样,转账目标地址可任意填写,转给自己也可以
./xchain-cli transfer --to=dpzuVdosQrF2kmzumhVeFQZa1aYcdgFpN --desc=vote.json --amount=1 --frozen -1
|
根据共识算法配置的候选人集合大小(上面配置中的”proposer_num”字段,假设为n),每一轮出块结束后系统都会查看被提名的候选人数目是否达到n,如果没有达到则继续按上一轮的顺序出块;如果达到n则会统计得票靠前的n个节点为新一轮的矿工集合
Note
细心的读者可能已经发现这些配置文件的json key 都类似,可以参考 xuperchain/core/contract/contract.go 中TxDesc的定义
撤销提名 && 撤销投票¶
Json格式的配置又来了
1 2 3 4 5 6 7 8 | {
"module":"proposal",
"method": "Thaw",
"args" : {
# 此处为提名或者投票时的txid,且address与提名或者投票时需要相同
"txid":"02cd75a721f2589a3ff6768b49650b46fa0b042f970df935b4d28a15aa19e49a"
}
}
|
然后使用转账操作发出(注意address一致),撤销提名/投票后,当时被冻结的资产会解冻,可以继续使用了
1 | ./xchain-cli transfer --to=dpzuVdosQrF2kmzumhVeFQZa1aYcdgFpN --desc=thaw.json --amount=1
|
TDPOS结果查询¶
超级链的客户端提供了这一功能
- 查询候选人信息
./xchain-cli tdpos query-candidates
- 查看某一轮的出块顺序
./xchain-cli tdpos query-checkResult -t=30
- 查询提名信息:某地址发起提名的记录
./xchain-cli tdpos query-nominate-records -a=dpzuVdosQrF2kmzumhVeFQZa1aYcdgFpN
- 被提名查询:某个候选人被提名的记录
./xchain-cli tdpos query-nominee-record -a=RU7Qv3CrecW5waKc1ZWYnEuTdJNjHc43u
- 某选民的有效投票记录
./xchain-cli tdpos query-vote-records -a=dpzuVdosQrF2kmzumhVeFQZa1aYcdgFpN
- 某候选人被投票记录
./xchain-cli tdpos query-voted-records -a=RU7Qv3CrecW5waKc1ZWYnEuTdJNjHc43u
各种查询命令的详细参数列表可以通过 ./xchain-cli tdpos -h 查询
常见问题¶
- 端口冲突:注意如果在一台机器上部署多个节点,各个节点的RPC监听端口以及p2p监听端口都需要设置地不相同,避免冲突;
- 节点公私钥和节点netUrl冲突:注意网络中不同节点./data/keys下的文件和./data/netkeys下的内容都应该不一样,这两个文件夹是节点在网络中的唯一标识,每个节点需要独自生成,否则网络启动异常;
- 启动时链接bootNodes节点失败:注意要先将bootNodes节点启动,再起动其他节点,否则会因为加入网络失败而启动失败;
- 遇到The gas you cousume is: XXXX, You need add fee 通过加–fee XXXX 参数附加资源;
创建合约¶
编写合约¶
源码可以参考 xuperchain/core/contractsdk/go/example/math/math.go
主要实现struct中initialize,invoke和query三个方法来实现自己的逻辑
1 2 3 | func (m *math) Initialize(nci code.Context) code.Response { ... }
func (m *math) Invoke(nci code.Context) code.Response { ... }
func (m *math) Query(nci code.Context) code.Response { ... }
|
每个函数的入口参数均为 code.Context ,具体结构可参考 xuperchain/core/contractsdk/go/code/context.go 接口中定义了如何获取传入方法的参数,如何使用读写功能,以及如何在链上进行交易/区块的查询、转账或调用其他合约
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | type Context interface {
Args() map[string][]byte
Caller() string
Initiator() string
AuthRequire() []string
PutObject(key []byte, value []byte) error
GetObject(key []byte) ([]byte, error)
DeleteObject(key []byte) error
NewIterator(start, limit []byte) Iterator
QueryTx(txid []byte) (*TxStatus, error)
QueryBlock(blockid []byte) (*Block, error)
Transfer(to string, amount *big.Int) error
Call(module, contract, method string, args map[string][]byte) (*Response, error)
}
|
对于C++版本的合约,可以参考代码 contractsdk/cpp/example/counter.cc 原理和Golang合约是一致的
Note
除了 Initialize 外的其他函数,是可以自行定义函数名的,可参考contractsdk/go/example/counter/counter.go中的具体实例,在之后调用合约时写明函数名即可
部署wasm合约¶
编译合约 - Golang
注意合约编译环境与源码编译环境一致,编译参数如下
GOOS=js GOARCH=wasm go build XXX.go
编译合约 - C++
对于C++合约,已提供编译脚本,位于 contractsdk/cpp/build.sh,需要注意的是,脚本依赖从hub.baidubce.com拉取的docker镜像,请在编译前确认docker相关环境是可用的
部署wasm合约
将编译好的合约二进制文件(以counter为例)放到目录node/data/blockchain/${chain name}/wasm/下,这里我们默认的链名 ${chain name}=xuper
部署合约的操作需要由合约账号完成,部署操作同样需要支付手续费,操作前需要确保合约账号下有足够的余额
示例中我们的环境里创建了一条名为xuper的链,包含一个合约账号 XC1111111111111111@xuper
账号的acl查询如下:
为部署合约,我们需要事先准备一个符合权限的地址列表(示例中将其保存在 data/acl/addrs 文件),这里因为acl里只有一个AK,我们只需在文件中添加一行(如果acl中需要多个AK,那么编辑文件,每行填写一个即可)
echo "XC1111111111111111@xuper/dpzuVdosQrF2kmzumhVeFQZa1aYcdgFpN" > data/acl/addrs
然后我们按照以下命令来部署wasm合约counter
./xchain-cli wasm deploy --account XC1111111111111111@xuper --cname counter -m -a '{"creator": "someone"}' -A data/acl/addrs -o tx.output --keys data/keys --name xuper -H localhost:37101 counter
此命令看起来很长,但是其中很多参数都有默认值,我们先来看一下参数的含义:
wasm deploy
:此为部署wasm合约的命令参数,不做过多解释--account XC1111111111111111@xuper
:此为部署wasm合约的账号(只有合约账号才能进行合约的部署)--cname counter
:这里的counter是指部署后在链上的合约名字,可以自行命名(但有规则,长度在4~16字符)-m
:意为多重签名的方式,目前版本的xchain部署wasm合约都需要以这种方式-a '{"creator": "someone"}'
:此为传入合约的参数,供合约Initialize方法使用(此参数并非必须,只不过此处的counter合约需要传一个”creator”参数,参见contractsdk/cpp/example/counter.cc)-A data/acl/addrs
:此即为需要收集签名的列表,默认路径为data/acl/addrs,如不是则需要显式传入(注意权重要满足acl要求)-o tx.output
:此为输出的tx文件,可不传,默认文件名为tx.out--keys data/keys
:此为部署发起者的密钥地址,可不传,默认值即为data/keys(部署发起者也要进行签名)--name xuper
:此为区块链名称,默认为xuper,如果创建链名称不是xuper则需要显式传入-H localhost:37101
:xchain服务的地址,默认是本机的37101端口,如不是则需要显式传入- 最后的counter是合约编译好的文件(编译完成默认是counter.wasm)
在此处,我们大部分参数取的是默认值,所以命令参数不必这么多了
./xchain-cli wasm deploy --account XC1111111111111111@xuper --cname counter -m -a '{"creator": "someone"}' counter
运行效果如下
运行时会提示手续费的数目,使用 –fee 参数传入即可
然后收集所需AK的签名,因为示例中我们只有一个AK(同时也是发起者),所以只需要签名一次
./xchain-cli multisig sign --tx tx.out --output sign.out --keys data/keys
这里的
--output
--keys
参数也有默认值(输出到sign.out文件,密钥位于data/keys),可以不加。运行后我们即可获得此AK的签名运行效果如下
收集完发起者和acl需要的签名后,我们即可发送交易,完成合约部署了
./xchain-cli multisig send --tx tx.out sign.out sign.out
这里 multisig send 为发送多重签名的命令参数,
--tx
是交易文件,后边的两个参数分别为发起者的签名和acl的签名(acl中有多个AK时,用逗号连接多个签名文件)。运行命令可得到交易上链后的id,我们也可以使用以下命令来查询部署结果./xchain-cli account contracts --account XC1111111111111111@xuper
会显示此合约账号部署过的所有合约
运行效果如下
发起提案¶
XuperChain中有多种提案-投票操作场景,但原理都是一致的,我们以通过提案更改共识算法(single改为tdpos)来介绍具体的操作流程
部署一个Single共识的超级链环境已经在“快速入门”一节有介绍
首先我们需要准备一个tdpos共识的配置,包括出块时间、代表名单等(假设文件名为proposal.json)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | {
"module": "proposal",
"method": "Propose",
"args" : {
"min_vote_percent": 51, # 生效的资源比例
"stop_vote_height": 800 # 计票截至的高度
},
"trigger": {
"height": 1000, # 期望生效的高度
"module": "consensus",
"method": "update_consensus",
"args" : {
"name": "tdpos",
"config": {
"version":"2",
"proposer_num":"2", # 代表个数
"period":"3000",
"alternate_interval":"6000",
"term_interval":"9000",
"block_num":"20",
"vote_unit_price":"1",
"init_proposer": { # 出块的代表名单
"1":["dpzuVdosQrF2kmzumhVeFQZa1aYcdgFpN", "U5SHuTiiSP1JAAHVMknqrm66QXk2VhXsK"]
}
}
}
}
}
|
需要注意的是当前的区块高度,来设置合理的截至计票高度和生效高度。然后在矿工节点下,执行给自己转账的操作,并在 –desc 参数里传入提案
1 | ./xchain-cli transfer --to dpzuVdosQrF2kmzumhVeFQZa1aYcdgFpN --desc proposal.json
|
运行后会得到本次提案的交易id,需要记录下来供投票使用
对提案进行投票操作由如下命令执行
1 | ./xchain-cli vote f26d670b695d9fd5da503a34d130ef19e738b35e031b18b70ad4cbbf6dfe2656 --frozen 1100 --amount 100002825031900000000
|
这里需要注意进行投票的节点需要有矿工账号的密钥对,以及 –frozen 参数的冻结高度大于提案生效的高度。因为最终通过的规则是投票资源大于总资源的51%,所以需要初始token量最多的矿工账号来进行投票,并保证token数符合要求。
如此进行后,等到区块出到设定的生效高度,便完成了提案-投票的整个流程。其他场景的提案机制都是类似的,仅是json配置文件不同而已。
配置变更¶
配置多盘存储¶
由区块链本身特点决定的,区块链服务启动后需要的存储空间会逐渐变多,即使交易不频繁,每到固定出块时间也会占用少量的存储空间。XuperChain提供了一种可以将存储路径配置在多个磁盘上的功能,来更好地支持单个磁盘存储空间不充裕的场景。
位于代码目录下的 core/conf/xchain.yaml ,包含了大部分XuperChain服务启动的配置项,其中有磁盘相关的章节
1 2 3 4 5 6 7 8 | # 数据存储路径
datapath: ./data/blockchain
# 多盘存储的路径
datapathOthers:
- /ssd1/blockchain
- /ssd2/blockchain
- /ssd3/blockchain
|
只需将“多盘存储路径”部分去掉注释,便可以灵活配置多个数据存储位置。
替换扩展插件¶
XuperChain采用了动态链接库的方式实现了加密、共识算法等扩展插件,可以根据实际使用场景进行替换。
插件目录位于 plugins ,对应的配置文件为 conf/plugins.conf (json格式)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | {
"crypto":[{
"subtype": "default",
"path": "plugins/crypto/crypto-default.so.1.0.0",
"version": "1.0.0",
"ondemand": false
},{
"subtype": "schnorr",
"path": "plugins/crypto/crypto-schnorr.so.1.0.0",
"version": "1.0.0",
"ondemand": false
}]
# ......
}
|
需要替换插件则修改对应的 .so 文件路径即可
使用平行链与群组¶
创建平行链¶
现在超级链中创建平行链的方式是:发起一个系统智能合约,发到xuper链。
当前xchain.yaml有两个配置项:
1 2 3 4 5 6 | Kernel:
# minNewChainAmount 设置创建平行链时最少要转多少utxo(门槛)到同链名的address
minNewChainAmount: “100”
# newChainWhiteList 有权创建平行链的address白名单
newChainWhiteList:
- dpzuVdosQrF2kmzumhVeFQZa1aYcdgFpN: true
|
创建平行链的json文件(模版),如下:
1 2 3 4 5 6 7 8 | {
"Module": "kernel",
"Method": "CreateBlockChain",
"Args": {
"name": "HelloChain",
"data": "{\"version\": \"1\", \"consensus\": {\"miner\":\"dpzuVdosQrF2kmzumhVeFQZa1aYcdgFpN\", \"type\":\"single\"},\"predistribution\":[{\"address\": \"dpzuVdosQrF2kmzumhVeFQZa1aYcdgFpN\",\"quota\": \"1000000000000000\"}],\"maxblocksize\": \"128\",\"period\": \"3000\",\"award\": \"1000000\"}"
}
}
|
使用如下指令即可创建平行链:
1 | ./xchain-cli transfer --to HelloChain --amount 100 --desc createChain.json
|
获取group_chain合约¶
超级链提供了默认的群组合约(group_chain)的实现,路径为 core/contractsdk/cpp/example/group_chain.cc 。在 core/contractsdk/cpp 目录下执行 sh build.sh 即可编译生成 group_chain.wasm ,即可使用 group_chain.wasm 实现群组合约的部署。
创建群组¶
如果希望创建的平行链只在自己希望的小范围使用,那么可以参考此节配置群组功能
当前超级链中创建群组的方式是:在xuper链上部署GroupChain智能合约,将节点白名单加到GroupChain合约中。
在创世块中配置群组合约配置:
1 2 3 4 5 6 7 8 | {
"group_chain_contract": {
"module_name": "wasm",
"contract_name": "group_chain",
"method_name": "list",
"args":{}
}
}
|
如果需要确保HelloChain具备群组属性,且白名单为<ip1,addr1>,<ip2,addr2>,其他节点不能获取这条平行链的信息,可以按如下操作
step1: 在xuper链部署GroupChain合约
1 2 | # 需要使用合约账号,部署编译好的合约文件
./xchain-cli wasm deploy --account XC1111111111111111@xuper --cname group_chain ./group_chain.wasm --fee xxx
|
step2: 调用GroupChain合约的AddNode方法将<ip1,add1>,<ip2,add2>加入白名单
1 2 | ./xchain-cli wasm invoke group_chain --method addNode -a '{"bcname":"HelloChain", "ip":"ip1", "address":"addr1"}'
./xchain-cli wasm invoke group_chain --method addNode -a '{"bcname":"HelloChain", "ip":"ip2", "address":"addr2"}'
|
step3: 调用GroupChain合约的AddChain确保HelloChain具备群组特性
1 | ./xchain-cli wasm invoke group_chain --method addChain -a '{"bcname":"HelloChain"}'
|
至此即完成了群组的设置,只有<ip1,add1>,<ip2,add2>两个节点可以获取平行链HelloChain的内容了。
电子存证合约¶
问题引入¶
假设我们面临着这样的一个问题:“几个摄影师朋友找到你,他们的摄影作品上传到自己的blog后总是被其他人盗用,使用水印之类的方法也无法避免像截取部分这种情况,他们需要一个能证明摄影作品最早是由自己上传、而且具有法律效力可供自己进行维权的工具”
显然区块链对于解决此问题有很大的帮助,它的不可篡改等特性很适合存证维权的场景,我们可以通过XuperChain来构建一个存取证据的智能合约(担心不被法院认可? 这里 或许能够解答你的疑问)
下面我们就来教你帮助摄影师朋友开发一个能够存储照片版权、还能在发现被盗用后进行维权的智能合约
数据结构的设计¶
对于摄影作品,通常是一个图片文件,其大小根据清晰度等原因可以多达几十MB(甚至更多),为避免存储空间浪费、以及保证区块链交易的效率,我们可以使用哈希算法(例如SHA256)只将图片的哈希值上链,而原图可以保存在其他地方
我们可以这样定义“证据文件”的数据结构,包含哈希值和上传的时间戳
1 2 3 4 | type UserFile struct {
Timestamp int64
Hashval []byte
}
|
为了能够存储多个“证据文件”,并且能够服务于更多的摄影师朋友,我们可以定义一个上传者到文件的map
1 2 3 4 | type User struct {
Owner string
UserFiles map[string]*UserFile
}
|
代码样例可以参看:contractsdk/go/example/eleccert.go
电子存证合约的功能实现¶
从场景我们可以大致推断,以下两个功能是必要的
- 存储一个到“证据文件”区块链(save方法)
- 获取已经存储过的某一个“证据文件”(query方法)
更底层考虑,我们可以使用XuperChain提供的合约SDK功能 PutObject 和 GetObject 来提供实际的存取功能
对于XuperChain中的智能合约,Initialize是一个必须实现的方法,当且仅当合约被部署的时候会运行一次,我们这里采用“每个摄影师部署自己的合约来存储自己需要的作品”这种方式,将一些和上传者相关的初始化操作放在函数中
Save、Query和Initialize方法的具体实现可以参考代码样例
合约使用方法¶
合约执行(Save)¶
执行合约进行“存证操作”的命令如下(运行需要使用 –fee 参数提供资源消耗):
1 | ./xchain-cli wasm invoke -a '下面json中args字段的内容' --method save -H localhost:37101 eleccert
|
1 2 3 4 5 6 7 8 9 10 | {
"module_name": "wasm", # native or wasm
"contract_name": "eleccert", # contract name
"method_name": "save", # invoke or query
"args": {
"owner": "aaa", # user name
"filehash": "存证文件的hash值",
"timestamp": "存证的timestamp"
}
}
|
合约查询(Query)¶
执行合约进行“取证操作”的命令如下(查询操作不需要提供资源):
1 | ./xchain-cli wasm query -a 'args内容' --method query -H localhost:37101 eleccert
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | {
"module_name": "native", # native or wasm
"contract_name": "eleccert", # contract name
"method_name": "query", # invoke or query
"args": {
"owner": "aaa", # user name
"filehash": "文件hash值"
}
}
# output 如下
{
"filehash": "文件hash值",
"timestamp": "文件存入timestamp"
}
|
数字资产交易¶
代码样例参看:contractsdk/go/example/erc721.go
ERC721简介¶
ERC721是数字资产合约,交易的商品是非同质性商品。其中,每一份资产,也就是token_id都是独一无二的类似收藏品交易。
ERC721具备哪些功能¶
- 通过initialize方法,向交易池注入自己的token_id
- 注意token_id必须是全局唯一
- 通过invoke方法,执行不同的交易功能
- transfer: userA将自己的某个收藏品token_id转给userB
- approve: userA将自己的某个收藏品token_id的售卖权限授予userB
- transferFrom: userB替userA将赋予权限的收藏品token_id卖给userC
- pproveAll: userA将自己的所有收藏品token_id的售卖权限授予userB
- 通过query方法,执行不同的查询功能
- balanceOf: userA的所有收藏品的数量
- totalSupply: 交易池中所有的收藏品的数量
- approvalOf: userA授权给userB的收藏品的数量
调用json文件示例¶
Initialize
./xchain-cli wasm invoke -a ‘下面json中args字段的内容’ –method initialize -H localhost:37101 erc721
1 2 3 4 5 6 7 8 9 | {
"module_name": "native", # native或wasm
"contract_name": "erc721", # contract name
"method_name": "initialize", # initialize or query or invoke
"args": {
"from": "dudu", # userName
"supply": "1,2" # token_ids
}
}
|
Invoke
./xchain-cli native invoke -a ‘args内容’ –method invoke -H localhost:37101 erc721
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | {
"module_name": "native", # native或wasm
"contract_name": "erc721", # contract name
"method_name": "invoke", # initialize or query or invoke
"args": {
"action": "transfer", # action name
"from": "dudu", # usera
"to": "chengcheng", # userb
"token_id": "1" # token_ids
}
}
{
"module_name": "native", # native或wasm
"contract_name": "erc721", # contract name
"method_name": "invoke", # initialize or query or invoke
"args": {
"action": "transferFrom", # action name
"from": "dudu", # userA
"caller": "chengcheng", # userB
"to": "miaomiao", # userC
"token_id": "1" # token_ids
}
}
{
"module_name": "native", # native或wasm
"contract_name": "erc721", # contract name
"method_name": "invoke", # initialize or query or invoke
"args": {
"action": "approve", # action name
"from": "dudu", # userA
"to": "chengcheng", # userB
"token_id": "1" # token_ids
}
}
|
Query
./xchain-cli native query -a ‘args内容’ –method query -H localhost:37101 erc721
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | {
"module_name": "native", # native或wasm
"contract_name": "erc721", # contract name
"method_name": "query", # initialize or query or invoke
"args": {
"action": "balanceOf", # action name
"from": "dudu" # userA
}
}
{
"module_name": "native", # native或wasm
"contract_name": "erc721", # contract name
"method_name": "query", # initialize or query or invoke
"args": {
"action": "totalSupply" # action name
}
}
{
"module_name": "native", # native或wasm
"contract_name": "erc721", # contract name
"method_name": "query", # initialize or query or invoke
"args": {
"action": "approvalOf", # action name
"from": "dudu", # userA
"to": "chengcheng" # userB
}
}
|
智能合约SDK使用说明¶
XuperChain为方便用户开发属于自己的智能合约,提供了一整套SDK套件,即XuperCDT(XuperChain Crontract Development Toolkit),包含C++语言和Go语言
C++接口API¶
get_object¶
bool ContextImpl::get_object(const std::string& key, std::string* value)
输入
参数 | 说明 |
---|---|
key | 查询的key值 |
value | 根据key查到的value值 |
输出
参数 | 说明 |
---|---|
true | key值查询成功,返回value值 |
false | key值不存在 |
put_object¶
bool ContextImpl::put_object(const std::string& key, const std::string& value)
输入
参数 | 说明 |
---|---|
key | 存入的key值 |
value | 存入key值对应的value值 |
输出
参数 | 说明 |
---|---|
true | 存入db成功 |
false | 存入db失败 |
delete_object¶
bool ContextImpl::delete_object(const std::string& key)
输入
参数 | 说明 |
---|---|
key | 将要删除的key值 |
输出
参数 | 说明 |
---|---|
true | 删除成功 |
false | 删除失败 |
query_tx¶
bool ContextImpl::query_tx(const std::string &txid, Transaction* tx)
输入
参数 | 说明 |
---|---|
txid | 待查询的txid |
tx | 得到此txid的transaction |
输出
参数 | 说明 |
---|---|
true | 查询交易成功 |
false | 查询交易失败 |
query_block¶
bool ContextImpl::query_block(const std::string &blockid, Block* block)
输入
参数 | 说明 |
---|---|
blockid | 待查询的blockid |
block | 得到此blockid的block |
输出
参数 | 说明 |
---|---|
true | 查询block成功 |
false | 查询block失败 |
table¶
定义表格¶
1 2 3 4 5 6 7 8 9 10 | // 表格定义以proto形式建立,存放目录为contractsdk/cpp/pb
syntax = "proto3";
option optimize_for = LITE_RUNTIME;
package anchor;
message Entity {
int64 id = 1;
string name = 2;
bytes desc = 3;
}
// table名称为Entity,属性分别为id,name,desc
|
初始化表格¶
1 2 3 4 5 6 7 8 9 10 | // 定义表格的主键,表格的索引
struct entity: public anchor::Entity {
DEFINE_ROWKEY(name);
DEFINE_INDEX_BEGIN(2)
DEFINE_INDEX_ADD(0, id, name)
DEFINE_INDEX_ADD(1, name, desc)
DEFINE_INDEX_END();
};
// 声明表格
xchain::cdt::Table<entity> _entity;
|
put¶
1 2 | template <typename T>
bool Table<T>::put(T t)
|
输入
参数 | 说明 |
---|---|
t | 待插入的数据项 |
输出
参数 | 说明 |
---|---|
true | 插入成功 |
false | 插入失败 |
样例
1 2 3 4 5 6 7 8 9 10 11 12 13 | // 参考样例 contractsdk/cpp/example/anchor.cc
DEFINE_METHOD(Anchor, set) {
xchain::Context* ctx = self.context();
const std::string& id= ctx->arg("id");
const std::string& name = ctx->arg("name");
const std::string& desc = ctx->arg("desc");
Anchor::entity ent;
ent.set_id(std::stoll(id));
ent.set_name(name.c_str());
ent.set_desc(desc);
self.get_entity().put(ent);
ctx->ok("done");
}
|
find¶
1 2 | template <typename T>
bool Table<T>::find(std::initializer_list<PairType> input, T* t)
|
输入
参数 | 说明 |
---|---|
input | 查询关键字 |
t | 返回的数据项 |
输出
参数 | 说明 |
---|---|
true | 查询成功 |
false | 查询失败 |
样例
1 2 3 4 5 6 7 8 9 10 | DEFINE_METHOD(Anchor, get) {
xchain::Context* ctx = self.context();
const std::string& name = ctx->arg("key");
Anchor::entity ent;
if (self.get_entity().find({{"name", name}}, &ent)) {
ctx->ok(ent.to_str());
return;
}
ctx->error("can not find " + name);
}
|
scan¶
1 2 | template <typename T>
std::unique_ptr<TableIterator<T>> Table<T>::scan(std::initializer_list<PairType> input)
|
输入
参数 | 说明 |
---|---|
input | 查询关键字 |
输出
参数 | 说明 |
---|---|
TableIterator | 符合条件的迭代器 |
样例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | DEFINE_METHOD(Anchor, scan) {
xchain::Context* ctx = self.context();
const std::string& name = ctx->arg("name");
const std::string& id = ctx->arg("id");
// const std::string& desc = ctx->arg("desc");
auto it = self.get_entity().scan({{"id", id},{"name", name}});
Anchor::entity ent;
int i = 0;
std::map<std::string, bool> kv;
while(it->next()) {
if (it->get(&ent)) {
/*
std::cout << "id: " << ent.id()<< std::endl;
std::cout << "name: " << ent.name()<< std::endl;
std::cout << "desc: " << ent.desc()<< std::endl;
*/
if (kv.find(ent.name()) != kv.end()) {
ctx->error("find duplicated key");
return;
}
kv[ent.name()] = true;
i += 1;
} else {
std::cout << "get error" << std::endl;
}
}
std::cout << i << std::endl;
if (it->error()) {
std::cout << it->error(true) << std::endl;
}
ctx->ok(std::to_string(i));
}
|
del¶
1 2 | template <typename T>
bool Table<T>::del(T t)
|
输入
参数 | 说明 |
---|---|
t | 一个数据项 |
输出
参数 | 说明 |
---|---|
true | 删除成功 |
false | 删除失败 |
样例
1 2 3 4 5 6 7 8 9 10 11 12 | DEFINE_METHOD(Anchor, del) {
xchain::Context* ctx = self.context();
const std::string& id= ctx->arg("id");
const std::string& name = ctx->arg("name");
const std::string& desc = ctx->arg("desc");
Anchor::entity ent;
ent.set_id(std::stoll(id));
ent.set_name(name.c_str());
ent.set_desc(desc);
self.get_entity().del(ent);
ctx->ok("done");
}
|
Go接口API¶
GetObject¶
func GetObject(key []byte) ([]byte, error)
输入
参数 | 说明 |
---|---|
key | 查询的key值 |
输出
参数 | 说明 |
---|---|
value, nil | key值查询成功,返回value值 |
_, 非nil | key值不存在 |
PutObject¶
func PutObject(key []byte, value []byte) error
输入
参数 | 说明 |
---|---|
key | 存入的key值 |
value | 存入key值对应的value值 |
输出
参数 | 说明 |
---|---|
nil | 存入db成功 |
非nil | 存入db失败 |
DeleteObject¶
func DeleteObject(key []byte) error
输入
参数 | 说明 |
---|---|
key | 将要删除的key值 |
输出
参数 | 说明 |
---|---|
nil | 删除成功 |
非nil | 删除失败 |
QueryTx¶
func QueryTx(txid string) (*pb.Transaction, error)
输入
参数 | 说明 |
---|---|
txid | 待查询的txid |
输出
参数 | 说明 |
---|---|
tx, nil | 查询交易成功, 得到此txid的transaction |
_, 非nil | 查询交易失败 |
QueryBlock¶
func QueryBlock(blockid string) (*pb.Block, error)
输入
参数 | 说明 |
---|---|
blockid | 待查询的blockid |
输出
参数 | 说明 |
---|---|
block, nil | 查询block成功, 得到此blockid的block |
_, 非nil | 查询block失败 |
NewIterator¶
func NewIterator(start, limit []byte) Iterator
输入
参数 | 说明 |
---|---|
start | 关键字 |
limit | 数据项的最大数量 |
输出
参数 | 说明 |
---|---|
Iterator | Interator的接口 |
样例
1 2 3 4 5 6 | Key() []byte
Value() []byte
Next() bool
Error() error
// Iterator 必须在使用完毕后关闭
Close()
|
XuperChain RPC 接口使用说明¶
XuperChain为方便用户深度使用超级链的各项功能,提供了多语言版本的SDK(JS,Golang,C#,Java,Python),这里我们以Golang为例来介绍一下XuperChain的RPC接口使用方式。
RPC接口介绍¶
查看XuperChain的 proto文件 ,可以在service定义中获取所有支持的RPC接口
GetBalance¶
此接口用于查询指定地址中的余额
参数结构 | AddressStatus |
返回结构 | AddressStatus |
这里 AddressStatus 的定义如下
1 2 3 4 5 | message AddressStatus {
Header header = 1;
string address = 2;
repeated TokenDetail bcs = 3;
}
|
其中的 address 字段为需要查询的地址,传入string即可
其中的 bcs 字段为需要查询的链名,因为XuperChain支持平行链的功能,此字段为列表,亦可传入多个链名,
TokenDetail 定义如下:
1 2 3 4 5 | message TokenDetail {
string bcname = 1;
string balance = 2;
XChainErrorEnum error = 3;
}
|
请求时只需传入 bcname 字段,例如 “xuper”,其余字段为返回时携带的,balance即为对应平行链上的余额
其中的 Header 如下
1 2 3 4 5 | message Header {
string logid = 1;
string from_node = 2;
XChainErrorEnum error = 3;
}
|
Header中的logid是回复中也会携带的id,用来对应请求或追溯日志使用的,一般用 core/global/common.go 中的 Glogid() 生成一个全局唯一id
Header中的from_node一般不需要填写,error字段也是返回中携带的错误内容,发请求时不需填写
以下为Golang示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | opts := make([]grpc.DialOption, 0)
opts = append(opts, grpc.WithInsecure())
opts = append(opts, grpc.WithMaxMsgSize(64<<20-1))
conn, _ := grpc.Dial("127.0.0.1:37101", opts...)
cli := pb.NewXchainClient(conn)
bc := &pb.TokenDetail{
Bcname: "xuper",
}
in := &pb.AddressStatus{
Header: global.Glogid(),
Address: "dpzuVdosQrF2kmzumhVeFQZa1aYcdgFpN",
Bcs: []*pb.TokenDetail{bc},
}
out, _ := cli.GetBalance(context.Background(), in)
|
GetBalanceDetail¶
此接口用于查询指定地址中的余额详细情况
参数结构 | AddressBalanceStatus |
返回结构 | AddressBalanceStatus |
AddressBalanceStatus 定义如下
1 2 3 4 5 | message AddressBalanceStatus {
Header header = 1;
string address = 2;
repeated TokenFrozenDetails tfds = 3;
}
|
address字段与GetBalance一样,tfds字段则多了是否冻结的内容,tfds在请求中只需要填充bcname,返回时会有TokenFrozenDetail数组给出正常余额和冻结余额的信息
以下为Golang示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | opts := make([]grpc.DialOption, 0)
opts = append(opts, grpc.WithInsecure())
opts = append(opts, grpc.WithMaxMsgSize(64<<20-1))
conn, _ := grpc.Dial("127.0.0.1:37101", opts...)
cli := pb.NewXchainClient(conn)
tfd := &pb.TokenFrozenDetails{
Bcname: "xuper",
}
in := &pb.AddressBalanceStatus{
Header: global.Glogid(),
Address: "dpzuVdosQrF2kmzumhVeFQZa1aYcdgFpN",
Tfds: []*pb.TokenFrozenDetails{bc},
}
out, _ := cli.GetBalanceDetail(context.Background(), in)
|
GetFrozenBalance¶
此接口用于查询指定地址中的冻结余额,请求方式与GetBalance完全一致,这里不再赘述
GetBlock¶
此接口用于查询指定id的区块内容
参数结构 | BlockID |
返回结构 | Block |
BlockID 定义如下
1 2 3 4 5 6 | message BlockID {
Header header = 4;
string bcname = 1;
bytes blockid = 2;
bool need_content = 3; //是否需要内容
}
|
header和bcname字段如上所述,blocked为要查询的区块id,注意是bytes类型,可能需要hex decode
need_content字段为布尔值,表明是否需要详细的区块内容(还是只查询区块是否在链和前驱后继)
以下为Golang示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | opts := make([]grpc.DialOption, 0)
opts = append(opts, grpc.WithInsecure())
opts = append(opts, grpc.WithMaxMsgSize(64<<20-1))
conn, _ := grpc.Dial("127.0.0.1:37101", opts...)
cli := pb.NewXchainClient(conn)
id, _ := hex.DecodeString("ee0d6fd34df4a7e1540df309d47441af4fda6fdd9d841046f18e7680fe0cea8c")
in := &pb.BlockID{
Header: global.Glogid(),
Bcname: "xuper",
Blockid: id,
NeedContent: true,
}
out, _ := cli.GetBlock(context.Background(), in)
|
GetBlockByHeight¶
此接口用于查询指定高度的区块内容
参数结构 | BlockHeight |
返回结构 | Block |
BlockHeight定义如下
1 2 3 4 5 | message BlockHeight {
Header header = 3;
string bcname = 1;
int64 height = 2;
}
|
同GetBlock类似,id换成整型的高度即可,返回内容也是类似的
GetBlockChainStatus¶
此接口用于查询指定链的当前状态
参数结构 | BCStatus |
返回结构 | BCStatus |
BCStatus定义如下
1 2 3 4 5 6 7 8 | message BCStatus {
Header header = 1;
string bcname = 2;
LedgerMeta meta = 3;
InternalBlock block = 4;
UtxoMeta utxoMeta = 5;
repeated string branchBlockid = 6;
}
|
传入参数只需填充header,bcname即可
以下为Golang示例
1 2 3 4 5 6 7 8 9 10 11 | opts := make([]grpc.DialOption, 0)
opts = append(opts, grpc.WithInsecure())
opts = append(opts, grpc.WithMaxMsgSize(64<<20-1))
conn, _ := grpc.Dial("127.0.0.1:37101", opts...)
cli := pb.NewXchainClient(conn)
in := &pb.BCStatus{
Header: global.Glogid(),
Bcname: "xuper",
}
out, _ := cli.GetBlockChainStatus(context.Background(), in)
|
GetBlockChains¶
此接口用于查询当前节点上有哪些链
参数结构 | CommonIn |
返回结构 | BlockChains |
CommonIn结构很简单,只有header字段,返回的BlockChains也仅有一个链名的string数组
以下为Golang示例
1 2 3 4 5 6 7 8 9 10 | opts := make([]grpc.DialOption, 0)
opts = append(opts, grpc.WithInsecure())
opts = append(opts, grpc.WithMaxMsgSize(64<<20-1))
conn, _ := grpc.Dial("127.0.0.1:37101", opts...)
cli := pb.NewXchainClient(conn)
in := &pb.CommonIn{
Header: global.Glogid(),
}
out, _ := cli.GetBlockChains(context.Background(), in)
|
GetSystemStatus¶
此接口用于查询当前节点的运行状态
参数结构 | CommonIn |
返回结构 | SystemsStatusReply |
此接口相当于先查询了GetBlockChains,在用GetBlockChainStatus查询每个链的状态,不在赘述
QueryACL¶
此接口用于查询指定合约账号的ACL内容
参数结构 | AclStatus |
返回结构 | AclStatus |
AclStatus定义如下
1 2 3 4 5 6 7 8 9 | message AclStatus {
Header header = 1;
string bcname = 2;
string accountName = 3;
string contractName = 4;
string methodName = 5;
bool confirmed = 6;
Acl acl = 7;
}
|
请求中仅需填充header,bcname,accountName即可,其余为返回内容
以下为Golang示例
1 2 3 4 5 6 | in := &pb.AclStatus{
Header: global.Glogid(),
Bcname: "xuper",
AccountName: "XC1111111111111111@xuper",
}
out, _ := cli.QueryACL(context.Background(), in)
|
QueryTx¶
此接口用于查询指定id的交易内容
参数结构 | TxStatus |
返回结构 | TxStatus |
TxStatus定义如下
1 2 3 4 5 6 7 8 | message TxStatus {
Header header = 1;
string bcname = 2;
bytes txid = 3;
TransactionStatus status = 4; //当前状态
int64 distance = 5; //离主干末端的距离(如果在主干上)
Transaction tx = 7;
}
|
请求中仅需填充header,bcname,txid字段
以下为Golang示例
1 2 3 4 5 6 7 | id, _ := hex.DecodeString("763ac8212c80b8789cefd049f1529eafe292f4d64eaffbc2d5fe19c79062a484")
in := &pb.AclStatus{
Header: global.Glogid(),
Bcname: "xuper",
Txid: id,
}
out, _ := cli.QueryTx(context.Background(), in)
|
SelectUTXO¶
此接口用于获取账号可用的utxo列表
参数结构 | UtxoInput |
返回结构 | UtxoOutput |
UtxoInput定义如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | message UtxoInput {
Header header = 1;
// which bcname to select
string bcname = 2;
// address to select
string address = 3;
// publickey of the address
string publickey = 4;
// totalNeed refer the total need utxos to select
string totalNeed = 5;
// userSign of input
bytes userSign = 7;
// need lock
bool needLock = 8;
}
|
请求中只需填充header,bcname,address,totalNeed,needLock,其中needLock表示是否需要锁定utxo(适用于并发执行场景)
UtxoOutput中的返回即可在组装交易时使用,具体组装交易的过程可参考文档下方
1 2 3 4 5 6 7 8 | in := &pb.UtxoInput{
Header: global.Glogid(),
Bcname: "xuper",
Address: "dpzuVdosQrF2kmzumhVeFQZa1aYcdgFpN",
TotalNeed: "50",
NeedLock: true,
}
out, _ := cli.SelectUTXO(context.Background(), in)
|
SelectUTXOBySize¶
此接口用于获取账号中部分utxo,填满交易后便不在继续获取
参数结构 | UtxoInput |
返回结构 | UtxoOutput |
使用过程和SelectUTXO基本相同,仅少了totalNeed字段。适用拥有太多utxo,一次SelectUtxo内容超过交易容纳上限时使用
PreExec¶
此接口用于在节点上进行合约的预执行操作,返回预执行后的请求和回复
参数结构 | InvokeRPCRequest |
返回结构 | InvokeRPCResponse |
InvokeRPCRequest定义如下
1 2 3 4 5 6 7 | message InvokeRPCRequest {
Header header = 1;
string bcname = 2;InvokeRequest
repeated requests = 3;
string initiator = 4;
repeated string auth_require = 5;
}
|
其中的InvokeRequest定义如下
1 2 3 4 5 6 7 8 | message InvokeRequest {
string module_name = 1;
string contract_name = 2;
string method_name = 3;
map<string, bytes> args = 4;
repeated ResourceLimit resource_limits = 5;
string amount = 6;
}
|
其中必填字段有module_name,contract_name,method_name,args,具体示例可参见下一章节
PreExecWithSelectUTXO¶
此接口用于在节点上进行消耗资源的合约预执行操作,内部是由一个PreExec加上一个SelectUTXO实现的,预执行并选择出需要消耗数额的utxo
参数结构 | PreExecWithSelectUTXORequest |
返回结构 | PreExecWithSelectUTXOResponse |
PreExecWithSelectUTXORequest定义如下,实际上就是把预执行的请求结构放在了SelectUTXO结构中
1 2 3 4 5 6 7 8 9 | message PreExecWithSelectUTXORequest {
Header header = 1;
string bcname = 2;
string address = 3;
int64 totalAmount = 4;
SignatureInfo signInfo = 6;
bool needLock = 7;
InvokeRPCRequest request = 5;
}
|
具体填充方式可参考下一章节
PostTx¶
此接口用于提交交易,是大部分操作都需要的最终环节
参数结构 | TxStatus |
返回结构 | CommonReply |
请求结构TxStatus定义在QueryTx中已经给出,但提交交易时需要填充Transaction字段,定义如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | message Transaction {
// txid is the id of this transaction
bytes txid = 1;
// the blockid the transaction belong to
bytes blockid = 2;
// Transaction input list
repeated TxInput tx_inputs = 3;
// Transaction output list
repeated TxOutput tx_outputs = 4;
// Transaction description or system contract
bytes desc = 6;
// Mining rewards
bool coinbase = 7;
// Random number used to avoid replay attacks
string nonce = 8;
// Timestamp to launch the transaction
int64 timestamp = 9;
// tx format version; tx格式版本号
int32 version = 10;
// auto generated tx
bool autogen = 11;
repeated TxInputExt tx_inputs_ext = 23;
repeated TxOutputExt tx_outputs_ext = 24;
repeated InvokeRequest contract_requests = 25;
// 权限系统新增字段
// 交易发起者, 可以是一个Address或者一个Account
string initiator = 26;
// 交易发起需要被收集签名的AddressURL集合信息,包括用于utxo转账和用于合约调用
repeated string auth_require = 27;
// 交易发起者对交易元数据签名,签名的内容包括auth_require字段
repeated SignatureInfo initiator_signs = 28;
// 收集到的签名
repeated SignatureInfo auth_require_signs = 29;
// 节点收到tx的时间戳,不参与签名
int64 received_timestamp = 30;
// 统一签名(支持多重签名/环签名等,与initiator_signs/auth_require_signs不同时使用)
XuperSignature xuper_sign = 31;
// 可修改区块链标记
ModifyBlock modify_block = 32;
}
|
Transaction属于XuperChain中比较核心的结构了,下一章我们将介绍各种场景的交易如何构造并提交
RPC接口应用¶
本章节将以几个简单的场景为例描述RPC接口的使用方法,主要体现逻辑和步骤。代码中仅使用了原始的RPC接口,如果使用SDK则会简便很多。
发起一次转账¶
这里我们演示如何使用RPC接口实现从账号Aclie向账号Bob的一次数额为10的转账,为了进行此操作,我们事先需要有以下信息(均为string)
Alice的地址 | addr_alice |
Alice的公钥 | pub_alice |
Alice的私钥 | pri_alice |
Bob的地址 | addr_bob |
发起转账交易的总体逻辑为,首先通过SelectUTXO获取Alice数额为10的资产,然后构造交易,最后通过PostTx提交
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | // 获取Alice的utxo
utxoreq := &pb.UtxoInput{
Header: global.Glogid(),
Bcname: "xuper",
Address: addr_alice,
TotalNeed: "10",
NeedLock: true,
}
utxorsp, _ := cli.SelectUTXO(context.Background(), utxoreq)
// 声明一个交易,发起者为Alice地址,因为是转账,所以Desc字段什么都不填
// 如果是提案等操作,将客户端的 --desc 参数写进去即可
tx := &pb.Transaction{
Version: 1,
Coinbase: false,
Desc: []byte(""),
Nonce: global.GenNonce(),
Timestamp: time.Now().UnixNano(),
Initiator: addr_alice,
}
// 填充交易的输入,即Select出来的Alice的utxo
for _, utxo := range utxorsp.UtxoList {
txin := &pb.TxInput{
RefTxid: utxo.RefTxid,
RefOffset: utxo.RefOffset,
FromAddr: utxo.ToAddr,
Amount: utxo.Amount,
}
tx.TxInputs = append(tx.TxInputs, txin)
}
// 填充交易的输出,即给Bob的utxo,注意Amount字段的类型
amount, _ := big.NewInt(0).SetString("10", 10)
txout := &pb.TxOutput{
ToAddr: []byte(addr_bob),
Amount: amount.Bytes(),
}
tx.TxOutputs = append(tx.TxOutputs, txout)
// 如果Select出来的Alice的utxo多于10,需要构造一个给Alice的找零
total, _ := big.NewInt(0).SetString(utxorsp.TotalSelected, 10)
if total.Cmp(amount) > 0 {
delta := total.Sub(total, amount)
charge := &pb.TxOutput{
ToAddr: []byte(addr_alice),
Amount: delta.Bytes(),
}
tx.TxOutputs = append(tx.TxOutputs, charge)
}
// 接下来用Alice的私钥对交易进行签名,在此交易中,我们只需Alice签名确认即可
tx.AuthRequire = append(tx.AuthRequire, addr_alice)
// 签名需要的库在 github.com/xuperchain/xuperchain/core/crypto/client
// 和 github.com/xuperchain/xuperchain/core/crypto/hash
cryptoCli, _ := client.CreateCryptoClient("default")
sign, _ := txhash.ProcessSignTx(cryptoCli, tx, []byte(pri_alice))
signInfo := &pb.SignatureInfo{
PublicKey: pub_alice,
Sign: sign,
}
// 将签名填充进交易
tx.InitiatorSigns = append(tx.InitiatorSigns, signInfo)
tx.AuthRequireSigns = append(tx.AuthRequireSigns, signInfo)
// 生成交易ID
tx.Txid, _ = txhash.MakeTransactionID(tx)
// 构造最终要Post的TxStatus
txs := &pb.TxStatus{
Bcname: "xuper",
Status: pb.TransactionStatus_UNCONFIRM,
Tx: tx,
Txid: tx.Txid,
}
// 最后一步,执行PostTx
rsp, err := cli.PostTx(context.Background(), txs)
// 这里的rsp即CommonReply,包含logid等内容
// 交易id我们已经生成在tx.Txid中,不过是bytes,输出可能需要hex.EncodeToString一下
|
新建合约账号¶
这里我们演示创建一个合约账号 XC1234567812345678@xuper ,ACL如下
1 2 3 4 5 6 7 8 9 10 | {
"pm": {
"rule": 1,
"acceptValue": 1.0
},
"aksWeight": {
"XXXaddress-aliceXXX" : 0.6,
"XXXXaddress-bobXXXX" : 0.4
}
}
|
为了进行此操作,我们事先需要有以下信息
Alice的地址 | addr_alice |
Alice的公钥 | pub_alice |
Alice的私钥 | pri_alice |
ACL的内容 | acct_acl |
创建合约账号的总体逻辑为,首先进行创建合约账号的预执行,然后构造相应的交易内容(如果需要支付资源由Alice出),最后提交交易
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | // 构造创建合约账号的请求
args := make(map[string][]byte)
args["account_name"] = []byte(1234567812345678)
args["acl"] = []byte(acct_acl)
invokereq := &pb.InvokeRequest{
ModuleName: "xkernel",
MethodName: "NewAccount",
Args: args,
}
invokereqs := []*pb.InvokeRequest{invokereq}
// 构造合约预执行的请求
authrequire := []string{addr_alice}
rpcreq := &pb.InvokeRPCRequest{
Header: global.Glogid(),
Bcname: "xuper",
Requests: invokereqs,
Initiator: addr_alice,
AuthRequire: authrequire,
}
// 花手续费需要出资的账号确认,填充一个验证的签名,才能正确的拿出utxo来
// 签名需要的库在 github.com/xuperchain/xuperchain/core/crypto/client
// 和 github.com/xuperchain/xuperchain/core/crypto/hash
content := hash.DoubleSha256([]byte("xuper" + addr_alice + "0" + "true"))
cryptoCli, _ := client.CreateCryptoClient("default")
prikey, _ := cryptoCli.GetEcdsaPrivateKeyFromJSON([]byte(pri_alice))
sign, _ := cryptoCli.SignECDSA(prikey, content)
signInfo := &pb.SignatureInfo{
PublicKey: pub_alice,
Sign: sign,
}
// 组合一个PreExecWithSelectUTXORequest用来预执行同时拿出需要支付的Alice的utxo
prereq := &pb.PreExecWithSelectUTXORequest{
Header: global.Glogid(),
Bcname: "xuper",
Address: addr_alice,
TotalAmount: 0,
SignInfo: signInfo,
NeedLock: true,
Request: rpcreq,
}
prersp := cli.PreExecWithSelectUTXO(context.Background(), prereq)
// 构造一个Alice发起的交易
tx := &pb.Transaction{
Version: 1,
Coinbase: false,
Desc: []byte(""),
Nonce: global.GenNonce(),
Timestamp: time.Now().UnixNano(),
Initiator: addr_alice,
}
// 填充支付的手续费,手续费需要“转账”给地址“$”
amount := big.NewInt(prersp.Response.GasUsed)
fee := &pb.TxOutput{
ToAddr: []byte("$"),
Amount: amount.Bytes(),
}
tx.TxOutputs = append(tx.TxOutputs, fee)
// 填充select出来的Alice的utxo
for _, utxo := range prersp.UtxoOutput.UtxoList {
txin := &pb.TxInput{
RefTxid: utxo.RefTxid,
RefOffset: utxo.RefOffset,
FromAddr: utxo.ToAddr,
Amount: utxo.Amount,
}
tx.TxInputs = append(tx.TxInputs, txin)
}
// 处理找零的逻辑
total, _ := big.NewInt(0).SetString(prersp.UtxoOutput.TotalSelected, 10)
if total.Cmp(amount) > 0 {
delta := total.Sub(total, amount)
charge := &pb.TxOutput{
ToAddr: []byte(addr_alice),
Amount: delta,
}
}
// 填充预执行的结果
tx.ContractRequests = prersp.GetResponse().GetRequests()
tx.TxInputsExt = prersp.GetResponse().GetInputs()
tx.TxOutputsExt = prersp.GetResponse().GetOutputs()
// 给交易签名
tx.AuthRequire = append(tx.AuthRequire, addr_alice)
txsign, _ := txhash.ProcessSignTx(cryptoCli, tx, []byte(pri_alice))
txsignInfo := &pb.SignatureInfo{
PublicKey: pub_alice,
Sign: txsign,
}
tx.InitiatorSigns = append(tx.InitiatorSigns, txsignInfo)
tx.AuthRequireSigns = append(tx.AuthRequireSigns, txsignInfo)
// 生成交易ID
tx.Txid, _ = txhash.MakeTransactionID(tx)
// 构造最终要Post的TxStatus
txs := &pb.TxStatus{
Bcname: "xuper",
Status: pb.TransactionStatus_UNCONFIRM,
Tx: tx,
Txid: tx.Txid,
}
// 最后一步,执行PostTx
rsp, err := cli.PostTx(context.Background(), txs)
|
修改合约账号ACL¶
延续上一小节的例子,假设我们要把ACL修改成以下状态
1 2 3 4 5 6 7 8 9 10 | {
"pm": {
"rule": 1,
"acceptValue": 1.0
},
"aksWeight": {
"XXXaddress-aliceXXX" : 1.0,
"XXXXaddress-bobXXXX" : 1.0
}
}
|
为了进行此操作,我们事先需要有以下信息
Alice的地址 | addr_alice |
Alice的公钥 | pub_alice |
Alice的私钥 | pri_alice |
Bob的地址 | addr_bob |
Bob的公钥 | pub_bob |
Bob的私钥 | pri_bob |
新ACL的内容 | new_acl |
修改ACL的总体逻辑为,首先进行修改的预执行,然后构造交易发送,这里需要注意的是,修改ACL操作需要满足现有的ACL要求才有权限,即Alice Bob都需要签名确认。简单起见,当中的手续费依然由Alice支付。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | // 构造修改ACL的请求
args := make(map[string][]byte)
args["account_name"] = []byte(1234567812345678)
args["acl"] = []byte(new_acl)
invokereq := &pb.InvokeRequest{
ModuleName: "xkernel",
MethodName: "SetAccountAcl",
Args: args,
}
invokereqs := []*pb.InvokeRequest{invokereq}
// 构造合约预执行的请求,和上一节一样,此处省略
///////////////////////////////////////////////
// 花手续费需要出资的账号确认,填充验证的签名,和上一节一样,此处省略
/////////////////////////////////////////////////////////////////////
// 按上一节逻辑一样,填充花费、找零,然后填充预执行的结果
tx.ContractRequests = prersp.GetResponse().GetRequests()
tx.TxInputsExt = prersp.GetResponse().GetInputs()
tx.TxOutputsExt = prersp.GetResponse().GetOutputs()
// 给交易签名需要原ACL里的多个账号了
tx.AuthRequire = append(tx.AuthRequire, addr_alice)
tx.AuthRequire = append(tx.AuthRequire, addr_bob)
alicesign, _ := txhash.ProcessSignTx(cryptoCli, tx, []byte(pri_alice))
alicesignInfo := &pb.SignatureInfo{
PublicKey: pub_alice,
Sign: alicesign,
}
bobsign, _ := txhash.ProcessSignTx(cryptoCli, tx, []byte(pri_bob))
bobsignInfo := &pb.SignatureInfo{
PublicKey: pub_bob,
Sign: bobsign,
}
tx.InitiatorSigns = append(tx.InitiatorSigns, alicesignInfo)
tx.AuthRequireSigns = append(tx.AuthRequireSigns, alicesignInfo)
tx.AuthRequireSigns = append(tx.AuthRequireSigns, bobsignInfo)
// 然后和上一节一致了,生成交易ID
tx.Txid, _ = txhash.MakeTransactionID(tx)
// 构造最终要Post的TxStatus
txs := &pb.TxStatus{
Bcname: "xuper",
Status: pb.TransactionStatus_UNCONFIRM,
Tx: tx,
Txid: tx.Txid,
}
// 最后一步,执行PostTx
rsp, err := cli.PostTx(context.Background(), txs)
|
部署一个合约¶
这里我们演示使用合约账号 XC1234567812345678@xuper 部署一个C++的counter合约,init参数为{“creator”:”xchain”},假设合约账号的ACL是修改过的版本
为了进行此操作,我们事先需要有以下信息
合约文件字节内容 | contract_code |
Alice的地址 | addr_alice |
Alice的公钥 | pub_alice |
Alice的私钥 | pri_alice |
部署合约的总体逻辑为,首先构造deploy操作预执行,部署需要的手续费由合约账号出,需要的签名由Alice提供(因为一个签名就满足ACL了)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | // 构造部署合约的请求,关注args的内容,基本上和使用xchain-cli一致
args := make(map[string][]byte)
args["account_name"] = []byte("XC1234567812345678@xuper")
args["contract_name"] = []byte("counter")
// github.com/golang/protobuf/proto
codedesc := desc := &pb.WasmCodeDesc{
Runtime: "c",
}
desc, _ := proto.Marshal(codedesc)
args["contract_desc"] = desc
args["contract_code"] = contract_code
initarg := `{"creator":"` + base64.StdEncoding.EncodeToString([]byte("xchain")) + `"}`
args["init_args"] = []byte(initarg)
invokereq := &pb.InvokeRequest{
ModuleName: "xkernel",
MethodName: "Deploy",
Args: args,
}
invokereqs := []*pb.InvokeRequest{invokereq}
// 这里预执行的authrequire格式为 XC1234567812345678@xuper/dpzuVdosQrF2kmzumhVeFQZa1aYcdgFpN,
// 表示是“某个合约账号的股东”,与直接写账号地址含义是不同的,ACL需求多个签名的时候即多个“股东”
authrequires := []string{"XC1234567812345678@xuper/XXXaddress-aliceXXX"}
rpcreq := &pb.InvokeRPCRequest{
Header: global.Glogid(),
Bcname: "xuper",
Requests: invokereqs,
Initiator: addr_alice,
AuthRequire: authrequires,
}
// SelectUTXO的目标是合约账号中的余额,出资账号签名中的地址变成了合约账号,与“创建账号”小节有区别
content := hash.DoubleSha256([]byte("xuper" + "XC1234567812345678@xuper" + "0" + "true"))
prikey, _ := cryptoCli.GetEcdsaPrivateKeyFromJSON([]byte(pri_alice))
sign, _ := cryptoCli.SignECDSA(prikey, content)
signInfo := &pb.SignatureInfo{
PublicKey: pub_alice,
Sign: sign,
}
// 组合一个PreExecWithSelectUTXORequest用来预执行同时拿出需要支付的合约账号的utxo
prereq := &pb.PreExecWithSelectUTXORequest{
Header: global.Glogid(),
Bcname: "xuper",
Address: "XC1234567812345678@xuper",
TotalAmount: 0,
SignInfo: signInfo,
NeedLock: true,
Request: rpcreq,
}
prersp, _ := cli.PreExecWithSelectUTXO(context.Background(), prereq)
// 构造一个Alice发起的交易
tx := &pb.Transaction{
Version: 1,
Coinbase: false,
Desc: []byte(""),
Nonce: global.GenNonce(),
Timestamp: time.Now().UnixNano(),
Initiator: addr_alice,
}
// 填充支付的手续费,手续费需要“转账”给地址“$”
amount := big.NewInt(prersp.Response.GasUsed)
fee := &pb.TxOutput{
ToAddr: []byte("$"),
Amount: amount.Bytes(),
}
tx.TxOutputs = append(tx.TxOutputs, fee)
// 填充select出来的Alice的utxo
for _, utxo := range prersp.UtxoOutput.UtxoList {
txin := &pb.TxInput{
RefTxid: utxo.RefTxid,
RefOffset: utxo.RefOffset,
FromAddr: utxo.ToAddr,
Amount: utxo.Amount,
}
tx.TxInputs = append(tx.TxInputs, txin)
}
// 处理找零的逻辑
total, _ := big.NewInt(0).SetString(prersp.UtxoOutput.TotalSelected, 10)
if total.Cmp(amount) > 0 {
delta := total.Sub(total, amount)
charge := &pb.TxOutput{
ToAddr: []byte("XC1234567812345678@xuper"),
Amount: delta,
}
}
// 填充预执行的结果
tx.ContractRequests = prersp.GetResponse().GetRequests()
tx.TxInputsExt = prersp.GetResponse().GetInputs()
tx.TxOutputsExt = prersp.GetResponse().GetOutputs()
// 给交易签名,此处也是以“股东”身份签名
tx.AuthRequire = append(tx.AuthRequire, "XC1234567812345678@xuper/XXXaddress-aliceXXX")
txsign, _ := txhash.ProcessSignTx(cryptoCli, tx, []byte(pri_alice))
txsignInfo := &pb.SignatureInfo{
PublicKey: pub_alice,
Sign: txsign,
}
// 虽然Alice和“股东Alice”含义不同,但签名的私钥是一样的
tx.InitiatorSigns = append(tx.InitiatorSigns, signInfo)
tx.AuthRequireSigns = append(tx.AuthRequireSigns, signInfo)
tx.Txid, _ = txhash.MakeTransactionID(tx)
// 构造最终要Post的TxStatus
txs := &pb.TxStatus{
Bcname: "xuper",
Status: pb.TransactionStatus_UNCONFIRM,
Tx: tx,
Txid: tx.Txid,
}
// 最后一步,执行PostTx
rsp, err := cli.PostTx(context.Background(), txs)
|
执行一个wasm合约¶
这里我们演示使用Alice账号调用上一节部署的counter合约,执行 increase 方法,参数为 {“key”: “example”}
为了进行此操作,我们事先需要有以下信息
Alice的地址 | addr_alice |
Alice的公钥 | pub_alice |
Alice的私钥 | pri_alice |
执行合约的总体逻辑为,首先构造相应预执行请求并预执行,如果是查询,那么直接读预执行结果即可,如果是要调用上链的操作,使用预执行结果组建交易并发送
1 2 3 4 5 6 7 8 9 10 11 | // 构造执行合约的请求
args := make(map[string][]byte)
args["key"] = []byte("example")
invokereq := &pb.InvokeRequest{
ModuleName: "wasm",
MethodName: "increase",
ContractName: "counter",
Args: args,
}
invokereqs := []*pb.InvokeRequest{invokereq}
// 其他内容和“创建合约账号”一节完全一致
|
超级链测试环境说明¶
区块链是信任的连接器, 通过区块链可以做到很多之前中心化信息系统做不到的事情,使得参与者可以凭借这个“连接器”完成成可信环境的构建和价值的安全流转。然而,目前多数公链的性能和安全不足以支撑各行各业的诸多场景,百度超级链(XuperChain)是百度自主研发的区块链技术,目前已启动测试环境对外公开测试,欢迎各界开发者使用我们的产品并且提出宝贵意见。
超级链公开测试环境(XuperChain-testnet)¶
超级链公开测试环境是超级链许可开放网络的测试版本,目前超级链测试环境已经实现了超级链的主要功能,为超级链早期用户和开发者提供一个可供使用的测试环境。
用户可以在超级链测试环境上测试部署和使用智能合约等功能,用户可以通过开源代码直接开发及平台化操作两种方式获取测试环境资源以及开发智能合约。
测试环境使用场景¶
超级链测试环境 适用于 :
- 创建测试账号,更为方便的按照教程尝试使用。
- 开发测试智能合约,而无需担心影响真实数据资产。
- 超级链新版本上线前的兼容性升级、功能测试等。
超级链测试环境 不适用于 :
- 压力测试:如果有压力测试需求,请在自行搭建的测试环境上实验,数据会更准确。
- 可用性测试:测试环境并不保证高可用性,可能在某些情况下出现短暂的服务不可用。
- 长期数据存储:测试环境不保证数据长期有效,可能在系统bug、不兼容升级、遭受攻击等情况下,会重置甚至关闭测试环境。我们会尽量保障用户数据不丢失,但在测试环境重置或关闭时,用户可能并不会得到通知,链上数据也可能无法找回。
测试环境资源¶
用户在测试环境部署和调用合约需要使用并且消耗测试资源。
目前测试环境获取测试资源会有以下两种方式:
- 通过超级链公开网络渠道微信群、邮箱等等获取测试资质并领取定量测试资源;
- 批量资源获取可邮件联系:xuper.baidu.com 。
测试资源仅用于测试环境消耗计算,只在测试环境上有效,没有任何经济价值和法律效力,也不支持用于任何形式的交易。如果测试环境因为遭受攻击、不兼容升级等情况下重建时,我们会尽量恢复用户账号中持有的测试资源。
测试环境开发方式¶
如何接入¶
测试环境通过RPC方式提供服务,开发者可以在github获取源代码,按照README说明编译得到cli客户端。
测试环境接入地址:14.215.179.74:37101
开发者只需要在使用xchain-cli时,通过-H参数指定测试环境地址,即可将客户端命令发送到测试环境。 例如查询账号测试资源:
1 | ./xchain-cli account balance dpzuVdosQrF2kmzumhVeFQZa1aYcdgFpN -H 14.215.179.74:37101
|
用户使用条款¶
超级链用户在使用公开测试环境期间,不得访问或使用本网站采取任何可能损害我们或者任何第三方的行为,干扰本网络的运营或者以违反任何法律的方式使用本网络的行为,超级链有权删除相关数据或者追究法律责任,包括但不限于:
- 分享任何违反这些条款或其他适用条款的内容;
- 上传病毒或恶意代码或做任何可能导致我们网络无法正常工作,负担过重或损害的事情;
- 使用自动方式访问或收集我们产品的数据(未经我们事先许可)或尝试访问您无权访问的数据
- 从事任何限制或禁止任何人使用或享用本网站的行为,或根据我们的判断会使我们或我们的任何用户、关联公司或任何其他第三方承担任何责任,损害或损害任何类型的行为。
- 违反系统或网络安全可能导致责任。我们可以随时以任何理由暂停或终止您访问本网站,恕不另行通知
- 使用本网络对外提供服务时,业务须自行前往网信办备案,如由于备案原因造成的法律风险本网站不承担任何责任。
您在使用超级链测试环境前,请确认已经明确了解上述用户须知,当您使用测试环境时表示您已知悉并接收测试环境用户须知。如果测试环境不能满足您的需求,您也可以按照 超级链官方文档 搭建自己的测试环境。如果因为测试环境使用上带来的问题,我们不承担任何法律责任。
超级链测试环境使用指南¶
测试环境说明¶
在使用测试环境之前,请先点开 超级链测试环境说明 ,认真阅读超级链测试环境的目的、使用场景、用户使用条款。
如果您对测试环境说明中的内容有任何疑问,可以通过 xchain.baidu.com 联系我们。如果您使用超级链测试环境,我们认为您已经明确并接受超级链测试环境说明中的相关内容和用户使用条款。
如何接入¶
测试环境通过RPC方式提供服务,开发者可以在 github 获取源代码,按照README说明编译得到cli客户端,当前测试环境使用v3.2分支。
- 测试环境接入地址: 14.215.179.74:37101
- 黄反服务的address: XDxkpQkfLwG6h56e896f3vBHhuN5g6M9u
开发者只需要在使用xchain-cli时,通过-H参数指定测试环境地址,即可将客户端命令发送到测试环境。 例如查询账号测试资源:
1 | ./xchain-cli account balance dpzuVdosQrF2kmzumhVeFQZa1aYcdgFpN -H 14.215.179.74:37101
|
关于测试资源¶
测试环境上部署和执行智能合约需要消耗测试资源,测试环境目前尚未开放外部全节点P2P连接,且测试环境上节点出块并不会获得奖励,但出块节点会获得该块中所有Transaction中支付的测试资源。
目前获取测试资源主要有两种方式:
- 通过超级链公开网络渠道微信群、邮箱等等获取测试资质,可以免费获得测试资源。可以通过 这里 的微信二维码加入超级链用户微信群。
- 批量资源获取可邮件联系:xuper.baidu.com
创建账号¶
创建个人账号(AK)¶
个人账号(AK)其实是一组公私钥对,个人帐号地址(address)是根据公钥经过一定规则导出的一个散列值。个人账号可以离线生成,不需要上链,只有在个人账号产生测试资源变动时(例如转入了一部分测试资源)才会在UTXO中产生记录。
在data/keys下会有一个默认的个人账号(AK),包括address(你的地址)、private.key(你的私钥)、public.key(你的公钥),建议按照如下命令重新生成一个独有的个人账号。
- 指定私钥目录:在data/test_demo下生成address、private.key、public.key: ./xchain-cli account newkeys –output data/test_demo
- 覆盖默认目录: 覆盖data/keys下的文件,需要先删除data/keys目录,然后重新生成新的address、private.key、public.key
1 2 | rm -r data/keys
./xchain-cli account newkeys
|
个人账号地址默认在data/keys/address文件中,可通过cat data/keys/address 查看自己的个人账号地址。
创建合约账号(Account)¶
合约账号可以用来部署智能合约,创建合约账号是一个上链操作,因此也需要消耗一定量的测试资源。合约账号可以设置为多个个人账号共同持有,只有一个交易中的背书签名满足一定合约账号的ACL要求,才能代表这个合约账号进行操作。关于合约账号和ACL权限相关的内容,可以参考
Note
创建合约账号需要向黄反服务拿一个签名,对应地,需要将黄反服务的address写到 data/acl/addrs 中,需要注意的是,multisig最终合入签名时需要将签名顺序与 data/acl/addrs 里面的地址顺序保持一致,否则会签名校验失败。
- Step0: 创建合约账号是一个系统合约,可以通过多重签名的方式发起系统合约调用。系统合约调用需要先创建一个合约调用描述文件,例如下面newAccount.json是一个创建合约账号的描述文件。 newAccount.json文件内容:
1 2 3 4 5 6 7 8 | {
"module_name": "xkernel",
"method_name": "NewAccount",
"args" : {
"account_name": "1234098776890654", # 说明:16位数字组成的字符串
"acl": "{\"pm\": {\"rule\": 1,\"acceptValue\": 1},\"aksWeight\": {\"dpzuVdosQrF2kmzumhVeFQZa1aYcdgFpN\": 1}}" # 这里的address改成自己的address
}
}
|
- Step1: 多重签名需要收集多个账号的签名,在测试环境中主要是需要交易发起者以及黄反服务的签名,因此修改data/acl/addrs文件,将需要收集签名的address写入该文件中。以创建合约账号为例,需要黄反服务背书,因此在该文件中写入黄反服务address。
1 | XDxkpQkfLwG6h56e896f3vBHhuN5g6M9u
|
- Step2: 生成创建合约账号的原始交易,命令如下:
1 | ./xchain-cli multisig gen --desc newAccount.json -H 14.215.179.74:37101 --fee 1000 --output rawTx.out
|
- Step3: 向黄反服务获取签名,命令如下:
1 | ./xchain-cli multisig get --tx ./rawTx.out --host 14.215.179.74:37101 --output complianceCheck.out
|
- Step4: 自己对原始交易签名,命令如下:
1 | ./xchain-cli multisig sign --tx ./rawTx.out --output my.sign
|
- Step5: 将原始交易以及签名发送出去,命令如下:
1 | ./xchain-cli multisig send my.sign complianceCheck.out --tx ./rawTx.out -H 14.215.179.74:37101
|
Note
Step5
中放签名的地方:第一个my.sign签名对应的是交易发起者(Initiator),第二个complianceCheck.out签名对应的是需要背书(AuthRequire)的地址,发起者签名和背书签名用空格分开,如果需要多个账号背书,那么多个背书签名用,隔开,且签名顺序需要与data/acl/addrs中的地址顺序一致。
创建成功后,你可以通过这个命令去查看你刚才创建的合约账号:
1 | ./xchain-cli account query --host 14.215.179.74:37101
|
设置合约账号ACL¶
Note
前置条件:将合约账号以及合约账号下的有权限的AK以合约账号/address形式以追加方式存放到data/acl/addrs
- Step1: 生成设置合约账号的原始交易,命令如下:
1 | ./xchain-cli multisig gen --desc accountAclSet.json -H 14.215.179.74:37101 --fee 10 --output rawTx.out
|
- Step2: 向黄反服务获取签名,命令如下:
1 | ./xchain-cli multisig get --tx ./rawTx.out --host 14.215.179.74:37101 --output complianceCheck.out
|
- Step3: 自己对原始交易签名,命令如下:
1 | ./xchain-cli multisig sign --tx ./rawTx.out --output my.sign
|
- Step4: 将原始交易以及签名发送出去,命令如下:
1 | ./xchain-cli multisig send my.sign complianceCheck.out,my.sign --tx ./rawTx.out -H 14.215.179.74:37101
|
accountAclSet.json模版如下:
1 2 3 4 5 6 7 8 | {
"module_name": "xkernel",
"method_name": "SetAccountAcl",
"args" : {
"account_name": "XC1234098776890654@xuper",
"acl": "{\"pm\": {\"rule\": 1,\"acceptValue\": 1},\"aksWeight\": {\"ak1\": 1}}"
}
}
|
合约操作¶
Note
合约操作包括编译、部署、调用、设置合约接口权限,目前XuperChain支持的合约语言包括C++,Go,我们以C++中的counter.cc为例,以此说明合约相关操作。
合约编译¶
Note
合约编译是指将合约编译成二进制形式
例子:C++版本的counter.cc,counter.cc存放路径为contractsdk/cpp/example 预置条件:安装docker
1 2 | cd contractsdk/cpp
sh build.sh
|
到当前目录build里,将编译好的合约二进制counter.wasm,重新命名为counter,放到某个目录下,比如笔者的目录是./output/
合约账号充入测试资源¶
合约部署需要合约账号才能操作,因此会消耗合约账号的测试资源,需要开发者先将个人账号的测试资源转一部分给合约账号。(注意,目前不支持合约账号的测试资源再转出给个人账号,因此请按需充入测试资源。)
- Step1: 生成测试资源转给合约账号的原始交易数据,命令如下:
1 | ./xchain-cli multisig gen --to XC1234098776890651@xuper --amount 150000 --output rawTx.out --host 14.215.179.74:37101
|
其中: –amount是转出的测试资源数量,–to是接收测试资源的账号名。如果转出方不是./data/keys下的默认地址,则可以使用–from指定转账来源账号,并将该来源地址的签名在multisig send时写在Initiator的位置。
- Step2: 向黄反服务获取签名,命令如下:
1 | ./xchain-cli multisig get --tx ./rawTx.out --output complianceCheck.out --host 14.215.179.74:37101
|
- Step3: 自己对原始交易签名,命令如下:
1 | ./xchain-cli multisig sign --tx ./rawTx.out --output my.sign
|
- Step4: 将原始交易以及签名发送出去,命令如下:
1 | ./xchain-cli multisig send my.sign complianceCheck.out --tx ./rawTx.out -H 14.215.179.74:37101
|
- Step5: 查询合约账号的测试资源数额,确定转账成功:
1 | ./xchain-cli account balance XC1234098776890651@xuper -H 14.215.179.74:37101
|
合约部署¶
Note
部署合约的前提条件是先创建一个合约账号,假设按照上述步骤已经创建了一个合约账号 XC1234098776890651@xuper
,并且对应的合约账号有充裕的测试资源 前置条件:将合约账号以及合约账号下的有权限的AK以 合约账号/address 形式以追加方式存放到 data/acl/addrs
- Step0: 合约部署需要在交易中写入满足合约账号ACL的背书AK签名,为了表示某个AK在代表某个账号背书,超级链中定义了一种AK URI,例如 dpzuVdosQrF2kmzumhVeFQZa1aYcdgFpN 代表 XC1234098776890651@xuper 这个合约账号,那么这个背书AK的AK URI可以写成:XC1234098776890651@xuper/dpzuVdosQrF2kmzumhVeFQZa1aYcdgFpN 。
以此为例,背书AK URI需要同时包含黄反服务和合约账号,因此需要将 data/acl/addrs 文件改成:
1 2 | XDxkpQkfLwG6h56e896f3vBHhuN5g6M9u
XC1234098776890651@xuper/dpzuVdosQrF2kmzumhVeFQZa1aYcdgFpN
|
Step1: 生成部署合约的原始交易,命令如下:
1 | ./xchain-cli wasm deploy --account XC1234098776890651@xuper --cname counter -H 14.215.179.74:37101 -m ./counter --arg '{"creator":"xchain"}' --output contractRawTx.out --fee 137493
|
Step2: 向黄反服务获取签名,命令如下:
1 | ./xchain-cli multisig get --tx ./contractRawTx.out --host 14.215.179.74:37101 --output complianceCheck.out
|
Step3: 自己对原始交易签名,命令如下:
1 | ./xchain-cli multisig sign --tx ./contractRawTx.out --output my.sign
|
Step4: 将原始交易以及签名发送出去,命令如下:
1 | ./xchain-cli multisig send my.sign complianceCheck.out,my.sign --tx ./contractRawTx.out -H 14.215.179.74:37101
|
合约调用¶
编译合约,部署合约的目的都是为了能够在区块链系统上运行智能合约,本小节说明如下调用合约。
- Step1: 生成合约调用的原始交易,命令有下面两种实现方式:
1 2 3 | ./xchain-cli multisig gen --desc counterIncrease.json -H 14.215.179.74:37101 --fee 85 --output rawTx.out
# 或者这样
./xchain-cli wasm invoke -a '{"key":"counter"}' --method increase counter -H 14.215.179.74:37101 --fee 85 -m --output rawTx.out
|
- Step2: 向黄反服务获取签名,命令如下:
1 | ./xchain-cli multisig get --tx ./rawTx.out --host 14.215.179.74:37101 --output complianceCheck.out
|
- Step3: 自己对原始交易签名,命令如下:
1 | ./xchain-cli multisig sign --tx ./rawTx.out --output my.sign
|
- Step4: 将原始交易以及签名发送出去,命令如下:
1 | ./xchain-cli multisig send my.sign complianceCheck.out --tx ./rawTx.out -H 14.215.179.74:37101
|
counterIncrese.json模板,如下:
1 2 3 4 5 6 7 8 | {
"module_name": "wasm",
"contract_name": "counter",
"method_name": "increase",
"args":{
"key":"counter"
}
}
|
设置合约接口ACL¶
Note
- Step1: 生成设置合约方法权限(ACL)的原始交易,命令如下:
1 | ./xchain-cli multisig gen --desc methodAclSet.json -H 14.215.179.74:37101 --fee 10 --output rawTx.out
|
- Step2: 向黄反服务获取签名,命令如下:
1 | ./xchain-cli multisig get --tx ./rawTx.out --host 14.215.179.74:37101 --output complianceCheck.out
|
- Step3: 自己对原始交易签名,命令如下:
1 | ./xchain-cli multisig sign --tx ./rawTx.out --output my.sign
|
- Step4: 将原始交易以及签名发送出去,命令如下:
1 | ./xchain-cli multisig send my.sign complianceCheck.out,my.sign --tx ./rawTx.out -H 14.215.179.74:37101
|
methodAclSet.json的模版,如下:
1 2 3 4 5 6 7 8 9 | {
"module_name": "xkernel",
"method_name": "SetMethodAcl",
"args" : {
"contract_name": "counter",
"method_name": "increase",
"acl": "{\"pm\": {\"rule\": 1,\"acceptValue\": 1},\"aksWeight\": {\"TqnHT6QQnD9rjvqRJehEaAUB3ZwzSFZhR\": 1}}"
}
}
|
FAQ¶
Q: | 为什么测试环境现在不开放全节点P2P账本同步? |
---|---|
A: | 目前超级链仍然处于高速迭代期,为了保证bug能够得到即时修复更新,我们暂时未开放外部P2P节点加入测试环境的功能,但用户通过GRPC接口已经能体验到测试环境的大部分功能。 当然,我们会在测试环境运行一段时间后,开放P2P节点加入乃至开放外部节点成为超级节点,具体时间目前还没有确定,请大家继续关注。 |
Q: | 测试环境中的测试资源可以转给别的个人账号吗? |
A: | 不能,测试资源仅供在测试环境上进行超级链体验、智能合约开发测试使用,用户可以通过加入测试计划免费获得,用户获得的测试资源无法转给其他任何个人账号。 |
Q: | 测试环境为什么所有交易都需要黄反服务签名? |
A: | 超级链测试环境上的数据是所有用户透明可见的,为了保证所有用户的体验,我们会对每个transaction中的数据进行合规性检测,尽量避免涉嫌黄反内容上链。用户违规发起涉嫌黄反内容的transaction引起的任何后果,都需要自行承担。请各位测试用户也从自身做起,保障一个干净和谐的网络环境。 |
Q: | 编译cpp合约出现 “Post http:///var/run/docker.sock/v1.19/containers/create: dial unix /var/run/docker.sock: permission denied. Are you trying to connect to a TLS-enabled daemon without TLS?” 是什么原因? |
A: | 这可能是因为用户安装docker后,没有创建docker用户组,或者当前运行的系统账号不在docker用户组中,可以尝试下面的命令: sudo groupadd docker
sudo usermod -aG docker ${USER} // 此处${USER}为你编译合约时使用的linux账号
service docker resteart
|
操作指导¶
如何升级软件¶
当版本升级时,需要更新为新版本的代码,并重新编译,然后将 plugins文件夹, 二进制文件xchain,xchain-cli 全部替换后全部重新启动即可,注意多节点模式下需要先启动bootNodes节点。
配置文件说明¶
XuperChain的配置文件默认读取有3个优先级:
- 默认配置:系统中所有配置项都有默认的配置信息,这个是优先级最低的配置;
- 配置文件:通过读取配置文件的方式,可以覆盖系统中默认的参数配置,默认的配置文件为 ./conf/xchain.yaml;
- 启动参数:有一些参数支持启动参数的方式设置,该设置方式的优先级最高,会覆盖配置文件中的配置项;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | log:
filepath: logs // 日志输出目录
filename: xchain // 日志文件名
console: true //是否答应console日志
level : trace // 日志等级,debug < trace < info < warn < error < crit
tcpServer:
port: :57404 // 节点RPC服务监听端口
p2pv2:
port: 47404 // 节点p2p网络监听的端口
bootNodes: /ip4/127.0.0.1/tcp/47401/p2p/QmXRyKS1BFmneUEuwxmEmHyeCSb7r7gSNZ28gmDXbTYEXK // 节点加入网络链接的种子节点的netUrl
miner:
keypath: ./data/keys //节点address目录
datapath: ./data/blockchain //账本存储目录
utxo:
cachesize: 5000 //Utxo内存cache大小设置
tmplockSeconds: 60 //GenerateTx的临时锁定期限,默认是60秒
|
core目录各文件说明¶
模块 | 功能及子文件说明 |
---|---|
acl | acl查询 account_acl.go 查询合约账号ACL的接口定义 acl_manager.go 查询合约账号ACL,合约方法ACL的具体实现 contract_acl.go 查询合约方法ACL的接口定义 |
cmd | XuperChain命令行功能集合,比如多重签名、交易查询、区块查询、合约部署、合约调用、余额查询等 |
common | 公共组件 batch_chan.go 将交易批量写入到channel中 common.go 获取序列化后的交易/区块的大小 lru_cache.go lru cache实现 util.go 去重string切片中的元素 |
config | 系统配置文件 config.go 包括日志配置、Tcp配置、P2p配置、矿工配置、Utxo配置、Fee配置、合约配置、控制台配置、节点配置、raft配置等 |
consensus | 共识模块 base 共识算法接口定义 consensus.go 可插拔共识实现 tdpos dpos共识算法的具体实现 single single共识算法的具体实现 |
contract | 智能合约 contract.go 智能合约接口定义 contract_mgr.go 创建智能合约实例 kernel 系统级串行智能合约 proposal 提案 wasm wasm虚拟机 |
core | xchaincore.go 区块链的业务逻辑实现 xchainmg.go 负责管理多条区块链 xchainmg_validate.go 对区块、交易、智能合约的合法性验证业务逻辑 sync.go 节点主动向其它节点同步区块业务逻辑 xchaincore_net.go 通过广播形式向周围节点要区块 xchainmg_net.go 注册接收的消息类型 xchainmg_util.go 权限验证 |
crypto | 密码学模块 account 生成用户账号 client 密码学模块的客户端接口 config 定义创建账号时产生的助记词中的标记符的值,及其所对应的椭圆曲线密码学算法的类 hash hash算法 sign 签名相关 utils 常用功能 |
global | 全局方法/变量 common.go 全局方法 global.go 全局变量 |
kv | 存储接口与实现 kvdb 单盘存储 mstorage 多盘存储 |
ledger | 账本模块 genesis.go 创世区块相关实现 ledger.go 账本核心业务逻辑实现 ledger_hash.go 账本涉及的hash实现,如生成Merkle树,生成区块ID |
log | 日志模块 log.go 创建日志实例 |
p2pv2 | p2p网络模块 pb p2p网络消息的pb定义 config.go p2p网络配置 filter.go p2p网络节点过滤实现 server.go p2p网络对外接口实现 stream.go p2p网络流的定义与实现 subscriber.go p2p网络消息订阅定义与实现 util.go p2p网络的全局方法 handlerMap.go p2p网络消息处理入口 node.go p2p网络节点定义与实现 stream_pool.go p2p网络节点对应的流定义与实现 type.go p2p网络对外接口定义 |
permission | 权限验证模块 permission.go 权限验证的业务逻辑实现 ptree 权限树 rule 权限模型 utils 通用工具 |
pluginmgr | 插件管理模块 pluginmgr.go 插件管理的业务逻辑实现 xchainpm.go 插件初始化工作 |
replica | 多副本模块 replica.go 多副本raft业务逻辑实现 |
server | util.go 通用工具实现,如获取远程节点ip |
xuper3 | contract contract/bridge xuperbridge定义与实现 contract/kernel 系统级合约(走预执行) contract/vm.go 虚拟机接口定义 |
xuper3 | xmodel xmodel实现 xmodel/pb 版本数据pb定义 xmodel/dbutils.go xmodel通用方法 xmodel/env.go 预执行环境初始化 xmodel/xmodel_cache.go model cache实现 xmodel/xmodel_iterator.go model迭代器实现 xmodel/xmodel_verify.go 读写集验证 xmodel/interface.go xmodel接口定 xmodel/versioned_data.go 版本数据 xmodel/xmodel_cache_iterator.go model cache迭代器 xmodel/xmodel.go model业务逻辑实现 |
vendor | 依赖的三方库 |
utxo | utxo模块 acl_valid_verify.go acl验证业务逻辑实现,包括SetAccountAcl, SetMethodAcl, 合约调用时的权限验证 topsort.go 交易集合的拓扑排序实现 txhash 交易相关的hash async.go 异步处理 tx_contract_generator.go 合约交易操作 utxo_cache.go utxo cache实现 utxo_item.go utxo表定义 withdraw.go 赎回实现 tx_contract_verifier.go 合约交易操作 |
指令介绍(API)¶
节点rpc接口¶
详细见:pb/xchain.proto
API | 功能 |
---|---|
rpc createAccount(AccountInput) returns (AccountOutput) | 创建公私钥对 |
rpc GenerateTx(TxData) returns (TxStatus) | 生成交易 |
rpc PostTx(TxStatus) returns (CommonReply) | 对一个交易进行验证并转发给附近网络节点 |
rpc BatchPostTx(BatchTxs) returns (CommonReply) | 对一批交易进行验证并转发给附近网络节点 |
rpc QueryAcl(AclStatus) returns (AclStatus) | 查询合约账号/合约方法的Acl |
rpc QueryTx(TxStatus) returns (TxStatus) | 查询一个交易 |
rpc GetBalance(AddressStatus) returns (AddressStatus) | 查询可用余额 |
rpc GetFrozenBalance(AddressStatus) returns (AddressStatus) | 查询被冻结的余额 |
rpc SendBlock(Block) returns (CommonReply) | 将当前区块为止的所有区块上账本 |
rpc GetBlock(BlockID) returns (Block) | 从当前账本获取特定区块 |
rpc GetBlockChainStatus(BCStatus) returns (BCStatus) | 获取账本的最新区块数据 |
rpc ConfirmBlockChainStatus(BCStatus) returns (BCTipStatus) | 判断某个区块是否为账本主干最新区块 |
rpc GetBlockChains(CommonIn) returns (BlockChains) | 获取所有的链名 |
rpc GetSystemStatus(CommonIn) returns (SystemsStatusReply) | 获取系统状态 |
rpc GetNetUrl(CommonIn) returns (RawUrl) | 获取区块链网络中某个节点的url |
rpc GenerateAccountByMnemonic(GenerateAccountByMnemonicInput) returns (AccountMnemonicInfo) | 创建一个带助记词的账号 |
rpc CreateNewAccountWithMnemonic(CreateNewAccountWithMnemonicInput) returns (AccountMnemonicInfo) | 通过助记词恢复账号 |
rpc MergeUTXO (TxData) returns (CommonReply) | 将同一个地址的多个余额项合并 |
rpc SelectUTXOV2 (UtxoInput) returns(UtxoOutput) | 查询一个地址/合约账号对应的余额是否足够 |
rpc QueryContract(QueryContractRequest) returns (QueryContractResponse) | 查询合约数据 |
开发者接口¶
详细见:contractsdk/pb/contract.proto
API | 功能 |
---|---|
rpc PutObject(PutRequest) returns (PutResponse) | 产生一个读加一个写 |
rpc GetObject(GetRequest) returns (GetResponse) | 生成一个读请求 |
rpc DeleteObject(DeleteRequest) returns (DeleteResponse) | 产生一个读加一个特殊的写 |
rpc NewIterator(IteratorRequest) returns (IteratorResponse) | 对迭代的key产生读 |
rpc QueryTx(QueryTxRequest) returns (QueryTxResponse) | 查询交易 |
rpc QueryBlock(QueryBlockRequest) returns (QueryBlockResponse) | 查询区块 |
rpc ContractCall(ContractCallRequest) returns (ContractCallResponse) | 合约调用 |
rpc Ping(PingRequest) returns (PingResponse) | 探测是否存活 |
常见问题解答¶
系统相关¶
Q: | 超级链按照一般的分法,是属于联盟链还是公链?仅从源码看,XuperChain是否更偏向于采用公链的理念设计,先记账后共识,只有一条主链? |
---|---|
A: | XuperChain在设计中以模块化和可插拔作为基本原则之一,对共识、合约等核心组件都可高度定制开发。因此从这点来讲,XuperChain并不是只为公链或联盟链设计,而是一种通用可定制的区块链架构。在多链方面,未来我们也会开源我们的跨链技术,请持续关注。 |
Q: | 超级节点技术中说到,“超级链实现了计算和存储分离的这样一个架构,即一个节点,它表面上是节点网络中的一个节点,它的背后则是一个强大的、分布式的计算和存储集群。”,这段话怎么理解? |
A: | 超级结点技术现在还不太完善,目前仅支持存储放到nfs上。我们的技术理想是,采用分布式存储+分布式计算调度技术,将超级结点的处理请求分发到这两个内部集群里面去。链内并行技术理论上是能分析出DAG并行处理依赖,然后将无依赖的请求调度计算集群里面分开计算。这样,以后超级结点其实是一个集群技术。 |
Q: | 超级链强调自己是100%自主研发,为什么要重新开发全新的区块链系统?新系统的可靠性和健壮性如何验证? |
A: | 技术是无国界的,但是工程师是有国籍的。几乎每个流行的区块链项目都有一个稳定的核心开发团队圈子,为了不受制于人,尤其对于新兴技术,唯有自主研发才是正途。 百度超级链的愿景是让信任的建立更加便捷,未来区块链会广泛应用在各行各业的大量业务,成为信任的连接器,当前我们已经在版权保护、司法存证、数据流通等领域落地了应用,可靠性健壮性得到了验证。此次开源也是为了在更广泛的业务场景中应用,我们也会不断迭代代码、打磨系统,完善文档。 |
Q: | XuperChain能跨链吗?XuperChain上的各个平行链之间数据能相互访问交互吗?如果现有的eth、eos或者fabric要和XuperChain对接该如何对接呢? |
A: | 超级链支持跨链,但此次开源的XuperChain里面没有包含跨链的组件,XuperChain 中的平行链不具备互操作性。跨链的功能后续将会开源在XuperCross这个项目里,目前已经实现对超级链平行链之间可以跨链,也可以实现对fabric的跨链,目前跨链技术仍在完善中。 |
Q: | 百度超级链在实现上和Fabric相比,具有哪些异同呢? |
A: | 与Fabric相比,唯一的相似点是都有合约预执行阶段,其他很多地方都不一样。首先, Fabric的Ledger是不支持分叉的,XuperChain是支持的。Fabric中数据的版本编码是和区块高度绑定的,因此它不支持在同一个区块内多笔事务修改同一个Key。在智能合约开发方面,Fabric没有合约虚拟机啊,不支持合约资源消耗控制,而且是通过Docker来实现粗粒度的资源限制。在网络方面,Fabric是用的RPC,并不能构建大规模的P2P网络。 |
Q: | 超级节点的服务器存储量是有限的,如果达到上限怎么办?是采取什么样的结构存储数据? |
A: | 目前提供多盘存储能力,如果超过单机容量可以挂载nfs,底层是扁平化的key-value存储,而且是可插拔的,目前支持leveldb和badger两种key-value存储引擎。如果有需要也可以开发分布式key-value 的组件。 |
Q: | 超级链在部署方面的亮点和技术优势是什么? |
A: | 部署亮点:不同于传统的联盟链系统,超级链具备全球化部署能力,节点通信基于加密的P2P网络,支持广域网超大规模节点,且底层账本支持分叉管理,自动收敛一致性,先进的TDPOS算法确保了大规模节点下的快速共识。 主要技术优势是: 高性能:通过原创的XuperModel模型,真正实现了智能合约的并行执行和验证,通过自研的WASM虚拟机,做到了指令集级别的极致优化。 架构灵活: 其可插拔、插件化的设计使得用户可以方便选择适合自己业务场景的解决方案,通过独有的XuperBridge技术,可插拔多语言虚拟机,从而支持丰富的合约开发语言。 安全: 内置了多私钥保护的账户体系,支持权重累计、集合运算等灵活的鉴权策略。保障了智能合约运行的安全和可控。 |
共识相关¶
Q: | 可插拔共识具体指什么?为什么设计成支持热更新的共识机制? |
---|---|
A: | 当前,每种共识都有各自的优缺点。POW太消耗能源,而DPOS经常被质疑是不够去中心化的。所以,在XuperChain的代码中,我们实现了一种可插拔的共识机制,开发者自己实现相关接口,编译成插件就可以完成替换。并且我们认为在系统运行的不同阶段会对共识机制有不同的要求,例如某些场景下冷启动时可能使用PoW更合适,而当系统进入一个稳定状态时,可能使用TDPoS更合适,因此设计了一种共识热更新机制,通过提案投票,可以在区块链网络不停服的情况下完成共识更新。 |
Q: | 超级链目前支持哪些共识算法?推荐使用哪个共识算法? |
A: | 超级链是可插拔共识,具有统一的共识接口,目前已经开源了包括TDPoS、PoW、Chained-BFT、授权证明等多种共识机制。我们建议的是TDPoS共识,开发者也可以选择自己实现共识接口,扩展出更多的共识机制。 |
Q: | 此次开源的出来的共识中, 最终确认时间是多少? |
A: | 目前开源的共识算法有TDPoS、PoW、Single、Chained-BFT。这几种共识算法的出块时间都是可配置的。一般来说Single是强一致模型,可以认为是即可生效;PoW的交易确认时间根据网络情况而定,一般是6个块以上;如果采用TDPoS+Chained-BFT,则交易确认时间是3个块。 |
Q: | 现在超级链支持一条链共识算法无缝由pow切换为tdpos吗? |
A: | 支持,共识的热更新是基于提案投票机制,可以通过提交一个proposal,并在提案中设定好触发高度,当提案收到足够比例的投票后,系统会在之前设定的高度时统一触发共识升级,调用consensus的updateConsensusMethod方法,来实现共识切换。 |
Q: | 在TDPOS机制是怎么优化DPOS机制的分叉问题的?如果此时有一两个块由于网络延时其他节点未收到该广播,那是再发广播,等到其他节点确认收到该广播以后再切换BP节点? |
A: | 主要是有以下2点:
1. 首先在时间调度算法切片上,我们有3个时间间隔配置,分别为出块间隔、轮内BP节点切换的时间间隔和切换轮的时间间隔,这个其实很简单,这样在切换BP节点时会可以让区块有足够的时间广播给下一个BP;
2. 在网络拓扑上进行的优化,我们超级节点的选举是在每轮的第一个区块,并且提前一轮确定,这时我们网络层有足够的时间在BP节点之间建立直接的链接,这样也可以降低我们切换BP节点的分叉率。
|
Q: | 如果想扩展自定义共识算法,有相关文档吗? |
A: | 目前没有具体的文档,如果感兴趣的话可以参考一下开源共识代码的实现,主要接口参考文件consensus/base/consensusinterface.go。 |
性能相关¶
Q: | 请问百度超级链8.7万的TPS是在什么样的硬件环境下测得的? |
---|---|
A: | 目前百度超级链通过了国家工业信息安全发展研究中心评测鉴定所的测评,在性能测试中,百度超级链并发可达每秒87000笔交易;测试环境采用TDPOS共识,5节点异步模式, 万兆网络,每个节点64核 2.4GHZ CPU,NVME SSD存储。 |
Q: | 链内并行在开源版本实现了吗? |
A: | 实现了,在func (xc *XChainCore) PostTx中。 节点收到一个tx后,除了校验密码学签名, 还会校验其依赖的Input哈希指针(指向依赖的其他tx output)是否有效(up to date),如果有效则加载数据构造合约执行环境,然后重新执行合约,看输出的数据是否和tx中声明的写集合“TxOutputExts“一致。这整个过程都是无锁的,因此能并行,利用多核能力。 验证通过后,最后调用doTxInternal写入账本, 这个过程是有锁的,会再次check 哈希指针的有效性,因为前面放锁了。 简单而言,是一种乐观锁实现。 |
Q: | XuperChain引入链内并发及多版本事务处理技术, 那么它又是如何保证事务的原子性及时序性? 是否与分布式数据库的事务相似呢? |
A: | 通过将一个事务涉及的数据变更打包在一个底层KV数据库的Batch写,保证其原子性。 事务的处理时序是通过事务的引用关系来定序:DAG的拓扑序。 在经典的UTXO模型中,事务声明了“资金引用”,而XuperChain的事务模型中,有两种引用:资金引用和数据引用,通过数据引用来支持通用的智能合约。 链内并行原理:节点收到一个事务后,除了校验密码学签名,还会校验其依赖的Input哈希指针(指向依赖的其他事务的output)是否有效(up to date),如果有效则加载数据构造合约执行环境,然后重新执行合约,验证输出的数据是否和其声明的Output一致。这整个过程都是无锁的,因此能并行,利用多核能力。验证通过后,事务的Output写入账本, 这个过程是有锁的,写入前会再次检查一次哈希指针的有效性。整体上的原理和分布式数据库的MVCC并发控制有相似之处。 |
Q: | 链内并行技术中,如果多个并发交易具有时序性, 是否会产生死锁问题?为什么? |
A: | 不会有死锁。从前面对链内并行的原理分析也可以看到,我们是采用的“乐观锁”的机制,有点类似CPU的硬件同步原语Compare and swap。 事务之间最差情况是冲突导致单次提交失败,不会死锁。 超级链中事务的提交分为两阶段,预执行(PreExec)和提交(PostTx)。预执行阶段合约对账本是只读(Read-Only)的,预执行结果会返回事务的“读写集合”,其中读集合描述了事务依赖数据各个Key的Hash指针,这个Hash指针指向已经成功提交的事务的Output域。客户端将读写集和自己的签名组装起来,开始第二阶段:提交,节点验证成功后,事务输出才写入生效,进入待上链状态。如果提交失败,客户端可以返回第一阶段重新开始。 |
Q: | 请问DAG技术有较为详细的文档么?或者在代码中哪个模块能看到相关实现? |
A: | DAG并发执行目前已经开源了块验证时的DAG识别和并发执行,具体可以参见开源代码中的UTXO模块。对于通过PostTx接收到的交易,目前还没有开源DAG并行架构,会在未来的版本中 |
合约相关¶
Q: | 超级链合约虚拟机开源了吗,兼容性怎么样? |
---|---|
A: | 已经开源了,目前主要支持XVM和WAVM两种虚拟机。XVM开源在XuperChain主框架项目中,可以看下XVM的实现,基本过程是wasm -> code injection -> c -> dylib. 然后go里面调用特定的几个导出函数符号执行。wasm import的符号会在c里面体现为外部导入符号,然后通过cgo,在go里面暴露出来。这个方式算是一种比较巧妙而且简洁的方式吧。 另外wavm开源在XuperChain/Wavm这个项目中。 |
Q: | XuperBridge是合约虚拟机和区块链账本的桥梁, 它的好处当然有很多, 可以统一接口, 更低的耦合度, 但同时也会限制一些灵活性, 关于这点是如何处理的呢? |
A: | XuperBridge通过统一接口降低了将不同类型的虚拟机接入到XuperChain的难度,给予开发者更多的选择来开发Dapp,而不限于某一类特定的编程语言。目前我们已经接入了WASM和Docker来满足不同场景的业务需求,后续我们会开放更多的接口来满足开发者的多样的开发需求。事实上,我们通过XuperBridge也已经支持了以太坊的solidity虚拟机,只是由于License问题,此次不便开源。 |
Q: | 合约预执行,与Fabric的endorser阶段策略类似吗?先生成read/write set? |
A: | 流程上大体类似,实现上是有差别的,例如数据版本定义等。Fabric中数据的版本编码是和区块高度绑定的,因此它不支持在同一个区块内多笔事务修改同一个Key,超级链中的版本类似于UTXO,同一个块中可以对一个key进行多次修改,因此可以大幅提升交易性能。 |
Q: | 合约之间是否可访问,例如A账户a1合约里面存储的数据在B账户b1合约里面可以访问么? 那用户B能调用A账户部署的合约吗? |
A: | 目前夸合约调用还不支持,出于用户权限和数据安全考虑,合约数据属于受保护的私有数据,不能被其他合约直接使用,即使两个合约都属于同一账户。未来可以通过系统级跨合约调用的方式实现数据共享,目前这部分技术还没有开源,请持续关注超级链后续版本。 但用户可以调用其他账户部署的合约,每个合约接口可以设置单独的ACL权限控制,因此合约所有者可以在合约接口ACL中配置哪些账户有权限访问该接口,这里的账户并不限于合约所有者。1. ACL权限模型可以自由扩充定制。 2. 如果合约接口创建后没设置ACL,默认是public,所有用户都可以访问。 |
Q: | 是否支持原生合约?原生合约有没有资源消耗机制? |
A: | 超级链支持原生(Native)合约,原生合约可以在docker环境中执行,但原生合约目前不支持资源消耗控制机制。 |
Q: | 智能合约是什么时候触发执行的?只能由客户端触发吗, 有没有可能就是在某个条件满足的时候自动触发呢? |
A: | 简单说,智能合约是在用户调用的时候触发执行的。更详细得说,用户在客户端发起一个智能合约调用,服务端会为该智能合约调用创建Context,然后将Context相关信息通过XBridge传给虚拟机,虚拟机通过调用SyscallService服务,来修改/获取智能合约的数据状态。 |
Q: | 是不是每种合约都需要有一条单独的链呢? |
A: | 合约由虚拟机来管理,一条链上可以部署很多智能合约,不过每一个智能合约都需要有不同的合约名字。 |
账户权限相关¶
Q: | 普通账户和合约账户的区别是什么? |
---|---|
A: | 账户是指一种本地或自定义权限的链上标识符。本地标识符称为用户账户,通常分配一个公钥和一个私钥,并对应一个address;自定义权限的链上标识符称为合约账户,通常分配一个或多个密钥或多个账户。 |
Q: | 什么是AK?超级链中所说的AK集合是什么? |
A: | AK是超级链中对一个公私钥用户账户的称呼,可以理解为Address,即通过一对公私钥转换来的一个唯一用户账户地址。AK集这是权限系统模块中的一种权限模型,是指多个AK组成的具有一定逻辑规则的权限模型,例如多个AK之间满足”或”的关系等。 |
Q: | 超级链的权限设计为什么采用的是ACL设计模式而不是ABAC的设计模式?关于权限验证目前是只有SIGN_THRESHOLD和SIGN_AKSET 的权限规则在使用吗? |
A: |
|
Q: | 在权限系统的设计中 身份账户的验证中会buildPermTree 和 validatePermTree,这个PermTree的设计思想是什么 这个Perm这个词是跟权限框架casbin中的PERM模型有关系吗? |
A: | 在身份验证中,PermTree主要是验证客户提供的账户与签名是否正确以及它们的权重是否满足对应的ACL要求。超级链的PERM与casbin并不相同,超级链的PERM指的是可扩展规则权限模型(Permission with Extensible Rule Model),超级链而账户/合约权限中可能嵌套其他账户,在验证账户的权重是否满足要求时,由于这种嵌套关系,权限会自然形成树形结构,每个节点都是一个账户或合约方法的权限ACL,而每个节点的ACL可以使用不同的权限规则模型,节点的子节点是代表子节点账户对父节点的授权关系。 |
Q: | 在执行背书签名时默认读取data/acl/addrs文件,例如multisig gen 时要读取这个文件的数据,请问这个文件的内容应该是什么? |
A: | 这是一个文本文件,文件内容中每一行表示一个需要签名的账户,是用于多重签名的地址,每个地址用换行分割。如果需要其他个人账户的授权,那么把个人账户的address作为一行写入这个文件中;如果需要合约账户授权,则需要使用”合约账户/个人账户address”的写法,表示需要某个合约账户ACL中的某个个人账户签名。 |
使用问题¶
Q: | 平行链是什么角色有权限创建?创建平行链的权限白名单是写在创世块中的么? |
---|---|
A: | 目前创建平行连有两种方法:一种直接通过xchain-cli的createChain命令,这种没有权限限制,只在本机创建;另一种是调用创建链的合约,这种情况可以在全网创建平行链,节点可以通过配置白名单的方式指定哪些用户能调用创建链的合约。创建平行链的权限白名单目前不在创世块中,而是在节点配置文件中。这么做的初衷是使每个节点有能力通过配置决定只托管符合自己要求的平行链。具体节点配置可以参考:https://github.com/xuperchain/xuperchain/blob/master/core/conf/xchain.yaml#L52 |
Q: | 自己搭链有root链和平行链之分么,还是自己搭的只是xuperchain的平行链?通过xchain-cli的createChain命令能在本地创建多条平行链吗? |
A: | 自己搭建的网络首先需要创建主链(代码里叫xuper链),然后再创建其他平行链,主链具有部分管理其他链的能力,例如创建平行链的系统合约是在主链中执行。通过xchain-cli的createChain命令可以在本地创建任意多条链。 |
Q: | 搭建多节点网络时,其他节点需要在配置里面指定根节点的网络地址吗? |
A: | 超级链的P2P网络具有自动路由功能,因此只要在配置中指定任意一个已经在网络中的节点地址即可。 |
Q: | 超级链中的rootBlockid和tipBlockid是什么意思,他们的关系是什么? |
A: | rootBlockid和tipBlockid都是指代区块的id,区块的id就是区块的唯一标识,通过两次SHA256生成。而rootBlockid特指创世区块id,即当前链的第一个区块的id;tipBlockid表示当前链主干分支最新的区块的id。也可以从rootBlockid和tipBlockid两个变量名称得知它们的意思,root有”根”的意思,而tip有”尖端”的意思。 |
Q: | 客户端发送交易后,怎么查询是否成功上链。通过querytx判断tx状态还是有其他事件机制?支持事件通知么? |
A: | 首先查询交易所在区块,然后查询区块是否在区块链主干上,如果交易所在区块在区块链主干上,表明此交易已经生效。事件通知暂不支持,后续有计划支持部分类型事务执行结果的事件通知。 |
Q: | RPC的文档中没有看到创建账户相关的说明? |
A: | 创建普通账户属于本地操作,该数据不上链,考虑到创建账户涉及到用户私钥传输,处于安全原因不提供RPC接口;创建合约账户属于系统合约调用,没有单独的RPC接口。 |
Q: | Windows系统可以编译运行么?可以用windows安装虚拟机来实现Linux环境么? |
A: | 目前暂不支持Windows的,要求是Linux或者Mac OS。可以使用虚拟机实现。 |
其他问题¶
Q: | 密码学中椭圆曲线选择使用P-256的考虑? |
---|---|
A: | 首先,P-256曲线目前依然在密码学界被广泛使用。其次,这次开源的是我们的基础版本,而在xchain的代码架构下,密码学相关的模块是插件化使用的,密码学crypto模块是可以独立研发并集成进开源框架中的。同时,在ECDSA之外,也已开源了多重签名、EDDSA、环签名等多种签名算法。在尚未开源的版本中,通过可变签名算法,我们已经支持国密/NIST的多条椭圆曲线。最后,这些算法和曲线被支持混合使用,开发者可以自由选择他们认为安全的曲线和算法来保护自己的数字资产。所以,敬请期待百度xchain后续的密码学相关开源进展。 |
Q: | 超级链有密钥保护机制吗?怎么实现的? |
A: | 有的,主要实现了两种方式:1) 通过支付密码在私钥加密后保存在本地。2)云端密钥保险箱。密钥保护功能在SDK里有实现,目前尚未开源。 |
Q: | 环签名、零知识证明等技术开源了么?他们的使用场景是什么? |
A: | 环签名、零知识证明等技术适用于对隐私保护有较高需求的网络中。目前环签名已经在crypto模块中开源,可以实现对交易发起者信息的混淆,例如在论文评审场景里,实现评审者对论文的匿名打分等;零知识证明目前尚未开源。 |
Q: | 超级链有区块链浏览器吗? |
A: | 暂时没有,在计划中,敬请关注。 |
Q: | 什么是VAT,它的作用是什么? |
A: | VAT (Verifiable Auto-generated Transaction, 可验证的自动生成事务)是智能合约在运行过程中,根据需要自动生成的系统Tx。这些Tx无法手动发起,结果也会上链,系统可验证。目前主要用在共识,提案等模块中,例如切换到tdpos共识,会自动生成候选人选举检票的VAT等。 |
Q: | 商用环境中,需要考虑数据隐私保护的问题,在这一块,xuper怎么考虑,有成熟的方案吗? |
A: | 在xuper+的应用中,有xuperdata,它是基于百度超级链、多方安全计算、数据隐私保护等技术的多企业数据安全协同计算方案。相关文档介绍:https://xuperchain.baidu.com/case/xuperdata |
Q: | 百度超级链的多盘存储是什么实现原理? |
A: | 超级链按照goleveldb的storage.go中的接口实现了自己的storage逻辑作为自己的file-system backed storage,代码可参考multi_disk_storage.go。具体实现中,leveldb的sst文件按照编号均匀散列放置在多块盘上,如果盘数扩容,第一次打开某个编号的sst文件的时候可能需要遍历尝试各个盘。另外,由于这个放置策略在compact的时候也生效,所以扩容的场景下,运行一段时间后,sst就会按新的路径均匀分布了。 |
词汇表¶
词汇 | 定义 |
---|---|
交易 | 对区块链进行状态更改的最小操作单元。通常表现为普通转帐以及智能合约调用。 |
区块(创世区块、普通区块、配置区块、当前区块) | 区块链中的最小确认单元,由零个或多个交易组成,一个区块中的交易要么全部生效,要么全部不生效。创世区块:区块链上第一个区块,确定分配规则、共识算法等系统级别的参数。配置区块:需要升级区块链系统级配置时,通过配置区块生效。 |
账本 | 存储区块数据、交易数据。 |
UTXO | 一种余额记账方式。用于存储账号的余额数据。 |
区块链 | 由若干个区块组成的DAG。从数据结构上来说,区块链就是一个DAG。 |
系统链 | 区块链网络中的第一条区块链实例。 |
平行链 | 从系统链衍生出来的子链,解决扩展性问题。 |
跨链 | 不同的区块链之间的通信操作,目的是实现区块链世界的价值互连,解决扩展性问题。 |
账号(用户账号、合约账号) | 一种本地或自定义权限的链上标识符。本地标识符称为用户账号,通常分配一个公钥和一个私钥;自定义权限的链上标识符称为合约账号,通常分配一个或多个密钥或多个账号。 |
密钥对(公钥、私钥) | 私钥以及由私钥生成的对应的公钥。私钥通常用于签名,公钥通常用于验证。 |
地址 | 与用户数据挂钩的最小单元。地址可以是合约账号,也可以是由公钥生成的一个长度为33的字符串。 |
签名(普通签名、多重签名) | 利用密码学哈希函数单向不可逆、抗碰撞特性,进行身份确认的一种机制。 |
共识 | 一种确认区块中交易集正确性以及交易上链顺序的机制。 |
委托权益证明 | 一种共识算法,通过选举出区块链网络中有限节点作为代表并轮流出块记账。 |
智能合约(系统级、用户合约) | 一个由计算机处理、可执行合约条款的交易协议,其总体目标是满足协议既定的条件,例如支付、抵押、保密协议。 |
权限(许可) | 一种安全机制,通过评估签名权限来验证一个或一组操作是否被正确授权。 |
虚拟机 | 智能合约的运行环境。通常包括合约上下文管理。 |
最长链 | 区块链中高度最大的分支。 |
DAG | 有向无环图。 |
双重消费 | 同一份数据同时消费多次。 |
最终一致性 | 存在某个时刻,整个系统达成一致状态。区块链满足最终一致性。 |
发起人 | 发起交易的账号,通常为用户账号或合约账号。 |
见证人 | 当选为当前周期内出块节点。 |
节点 | 区块链网络中的一个节点。 |
周期(Epoch) | 在委托权益证明共识算法中,一轮出块时间为一个周期。 |
提案 | 一种区块链系统级配置进行升级的机制。 |
查询 | 对区块链中的数据按照key进行查询。 |
对等网络 | 网络中的节点直接互联并交互信息,不需要借助第三方。 |
拜占庭(拜占庭问题、拜占庭容错) | 拜占庭问题:在一个需要共识系统中,由于作恶节点导致的问题。拜占庭容错:在一个需要共识系统中,即使出现作恶节点也能保证系统正常运转的能力。 |
状态转移系统 | 一个维护状态变化的系统。区块链通常被认为是一种状态转移系统,其中的状态通常包括账本、余额、合约数据。 |
读写集 | 用于支持智能合约并发执行的一种技术。 |
超级链小课堂¶

- xchain-cli,交互式命令行工具,直接使用xchain-cli即可发起操作,本质是通过rpc接口与服务端进行交互,可以从xuperchain库中获取;xchain-cli具有丰富的命令,包括创建账户、普通转账、合约部署以及调用、提案、投票、链上交易以及区块查询等功能;比如./xchain-cli transfer –to bob –amount 1就可以发起一笔向bob转账1个utxo的交易,更多命令可以通过./xchain-cli -h获取;
- SDK:提供一系列的API接口,用户可以基于提供的API接口做定制化的操作,相比xchain-cli更灵活;目前开源的SDK包括Go SDK,Python SDK,C# SDK;
- curl:直接通过curl命令来发起查询、构造交易等操作,客户端除了curl,不依赖任何三方库,此时需要服务端启动xchain-httpgw,然后通过curl http://localhost:8098/v1/get_balance -d ‘{“bcs”:[{“bcname”:”xuper”}, “address”:”bob”], 即可查询xuper链上bob的余额信息
- 查询用户UTXO面额:./xchain-cli account balance;
- 查询用户UTXO详细信息:./xchain-cli account list-utxo;可以通过该命令查看哪些utxo当前可用,哪些utxo当前被锁定以及哪些utxo当前被冻结;
- 合并UTXO:./xchain-cli account merge;可以通过该命令将用户多个utxo合并,来解决因UTXO太零散导致交易过大问题;
- 拆分UTXO:./xchain-cli account split;可以通过该命令将用户的一个UTXO进行拆分,解决用户无法同时发起多笔交易的问题;
- 自主安全高可靠:基于百度完全自主研发且开源的区块链技术搭建,满足中国区块链标准要求;
- 灵活便捷低门槛: 无需建链即可运用区块链技术,丰富的合约模板和强大的功能组件,降低使用门槛;
- 弹性付费成本低:具备计算和存储等资源的弹性付费能力,可以实现按需按量灵活计费,一分钱即可用;
- 节点开放公信强:由分布全国的超级联盟节点构成,面向社会开放节点接入,具备极强的公信力;
- 可以通过阅读超级链任意开源项目,包括源代码、文档,以便了解当前的开发方向;
- 找到自己感兴趣的功能或者模块;
- 实际开发时需要自测功能是否正常、性能是否符合预期,并运行make & make test检查是否通过所有单测;
- 发起一个Pull Request,如果你的代码合入到主干后,就有机会运行在线上机器上。
- 从GitHub上fork超级链的项目,并通过git拉取到本地;
- 在本地用git新起一个分支,贡献的代码全部放在本地分支上;
- 本地代码开发完毕,通过git push将本地分支代码提交至远程服务端;
- 点击GitHub对应项目栏下面的Pull Request按钮,填写需要合并的分支以及被合并的分支,然后点击create pull request即发起一个PR。