一、目的
本文档从 C++ 及智能合约基本概念出发再到实战运行智能合约,介绍了中移链合约开发的基本流程,同时对常见问题做出梳理。本文档将以hello
合约作为示例介绍智能合约如何在链上工作,适合刚接触合约开发的开发人员用来了解 EOS 智能合约如何编写、编译、部署、动作调用以及管理授权,帮助其快速了解以及上手智能合约。
二、智能合约介绍
区块链作为一种分布式可信计算平台,去中心化是其最本质的特征。每笔交易的记录不可篡改地存储在区块链上。智能合约中定义可以在区块链上执行的动作action
和交易transaction
的代码。可以在区块链上执行,并将合约执行状态作为该区块链实例不可变历史的一部分。
因此,开发人员可以依赖该区块链作为可信计算环境,其中智能合约的输入、执行和结果都是独立的,不受外部影响。
三、术语解释
EOSIO.CDT(合约开发工具包)
EOSIO.CDT是WebAssembly(WASM)的工具链,也是用于促进EOSIO平台智能合约开发的一组工具。使用C++编程语言创建EOSIO智能合约。EOSIO.CDT提供构建智能合约所需的库和工具。
WebAssembly(WASM)
用于执行可移植二进制代码格式的虚拟机,托管在nodeos中。
应用程序二进制接口(ABI)
定义如何将数据编组进出WebAssembly虚拟机的接口。
动作(Action)
智能合约公开的功能,通过批准的交易将正确的参数传递给 EOSIO 网络来执行。
李嘉图合约(Ricardian Contract)
在基于EOSIO的区块链环境中,李嘉图合约是一个伴随智能合约的数字文档,定义了智能合约与其用户之间交互的条款和条件,以人类可读的文本编写,然后对其进行加密签署和验证。对于人和程序来说,它都很容易阅读,并有助于为智能合约和其用户之间的交互中可能出现的任何情况提供清晰的理解。
四、开发流程
(一)知识和环境准备
1、了解 C++ 语言
EOSIO 的智能合约都由 C++ 代码编写,因此,您应当具有一定的 C++ 编程能力。不过,智能合约对 C++ 的使用并不会过于复杂。 C++ 是一种静态类型的、编译式的编程语言,支持过程化编程、面向对象编程和泛型编程。因此, C++ 是在编译时执行类型检查,而不是在运行时执行类型检查。您所编写的.cpp
文件只有在通过编译后,才能尝试运行。
2、代码编辑器或 IDE
任何支持C++的IDE都可以用于编写智能合约,本文中智能合约的编译和部署都将在终端命令中进行。因此您使用的编辑器甚至可以不具备编译C++代码的功能。若仅用于编写代码,.txt
文本文件就可以胜任。但为了方便编写和检查,还是建议使用例如VSCode的常规IDE,或智能合约开发IDE,EOS Stuido。
3、完全配置的本地开发环境
具体可参考 中移链(基于EOS)测试环境搭建
(二)编写合约
1、创建文件
创建智能合约的过程中,通常会创建两个文件,分别是包含智能合约类声明的头文件.hpp
,以及包含智能合约操作实现的文件.cpp
。如果代码内容十分简单时,也可以直接在一个.cpp
文件中编写声明和实现。此处我们仅用一个.cpp
文件来展示hello
样例。但往后的程序都应采用规范的声明实现分离写法。
创建一个名为hello
的新文件夹来存储您的智能合约文件,并进入目录:
mkdir hello
cd hello
创建新文件hello.cpp
:
touch hello.cpp
使用您的文本编辑器打开它后,可以开始编写智能合约代码。
2、编写代码
编写智能合约代码,主要包括以下四个步骤:
(1)使用include
导入 EOSIO 基础库
#include
eosio.hpp
中包含编写一个智能合约所需要的基础类,例如eosio::contract
。
(2)创建一个类,并使它继承eosio::contract
使用[[eosio::contract]]
属性通知EOSIO.CDT这是一个智能合约。
添加一行代码:
class [[eosio::contract]] hello : public eosio::contract {};
这表明hello
作为一个智能合约,公有的继承了eosio::contract
类型。
(3)添加公共访问说明符和使用声明
在C++
的类中,可以声明公有、私有、保护三种类型的类成员和类成员函数。其中,构造函数是在类被创建时需要运行的类成员函数。我们创建的hello
类作为eosio::contract
的派生类(子类),可以继承基类(父类)的接口和实现。
使用using
添加一行代码表示声明了eosio::contract
的默认基类构造函数:
public:
using eosio::contract::contract;
(4)添加动作hi
使用[[eosio::action]]
通知 EOSIO.CDT 这是一个合约动作。
添加以下代码:
[[eosio::action]] void hi(eosio::name user){
print("Hello, ", user);
}
在EOSIO中,智能合约的动作action
就类似于C++中的类成员函数。此处添加的hi
动作功能为:接收一个类型为eosio::name
的参数,打印与该参数打招呼的信息。其中eosio::print
是包含在eosio/eosio.hpp
中的函数,可以直接使用。
(5)保存文件
现在,hello.cpp
文件应该如下所示:
#include
class [[eosio::contract]] hello : public eosio::contract {
public:
using eosio::contract::contract;
[[eosio::action]] void hi( eosio::name user ) {
print( "Hello, ", user);
}
};
(三)编译并部署合约
成功创建智能合约后,要对合约进行编译和部署。
1、编译
使用eosio-cpp
命令编译hello.cpp
文件
要将智能合约部署到区块链上,首先使用eosio-cpp
工具编译智能合约。编译构建一个 WebAssembly 文件.wasm
和一个相应的应用程序二进制接口文件.abi
。
WebAssenbly 并不是一种编程语言,而是一种编译器的编译目标,可以把.wasm
文件当成是.cpp
文件通过编译以后生成的文件。.wasm
文件是区块链中的 WebAssembly 引擎执行的二进制代码。WebAssembly 引擎托管在 nodeos 守护进程中并执行智能合约代码。.abi
文件定义了数据如何编组进出 WASM 引擎。
在与合约程序相同的文件夹中运行以下命令,或在其他位置使用绝对或相对路径来引用该文件:
eosio-cpp -abigen -o hello.wasm hello.cpp
此时文件夹中创建了两个新文件:hello.wasm
和hello.abi
。
2、部署
将hello
合约部署到同名账户
使用以下命令将编译好的hello.wasm
和hello.abi
文件部署到区块链上的hello账户:
cleos set contract hello ./hello -p hello@active
cleos set contract
命令后必须跟随部署合约的账户名,此处为同名账户hello
。
如果您没有hello账户,请参考 中移链(基于EOS)测试环境搭建中的 (六)创建开发账户
运行此步骤前,请确保您的账户中有处于解锁状态的钱包。cleos会寻找一个解锁状态的钱包以获取您使用的权限的私钥。在本例中,使用的权限为-p hello@active
,即hello账户的active权限。
成功部署后,会得到类似下图的返回信息:
Reading WASM from ./hello/hello.wasm...
Skipping set abi because the new abi is the same as the existing abi
Publishing contract...
executed transaction: 5f49530dcef3221d51f3160deb3f9ba0911cc6b93b2ce0a6560dff271178f13b 14288 bytes 23172 us
# eosio <= eosio::setcode "00000000001aa36a0000ab8c020061736d0100000001d4012260000060037f7f7f017f60037f7e7f017e60047f7f7f7f006...
(四)调用动作
使用cleos push action
命令,用已有账户调用hello
合约中的hi
动作:
cleos push action hello hi '["bob"]' -p bob@active
应该产生:
executed transaction: 5be02a763ba87db15a22ae2135e7fc6ec5b0ee8fed3a9a012f65f5ebde706b3e 104 bytes 2459 us
# hello <= hello::hi {"user":"bob"}
>> Hello, bob
可以看到,该合约能够允许任何账户向任何用户打招呼。例如,将账户更换为alice后:
cleos push action hello hi '["bob"]' -p alice@active
产生结果如下,依旧可以实现对bob的动作:
executed transaction: bfb8787117257f8e7d3f659509678f24a7bdb040a6b1a846fa090d428bc06540 104 bytes 271 us
# hello <= hello::hi {"user":"bob"}
>> Hello, bob
(五)动作的授权
EOSIO 区块链使用非对称密码学来验证推送交易的账户是否已使用匹配的私钥签署了交易。使用账户权限表来检查账户是否具有执行操作所需的权限。使用授权是保护智能合约的第一步。
本文档提供了四种方式在合约代码中进行授权检查。
1、has_auth(name n) 函数
验证指定账户是否与调用动作的账户相符,返回bool
值。
以hi
动作为例,如果我们希望动作只能向调用账户进行问好,而不相符的账户名进行消息提示,可以将动作部分的代码做如下调整:
[[eosio::action]] void hi( eosio::name user ) {
if(has_auth( user )){
print("Hello, ", eosio::name{user} );
}else{
print("This is not ",eosio::name{user} );
}
}
重新编译并部署合约:
eosio-cpp -abigen -o hello.wasm hello.cpp
cleos set contract hello /home/xxx/biosboot/genesis/hello -p hello@active
使用alice账户对bob账户和alice账户分别调用hi
动作。结果分别如下:
cleos push action hello hi '["bob"]' -p alice@active
executed transaction: 20a297df95fe19840893305a8f18e6380ee3482a3773885224f6381487559105 104 bytes 601 us
# hello <= hello::hi {"user":"bob"}
>> This is not bob
cleos push action hello hi '["alice"]' -p alice@active
executed transaction: 3f12347d3458204be643a2c4cef5ef3542994ed3bb320466aac423e1a983e8d7 104 bytes 204 us
# hello <= hello::hi {"user":"alice"}
>> Hello, alice
2、require_auth(name n) 函数
验证指定账户是否与调用动作的账户相符,不相符则直接报错失败。
与has_auth
函数不同,此函数会在验证失败时直接停止运行。同样以hi
动作为例,如果我们希望动作只能向调用账户进行问好,而对不相符的账户直接显示失败,可以将动作部分的代码做如下调整:
[[eosio::action]] void hi( eosio::name user ) {
require_auth( user );
print("Hello, ", eosio::name{user} );
}
重新编译并部署合约:
eosio-cpp -abigen -o hello.wasm hello.cpp
cleos set contract hello /home/xxx/biosboot/genesis/hello -p hello@active
使用alice账户对bob账户和alice账户分别调用hi
动作。结果分别如下:
cleos push action hello hi '["bob"]' -p alice@active
Error 3090004: Missing required authority
Ensure that you have the related authority inside your transaction!;
If you are currently using 'cleos push action' command, try to add the relevant authority using -p option.
Error Details:
missing authority of bob
pending console output:
cleos push action hello hi '["alice"]' -p alice@active
executed transaction: 495b0e8d6fa3610f5b91ffd28eb4013d1a53a1e1e13046b6a3069566bfeaf65f 104 bytes 168 us
# hello <= hello::hi {"user":"alice"}
>> Hello, alice
3、require_auth2(capi_name name, capi_name permission) 函数
验证指定账户和权限是否与调用动作的账户和权限相符,不相符则直接报错失败。
此函数比之前增加了对账户权限的限制。同样以hi
动作为例,如果我们希望动作只能由账户的active
权限进行调用,而对不相符的账户直接显示失败,可以将动作部分的代码做如下调整:
[[eosio::action]] void hi( eosio::name user ) {
require_auth2(user.value, "active"_n.value);
print("Hello, ", eosio::name{user} );
}
重新编译并部署合约:
eosio-cpp -abigen -o hello.wasm hello.cpp
cleos set contract hello /home/xxx/biosboot/genesis/hello -p hello@active
使用alice账户的family和active权限分别对alice账户调用hi
动作。结果分别如下:
cleos push action hello hi '["alice"]' -p alice@family
Error 3090003: Provided keys, permissions, and delays do not satisfy declared authorizations
Ensure that you have the related private keys inside your wallet and your wallet is unlocked.
Error Details:
transaction declares authority '{"actor":"alice","permission":"family"}', but does not have signatures for it.
cleos push action hello hi '["alice"]' -p alice@active
executed transaction: 055228767c6f23c283 910ebc348d8a76d444fa1165454c9886dd58b940023ef5 104 bytes 395 us
# hello <= hello::hi {"user":"alice"}
>> Hello, alice
4、check(bool pred, ...) 函数
断言,如果pred
为假,则使用提供的消息进行反馈。例如:
eosio::check(a == b, "a does not equal b");
因此,之前我们实现的在参数与账户不相符时打印错误信息的代码可以优化为:
[[eosio::action]] void hi( eosio::name user ) {
eosio::check(has_auth(user), "User is not authorized to perform this action.");
print("Hello, ", eosio::name{user} );
}
重新编译并部署合约:
eosio-cpp -abigen -o hello.wasm hello.cpp
cleos set contract hello /home/xxx/biosboot/genesis/hello -p hello@active
使用alice账户对bob账户和alice账户分别调用hi
动作。结果分别如下:
cleos push action hello hi '["bob"]' -p alice@active
Error 3050003: eosio_assert_message assertion failure
Error Details:
assertion failure with message: User is not authorized to perform this action.
pending console output:
cleos push action hello hi '["alice"]' -p alice@active
executed transaction: 86887c76f45d9c0f9abc017f2cfc49132bd5d0c1d6bbd7f41aeb7bc1675ab42c 104 bytes 172 us
# hello <= hello::hi {"user":"alice"}
>> Hello, alice
关于上链
在之后的代码实践中,开发人员可以通过以上这四种授权检查代码的搭配来实现合约中动作的授权管理。
对于触发Error导致方法运行中断的情况,交易记录不会上链。只有当交易ID产生,动作正常运行完成,此次记录才会记录在链上。
对于上文的举例来说,第一种代码的错误案例完成了上链。因为交易正常结束,并打印出了错误信息。而后面三种分别触发了Error 3090003
、Error 3090004
、Error 3050003
,导致动作中断,数据不会上链。
五、常见问题
(一)部署合约时遇到错误
<3>error 2022-08-08T08:44:27.913 cleos main.cpp:4371 operator() ] Failed with error: unspecified (0)
Unable to resolve path './hello'
遇到Unable to resolve path
错误,可以将合约地址改为绝对路径,避免因相对路径产生的报错。
(二)require_auth2编译错误
/home/xxx/biosboot/genesis/hello/hello.cpp:6:10: error: use of undeclared identifier 'require_auth2'; did you mean 'eosio::internal_use_do_not_use::require_auth2'?
require_auth2(user.value, "active"_n.value);
^~~~~~~~~~~~~
eosio::internal_use_do_not_use::require_auth2
常规情况中可依据报错提示信息将require_auth2
补充为eosio::internal_use_do_not_use::require_auth2
解决。但本例中该前缀已经表明这是一个不推荐使用的方法。
故推荐的解决方法为引入action
头文件。
#include <../capi/eosio/action.h>
(三)编译警告:缺少李嘉图合约
合约编译后会遇到警告,不会影响到合约部署后的正常运行。
警告信息如下:
Warning, empty ricardian clause file
Warning, action does not have a ricardian contract
此警告说明合约中的动作不具备李嘉图合约。
从 EOSIO.CDT v1.4.0 开始,ABI 生成器将尝试自动将合约和条款导入到生成的 ABI 中。对此有一些警告,包括文件的严格命名策略和用于标记每个李嘉图合约和每个条款的 HTML 标记。
李嘉图合约应该存放在一个名为.contracts.md
的文件中,条款名为.clauses.md
,在同一文件夹中。