智能合约开发

FISCO BCOS平台目前支持Solidity、CRUD、Precompiled三种智能合约形式。

  • Solidity合约与以太坊相同,支持最新版本。
  • CRUD接口通过在Solidity合约中支持分布式存储预编译合约,可以实现将Solidity合约中数据存储在FISCO BCOS平台AMDB的表结构中,实现合约逻辑与数据的分离。
  • 预编译(Precompiled)合约使用C++开发,内置于FISCO BCOS平台,相比于Solidity合约具有更好的性能,其合约接口需要在编译时预先确定,适用于逻辑固定但需要共识的场景,例如群组配置。关于预编译合约的开发将在下一节进行介绍。

Solidity合约开发

使用合约CRUD接口

访问 AMDB 需要使用 AMDB 专用的智能合约Table.sol接口,该接口是数据库合约,可以创建表,并对表进行增删改查操作。

注解

为实现AMDB创建的表可被多个合约共享访问,其表名是群组内全局可见且唯一的,所以无法在同一条链上的同一个群组中,创建多个表名相同的表

Table.sol文件代码如下:

  1. pragma solidity ^0.4.24;
  2.  
  3. contract TableFactory {
  4. function openTable(string) public constant returns (Table); // 打开表
  5. function createTable(string,string,string) public returns(int); // 创建表
  6. }
  7.  
  8. // 查询条件
  9. contract Condition {
  10. //等于
  11. function EQ(string, int) public;
  12. function EQ(string, string) public;
  13.  
  14. //不等于
  15. function NE(string, int) public;
  16. function NE(string, string) public;
  17.  
  18. //大于
  19. function GT(string, int) public;
  20. //大于或等于
  21. function GE(string, int) public;
  22.  
  23. //小于
  24. function LT(string, int) public;
  25. //小于或等于
  26. function LE(string, int) public;
  27.  
  28. //限制返回记录条数
  29. function limit(int) public;
  30. function limit(int, int) public;
  31. }
  32.  
  33. // 单条数据记录
  34. contract Entry {
  35. function getInt(string) public constant returns(int);
  36. function getAddress(string) public constant returns(address);
  37. function getBytes64(string) public constant returns(byte[64]);
  38. function getBytes32(string) public constant returns(bytes32);
  39. function getString(string) public constant returns(string);
  40.  
  41. function set(string, int) public;
  42. function set(string, string) public;
  43. function set(string, address) public;
  44. }
  45.  
  46. // 数据记录集
  47. contract Entries {
  48. function get(int) public constant returns(Entry);
  49. function size() public constant returns(int);
  50. }
  51.  
  52. // Table主类
  53. contract Table {
  54. // 查询接口
  55. function select(string, Condition) public constant returns(Entries);
  56. // 插入接口
  57. function insert(string, Entry) public returns(int);
  58. // 更新接口
  59. function update(string, Entry, Condition) public returns(int);
  60. // 删除接口
  61. function remove(string, Condition) public returns(int);
  62.  
  63. function newEntry() public constant returns(Entry);
  64. function newCondition() public constant returns(Condition);
  65. }

重要

v2.2.0及之前版本CRUD接口的表中中主Key下可以有多条记录,v2.3.0将会废弃该特性。在v2.3.0之后的版本中主Key只允许唯一对应一条记录。

提供一个合约案例TableTest.sol,代码如下:

  1. pragma solidity ^0.4.24;
  2.  
  3. import "./Table.sol";
  4.  
  5. contract TableTest {
  6. event CreateResult(int count);
  7. event InsertResult(int count);
  8. event UpdateResult(int count);
  9. event RemoveResult(int count);
  10.  
  11. // 创建表
  12. function create() public returns(int){
  13. TableFactory tf = TableFactory(0x1001); // TableFactory的地址固定为0x1001
  14. // 创建t_test表,表的key_field为name,value_field为item_id,item_name
  15. // key_field表示AMDB主key value_field表示表中的列,可以有多列,以逗号分隔
  16. int count = tf.createTable("t_test", "name", "item_id,item_name");
  17. emit CreateResult(count);
  18.  
  19. return count;
  20. }
  21.  
  22. // 查询数据
  23. function select(string name) public constant returns(bytes32[], int[], bytes32[]){
  24. TableFactory tf = TableFactory(0x1001);
  25. Table table = tf.openTable("t_test");
  26.  
  27. // 条件为空表示不筛选 也可以根据需要使用条件筛选
  28. Condition condition = table.newCondition();
  29.  
  30. Entries entries = table.select(name, condition);
  31. bytes32[] memory user_name_bytes_list = new bytes32[](uint256(entries.size()));
  32. int[] memory item_id_list = new int[](uint256(entries.size()));
  33. bytes32[] memory item_name_bytes_list = new bytes32[](uint256(entries.size()));
  34.  
  35. for(int i=0; i<entries.size(); ++i) {
  36. Entry entry = entries.get(i);
  37.  
  38. user_name_bytes_list[uint256(i)] = entry.getBytes32("name");
  39. item_id_list[uint256(i)] = entry.getInt("item_id");
  40. item_name_bytes_list[uint256(i)] = entry.getBytes32("item_name");
  41. }
  42.  
  43. return (user_name_bytes_list, item_id_list, item_name_bytes_list);
  44. }
  45. // 插入数据
  46. function insert(string name, int item_id, string item_name) public returns(int) {
  47. TableFactory tf = TableFactory(0x1001);
  48. Table table = tf.openTable("t_test");
  49.  
  50. Entry entry = table.newEntry();
  51. entry.set("name", name);
  52. entry.set("item_id", item_id);
  53. entry.set("item_name", item_name);
  54.  
  55. int count = table.insert(name, entry);
  56. emit InsertResult(count);
  57.  
  58. return count;
  59. }
  60. // 更新数据
  61. function update(string name, int item_id, string item_name) public returns(int) {
  62. TableFactory tf = TableFactory(0x1001);
  63. Table table = tf.openTable("t_test");
  64.  
  65. Entry entry = table.newEntry();
  66. entry.set("item_name", item_name);
  67.  
  68. Condition condition = table.newCondition();
  69. condition.EQ("name", name);
  70. condition.EQ("item_id", item_id);
  71.  
  72. int count = table.update(name, entry, condition);
  73. emit UpdateResult(count);
  74.  
  75. return count;
  76. }
  77. // 删除数据
  78. function remove(string name, int item_id) public returns(int){
  79. TableFactory tf = TableFactory(0x1001);
  80. Table table = tf.openTable("t_test");
  81.  
  82. Condition condition = table.newCondition();
  83. condition.EQ("name", name);
  84. condition.EQ("item_id", item_id);
  85.  
  86. int count = table.remove(name, condition);
  87. emit RemoveResult(count);
  88.  
  89. return count;
  90. }
  91. }

TableTest.sol调用了 AMDB 专用的智能合约Table.sol,实现的是创建用户表t_test,并对t_test表进行增删改查的功能。t_test表结构如下,该表记录某公司员工领用物资和编号。

name*item_nameitem_id
BobLaptop100010001001

重要

客户端需要调用转换为Java文件的合约代码,需要将TableTest.sol和Table.sol放入控制台的contracts/solidity目录下,通过控制台的编译脚本sol2java.sh生成TableTest.java。

预编译合约开发

一. 简介

预编译(precompiled)合约是一项以太坊原生支持的功能:在底层使用c++代码实现特定功能的合约,提供给EVM模块调用。FISCO BCOS继承并且拓展了这种特性,在此基础上发展了一套功能强大并易于拓展的框架precompiled设计原理。 本文作为一篇入门指导,旨在指引用户如何实现自己的precompiled合约,并实现precompiled合约的调用。

二. 实现预编译合约

2.1 流程

实现预编译合约的流程:

  • 分配合约地址

调用solidity合约或者预编译合约需要根据合约地址来区分,地址空间划分:

地址用途地址范围
以太坊precompiled0x0001-0x0008
保留0x0008-0x0fff
FISCO BCOS precompied0x1000-0x1006
FISCO BCOS预留0x1007-0x5000
用户分配区间0x5001 - 0xffff
CRUD临时合约0x10000+
solidity其他

用户分配地址空间为0x5001-0xffff,用户需要为新添加的预编译合约分配一个未使用的地址,预编译合约地址必须唯一, 不可冲突

FISCO BCOS中实现的precompild合约列表以及地址分配:

地址功能源码(libprecompiled目录)
0x1000系统参数管理SystemConfigPrecompiled.cpp
0x1001表工厂合约TableFactoryPrecompiled.cpp
0x1002CRUD合约CRUDPrecompiled.cpp
0x1003共识节点管理ConsensusPrecompiled.cpp
0x1004CNS功能CNSPrecompiled.cpp
0x1005存储表权限管理AuthorityPrecompiled.cpp
0x1006并行合约配置ParallelConfigPrecompiled.cpp
  • 定义合约接口

同solidity合约,设计合约时需要首先确定合约的ABI接口, precomipiled合约的ABI接口规则与solidity完全相同,solidity ABI链接

定义预编译合约接口时,通常需要定义一个有相同接口的solidity合约,并且将所有的接口的函数体置空,这个合约我们称为预编译合约的接口合约,接口合约在调用预编译合约时需要使用。

  1. pragma solidity ^0.4.24;
  2. contract Contract_Name {
  3. function interface0(parameters ... ) {}
  4. ....
  5. function interfaceN(parameters ... ) {}
  6. }
  • 设计存储结构

预编译合约涉及存储操作时,需要确定存储的表信息(表名与表结构,存储数据在FISCO BCOS中会统一抽象为表结构), 存储结构

注解

不涉及存储操作可以省略该流程。

  • 实现调用逻辑

实现新增合约的调用逻辑,需要新实现一个c++类,该类需要继承Precompiled, 重载call函数, 在call函数中实现各个接口的调用行为。

  1. // libblockverifier/Precompiled.h
  2. class Precompiled
  3. {
  4. virtual bytes call(std::shared_ptr<ExecutiveContext> _context, bytesConstRef _param,
  5. Address const& _origin = Address()) = 0;
  6. };

call函数有三个参数:

std::shared_ptr<ExecutiveContext> _context : 保存交易执行的上下文

bytesConstRef _param : 调用合约的参数信息,本次调用对应合约接口以及接口的参数可以从_param解析获取

Address const& _origin : 交易发送者,用来进行权限控制

如何实现一个Precompiled类在下面的sample中会详细说明。

  • 注册合约

最后需要将合约的地址与对应的类注册到合约的执行上下文,这样通过地址调用precompiled合约时合约的执行逻辑才能被正确识别执行, 查看注册的预编译合约列表
注册路径:

  1. file libblockverifier/ExecutiveContextFactory.cpp
  2. function initExecutiveContext

2.2 示例合约开发

  1. // HelloWorld.sol
  2. pragma solidity ^0.4.24;
  3.  
  4. contract HelloWorld{
  5. string name;
  6.  
  7. function HelloWorld(){
  8. name = "Hello, World!";
  9. }
  10.  
  11. function get()constant returns(string){
  12. return name;
  13. }
  14.  
  15. function set(string n){
  16. name = n;
  17. }
  18. }

上述源码为solidity编写的HelloWorld合约, 本章节会实现一个相同功能的预编译合约,通过step by step使用户对预编译合约编写有直观的认识。 示例的c++源码路径

  1. libprecompiled/extension/HelloWorldPrecompiled.h
  2. libprecompiled/extension/HelloWorldPrecompiled.cpp
2.2.1 分配合约地址

参照地址分配空间,HelloWorld预编译合约的地址分配为:

  1. 0x5001
2.2.2 定义合约接口

需要实现HelloWorld合约的功能,接口与HelloWorld接口相同, HelloWorldPrecompiled的接口合约:

  1. pragma solidity ^0.4.24;
  2.  
  3. contract HelloWorldPrecompiled {
  4. function get() public constant returns(string) {}
  5. function set(string _m) {}
  6. }
2.2.3 设计存储结构

HelloWorldPrecompiled需要存储set的字符串值,所以涉及到存储操作,需要设计存储的表结构。

表名: _ext_hello_world_

表结构:

keyvalue
hello_keyhello_value

该表只存储一对键值对,key字段为hello_key,value字段为hello_value 存储对应的字符串值,可以通过set(string)接口修改,通过get()接口获取。

2.2.4 实现调用逻辑

添加HelloWorldPrecompiled类,重载call函数,实现所有接口的调用行为,call函数源码

用户自定义的Precompiled合约需要新增一个类,在类中定义合约的调用行为,在示例中添加HelloWorldPrecompiled类,然后主要需要完成以下工作:

  • 接口注册
  1. // 定义类中所有的接口
  2. const char* const HELLO_WORLD_METHOD_GET = "get()";
  3. const char* const HELLO_WORLD_METHOD_SET = "set(string)";
  4.  
  5. // 在构造函数进行接口注册
  6. HelloWorldPrecompiled::HelloWorldPrecompiled()
  7. {
  8. // name2Selector是基类Precompiled类中成员,保存接口调用的映射关系
  9. name2Selector[HELLO_WORLD_METHOD_GET] = getFuncSelector(HELLO_WORLD_METHOD_GET);
  10. name2Selector[HELLO_WORLD_METHOD_SET] = getFuncSelector(HELLO_WORLD_METHOD_SET);
  11. }
  • 创建表

定义表名,表的字段结构

  1. // 定义表名
  2. const std::string HELLO_WORLD_TABLE_NAME = "_ext_hello_world_";
  3. // 主键字段
  4. const std::string HELLOWORLD_KEY_FIELD = "key";
  5. // 其他字段字段,多个字段使用逗号分割,比如 "field0,field1,field2"
  6. const std::string HELLOWORLD_VALUE_FIELD = "value";
  1. // call函数中,表存在时打开,否则首先创建表
  2. Table::Ptr table = openTable(_context, HELLO_WORLD_TABLE_NAME);
  3. if (!table)
  4. {
  5. // 表不存在,首先创建
  6. table = createTable(_context, HELLO_WORLD_TABLE_NAME, HELLOWORLD_KEY_FIELD,
  7. HELLOWORLD_VALUE_FIELD, _origin);
  8. if (!table)
  9. {
  10. // 创建表失败,返回错误码
  11. }
  12. }

获取表的操作句柄之后,用户可以实现对表操作的具体逻辑。

  • 区分调用接口

通过getParamFunc解析_param可以区分调用的接口。
注意:合约接口一定要先在构造函数中注册

  1. uint32_t func = getParamFunc(_param);
  2. if (func == name2Selector[HELLO_WORLD_METHOD_GET])
  3. {
  4. // get() 接口调用逻辑
  5. }
  6. else if (func == name2Selector[HELLO_WORLD_METHOD_SET])
  7. {
  8. // set(string) 接口调用逻辑
  9. }
  10. else
  11. {
  12. // 未知接口,调用错误,返回错误码
  13. }
  • 参数解析与结果返回

调用合约时的参数包含在call函数的_param参数中,是按照Solidity ABI格式进行编码,使用dev::eth::ContractABI工具类可以进行参数的解析,同样接口返回时返回值也需要按照该编码格编码。Solidity ABI

dev::eth::ContractABI类中我们需要使用abiIn abiOut两个接口,前者用户参数的序列化,后者可以从序列化的数据中解析参数

  1. // 序列化ABI数据, c++类型数据序列化为evm使用的格式
  2. // _id : 函数接口声明对应的字符串, 一般默认为""即可。
  3. template <class... T> bytes abiIn(std::string _id, T const&... _t)
  4. // 将序列化数据解析为c++类型数据
  5. template <class... T> void abiOut(bytesConstRef _data, T&... _t)

下面的示例代码说明接口如何使用:

  1. // 对于transfer接口 : transfer(string,string,uint256)
  2.  
  3. // 参数1
  4. std::string str1 = "fromAccount";
  5. // 参数2
  6. std::string str2 = "toAccount";
  7. // 参数3
  8. uint256 transferAmoumt = 11111;
  9.  
  10. dev::eth::ContractABI abi;
  11. // 序列化, abiIn第一个string参数默认""
  12. bytes out = abi.abiIn("", str1, str2, transferAmoumt);
  13.  
  14. std::string strOut1;
  15. std::string strOut2;
  16. uint256 amoumt;
  17.  
  18. // 解析参数
  19. abi.abiOut(out, strOut1, strOut2, amount);
  20. // 解析之后
  21. // strOut1 = "fromAccount";
  22. // strOut2 = "toAccount"
  23. // amoumt = 11111

最后,给出HelloWorldPrecompiled call函数的完整实现源码链接

  1. bytes HelloWorldPrecompiled::call(dev::blockverifier::ExecutiveContext::Ptr _context,
  2. bytesConstRef _param, Address const& _origin)
  3. {
  4. // 解析函数接口
  5. uint32_t func = getParamFunc(_param);
  6. //
  7. bytesConstRef data = getParamData(_param);
  8. bytes out;
  9. dev::eth::ContractABI abi;
  10.  
  11. // 打开表
  12. Table::Ptr table = openTable(_context, HELLO_WORLD_TABLE_NAME);
  13. if (!table)
  14. {
  15. // 表不存在,首先创建
  16. table = createTable(_context, HELLO_WORLD_TABLE_NAME, HELLOWORLD_KEY_FIELD,
  17. HELLOWORLD_VALUE_FIELD, _origin);
  18. if (!table)
  19. {
  20. // 创建表失败,无权限?
  21. out = abi.abiIn("", CODE_NO_AUTHORIZED);
  22. return out;
  23. }
  24. }
  25.  
  26. // 区分调用接口,各个接口的具体调用逻辑
  27. if (func == name2Selector[HELLO_WORLD_METHOD_GET])
  28. { // get() 接口调用
  29. // 默认返回值
  30. std::string retValue = "Hello World!";
  31. auto entries = table->select(HELLOWORLD_KEY_FIELD_NAME, table->newCondition());
  32. if (0u != entries->size())
  33. {
  34. auto entry = entries->get(0);
  35. retValue = entry->getField(HELLOWORLD_VALUE_FIELD);
  36. }
  37. out = abi.abiIn("", retValue);
  38. }
  39. else if (func == name2Selector[HELLO_WORLD_METHOD_SET])
  40. { // set(string) 接口调用
  41.  
  42. std::string strValue;
  43. abi.abiOut(data, strValue);
  44. auto entries = table->select(HELLOWORLD_KEY_FIELD_NAME, table->newCondition());
  45. auto entry = table->newEntry();
  46. entry->setField(HELLOWORLD_KEY_FIELD, HELLOWORLD_KEY_FIELD_NAME);
  47. entry->setField(HELLOWORLD_VALUE_FIELD, strValue);
  48.  
  49. int count = 0;
  50. if (0u != entries->size())
  51. { // 值存在,更新
  52. count = table->update(HELLOWORLD_KEY_FIELD_NAME, entry, table->newCondition(),
  53. std::make_shared<AccessOptions>(_origin));
  54. }
  55. else
  56. { // 值不存在,插入
  57. count = table->insert(
  58. HELLOWORLD_KEY_FIELD_NAME, entry, std::make_shared<AccessOptions>(_origin));
  59. }
  60.  
  61. if (count == CODE_NO_AUTHORIZED)
  62. { // 没有表操作权限
  63. PRECOMPILED_LOG(ERROR) << LOG_BADGE("HelloWorldPrecompiled") << LOG_DESC("set")
  64. << LOG_DESC("non-authorized");
  65. }
  66. out = abi.abiIn("", u256(count));
  67. }
  68. else
  69. { // 参数错误,未知的接口调用
  70. PRECOMPILED_LOG(ERROR) << LOG_BADGE("HelloWorldPrecompiled") << LOG_DESC(" unkown func ")
  71. << LOG_KV("func", func);
  72. out = abi.abiIn("", u256(CODE_UNKNOW_FUNCTION_CALL));
  73. }
  74.  
  75. return out;
  76. }
2.2.5 注册合约并编译源码
  • 注册开发的预编译合约。修改FISCO-BCOS/cmake/templates/UserPrecompiled.h.in,在下面的函数中注册HelloWorldPrecompiled合约的地址。默认已有,取消注释即可。
  1. void dev::blockverifier::ExecutiveContextFactory::registerUserPrecompiled(dev::blockverifier::ExecutiveContext::Ptr context)
  2. {
  3. // Address should in [0x5001,0xffff]
  4. context->setAddress2Precompiled(Address(0x5001), std::make_shared<dev::precompiled::HelloWorldPrecompiled>());
  5. }
  • 编译源码。请参考这里,安装依赖并编译源码。

注意:实现的HelloWorldPrecompiled.cpp和头文件需要放置于FISCO-BCOS/libprecompiled/extension目录下。

  • 搭建FISCO BCOS联盟链。 假设当前位于FISCO-BCOS/build目录下,则使用下面的指令搭建本机4节点的链指令如下。更多选项参考这里
  1. bash ../tools/build_chain.sh -l "127.0.0.1:4" -e bin/fisco-bcos

三 调用

从用户角度,预编译合约与solidity合约的调用方式基本相同,唯一的区别是solidity合约在部署之后才能获取到调用的合约地址,预编译合约的地址为预分配,不用部署,可以直接使用。

3.1 使用控制台调用HelloWorld预编译合约

在控制台contracts/solidity创建HelloWorldPrecompiled.sol文件,文件内容是HelloWorld预编译合约的接口声明,如下

  1. pragma solidity ^0.4.24;
  2. contract HelloWorldPrecompiled{
  3. function get() public constant returns(string);
  4. function set(string n);
  5. }

使用编译出的二进制搭建节点后,部署控制台v1.0.2以上版本,然后执行下面语句即可调用 ../../_images/call_helloworld.png

3.2 solidity调用

我们尝试在Solidity合约中创建预编译合约对象并调用其接口。在控制台contracts/solidity创建HelloWorldHelper.sol文件,文件内容如下

  1. pragma solidity ^0.4.24;
  2. import "./HelloWorldPrecompiled.sol";
  3.  
  4. contract HelloWorldHelper {
  5. HelloWorldPrecompiled hello;
  6. function HelloWorldHelper() {
  7. // 调用HelloWorld预编译合约
  8. hello = HelloWorldPrecompiled(0x5001);
  9. }
  10. function get() public constant returns(string) {
  11. return hello.get();
  12. }
  13. function set(string m) {
  14. hello.set(m);
  15. }
  16. }

部署HelloWorldHelper合约,然后调用HelloWorldHelper合约的接口,结果如下 ../../_images/call_helloworldHelper.png