写点什么

合约编写基础知识介绍基础篇

作者:BSN研习社
  • 2023-07-10
    北京
  • 本文字数:6802 字

    阅读完需:约 22 分钟

合约编写基础知识介绍基础篇

本文档介绍了合约编写的基础知识,包括合约初始化、action 和权限的相关知识。适用于想要了解智能合约编写基础知识的初学者和开发者,帮助其快速了解和上手 EOS 智能合约的编写。作为智能合约的基础篇,本文仅涉及合约初始化、action 和权限方面的内容。


01

智能合约介绍


区块链作为一种分布式可信计算平台,去中心化是其最本质的特征。每笔交易的记录不可篡改地存储在区块链上。智能合约中定义可以在区块链上执行的动作 action 和交易 transaction 的代码。可以在区块链上执行,并将合约执行状态作为该区块链实例不可变历史的一部分。


因此,开发人员可以依赖该区块链作为可信计算环境,其中智能合约的输入、执行和结果都是独立的,不受外部影响。


02

合约编写基础知识介绍


(一)合约初始化


1、合约的构造函数


在 EOSIO 中,智能合约通过 C++编写,并通过 WASM(WebAssembly)字节码形式部署到区块链网络上。当部署完成后,可以调用合约的 action 执行相应的操作。在合约被部署后,会自动执行其初始化函数,以初始化合约的数据和状态。


智能合约的构造函数是在合约部署时被调用的,用于初始化合约的状态和数据。构造函数是一个特殊的成员函数,它没有返回值类型,且函数名与合约名相同。


以下是一个简单的 EOS 智能合约的构造函数示例:


#include <eosio/eosio.hpp>
using namespace eosio;
class [[eosio::contract("hello")]] hello : public contract {public: hello(name receiver, name code, datastream<const char*> ds) : contract(receiver, code, ds) { eosio::print_f("hello is ready. "); }};
复制代码


在上述代码中,构造函数的参数包括:


receiver:合约实例的接收者,指定该实例的账户名。


code:合约所属的账户名。


ds:数据流对象,用于序列化和反序列化合约数据。通常在构造函数中会使用它来初始化合约的状态。


构造函数必须继承自 contract 类,并调用 contract 类的构造函数来初始化合约状态和数据。


在构造函数中,你可以执行各种初始化操作,例如分配初始资源,初始化数据结构,加载配置等等。


需要注意的是,constructor 函数只会在合约部署时执行一次,之后无法再次执行。因此,合约构造函数对于初始化合约状态以及设置合约的权限和授权等功能至关重要。如果需要修改合约的数据和状态,需要通过 action 来调用其他函数。


2、ds


ds 的全称为 datastream,是一个用于读写字节形式的数据流。数据流对象是 EOS 智能合约中的一个重要对象,用于序列化和反序列化合约数据。在 EOS 中,合约与区块链节点通信时需要将数据序列化为二进制格式,同时在合约内部也需要将二进制格式的数据反序列化为对应的数据类型进行处理。ds 可以帮助合约开发人员方便地实现这些数据序列化和反序列化操作。


(1)ds 的使用场景


作为智能合约构造函数的参数


在智能合约中,ds 用于序列化和反序列化合约数据。通常在构造函数中会使用它来初始化合约的状态。


在智能合约中读取和写入参数


在智能合约中,通常需要读取和写入一些参数,例如在调用一个合约的 action 时需要传入一些参数,或者在向表中添加数据时需要指定一些字段值。对于这些参数,可以使用 ds 对它们进行序列化和反序列化,以便在智能合约中进行处理。


与其他合约进行通信


在 EOSIO 中,多个合约可以相互调用和通信。当需要将数据传递给其他合约时,可以使用 ds 将数据进行序列化,以便在不同合约之间传递二进制数据流。


在智能合约中处理复杂的数据类型


在智能合约中,您可以自定义一些复杂的数据类型,例如结构体、对象等等。当需要在智能合约中处理这些复杂的数据类型时,可以使用 ds 对它们进行序列化和反序列化,以便在智能合约中进行处理。


(2)使用 ds 的好处


易于序列化和反序列化:使用 ds 对象,可以方便地将各种数据类型序列化为二进制格式,或者将二进制格式反序列化为对应的数据类型。这对于编写合约代码以及与区块链节点通信非常重要。


减少空间开销:使用 ds 可以减少数据在内存中的占用空间,从而节省资源。序列化后的二进制数据通常比原始数据占用更少的空间,并且也更容易在网络上传输。


提高效率:使用 ds 可以提高代码执行效率。序列化和反序列化操作通常需要大量的计算资源和时间,但 ds 可以提供高效的数据流处理功能,从而提高代码的执行效率。


(3)ignore 关键字


ignore 类型指令告诉数据流忽略某个类型,但是让 ABI 生成器添加正确的类型信息。当前非 ignore 类型不能紧随 ignore 类型在方法定义中出现,例如 void foo(float, ignore)是允许的,而 void foo(float, ignore, int) 不允许。


在 EOSIO 智能合约中,使用 ignore 关键字可以标记不需要使用的参数。在函数参数中使用 ignore 关键字可以告诉虚拟机不要解析这个参数,而是让我们手动解析这个参数。


当 ACTION 函数执行过程中需要使用被 ignore 忽略的字段时,可以使用预定义的数据流对象_ds 进行进一步的反序列化操作。_ds 对象可以帮助我们对操作数据进行进一步的反序列化和处理,尤其是对被 ignore 忽略的字段进行处理。例如:


 #include <eosio/eosio.hpp>#include <eosio/ignore.hpp>
using namespace eosio;
struct person{ name key; std::string first_name; std::string last_name; uint64_t age; std::string street; std::string city; std::string state;};
class [[eosio::contract("addressbook")]] addressbook : public eosio::contract{public: addressbook(name receiver, name code, datastream<const char *> ds) : contract(receiver, code, ds) {}
[[eosio::action]] void test( name user, ignore<uint64_t>, ignore<person>) { print( "Hello", user );
// 读取 ignore 数据。 uint64_t id; person p; _ds >> id >> p; print(id); print("##") print(p.city,"##",p.street); }};
复制代码


使用以下命令调用 test 动作:

cleos push action addressbook test '["alice",5,{"key":"alice","first_name":"alice","last_name":"final","age":32,"street":"Beijin","city":"heping","state":"amsterdam"}]' -p alice@active
复制代码


运行结果如下:


executed transaction: c4195834f803c38964b00e0c0baf7ee6fd15b2ca17df7782641c8ec4f81dc70d  160 bytes  260 us#   addressbook <= addressbook::test            "0000000000855c3405000000000000000000000000855c3405616c6963650566696e616c2000000000000000064265696a6...>> Helloalice5##heping##Beijin
复制代码


该示例中,通过_ds 数据流对象,从输入数据流中反序列化了一个 uint64_t 类型的变量 id,以及一个 person 类型的变量 p。这些数据本来被标记为 ignore,但通过对 ignore 数据进行反序列化,可以将这些数据存储到相应的变量中,并在 ACTION 函数中进一步处理这些数据。


需要注意的是,当我们使用 ignore 关键字时,虚拟机并不会跳过这个字段的解析。相反,虚拟机会将这个字段解析为一个空值,并将其留在数据流中。如果我们需要使用这个字段,我们可以使用_ds 对象来手动解析这个字段。


(二)action


一个关于 action 的打包表示方式,同时还包含了有关授权级别的元数据信息。在 EOSIO 中,当一个 action 被发送时,它将被打包成二进制格式,并在网络上进行传输。这个二进制格式包含了 action 的操作名称、操作数据以及授权级别等信息,以便 EOSIO 网络节点能够正确地解析和执行该 action。元数据信息包括了关于执行该 action 所需的授权级别、签名以及其他验证信息。


1、action 示例

#include <eosio/eosio.hpp>class [[eosio::contract]] hello : public eosio::contract {  public:      using eosio::contract::contract;      // 定义一个名为hi的action,需要传入用户名      [[eosio::action]] void hi( eosio::name user ) {         // 打印"Hello,用户名"         print( "Hello, ", user);      }};
复制代码

这里类名和合约账户名没有关系,但是为了方便管理,合约文件名、类名以及合约账户最好一致。合约定义了一个名为"hi"的 action,它接受一个"name" ,里面只有一句打印的语句,任何用户都可以调用该 action,相应的合约会打印 Hello,用户名作为回应。一个合约里面可以定义多个 action,而且这些 action 还可以互相调用。


注意:


一个 action 必须是 C++合约类的成员方法


使用[[eosio::action]]来标识这是一个 action,否则就是一个普通的类成员函数


访问级别必须是公开的 public


返回值必须是空值


可以接受任意数量的输入参数


2、action_wrapper


action_wrapper 是 action 的包装器,方便自身或者其他合约调用声明的 action。


(1)如何使用 action_wrapper


在名为 hello.hpp 的文件中,有一个名为 hi 的操作。


#include <eosio/eosio.hpp>
class [[eosio::contract("hello")]] hello : public contract { public: using eosio::contract::contract; [[eosio::action]] void hi( eosio::name user ) ; [[eosio::action]] void sayhi( eosio::name user ); };
复制代码


为了定义 hi 动作的 Action Wrapper,可以使用 eosio::action_wrapper 模板。模板的第一个参数为 eosio::name 类型的动作名称,第二个参数为指向动作方法的引用。

using hi_action = eosio::action_wrapper<"hi"_n, &hello::hi>;
复制代码

要使用 action wrapper,需要包含定义 action wrapper 的头文件。在上面的例子中,可以将以下代码添加到合约的头文件中:

#include <hello.hpp>
复制代码

然后,实例化上述定义的 hi_action,将要发送该 action 的合约作为第一个参数指定。在这种情况下,假定合约部署到了 hello 账户,然后通过调用 get_self()方法获取自身账户,最后指定 active 权限(你可以根据需求修改这两个参数)来定义一个包含两个参数的结构体。

hi_action wrapper{"hello"_n,{get_self(), "active"_n}};
复制代码

最后,调用操作包装器的 send 方法,并将 hi 操作的参数作为位置参数传递进去。

wrapper.send(user);
复制代码

完整代码如下:


hello.hpp


#include <eosio/eosio.hpp>
class [[eosio::contract("hello")]] hello : public contract { public: using eosio::contract::contract; [[eosio::action]] void hi( eosio::name user ) ; [[eosio::action]] void sayhi( eosio::name user ); };using hi_action = eosio::action_wrapper<"hi"_n, &hello::hi>;
复制代码


hello.cpp

#include "hello.hpp"
[[eosio::action]] void hello::hi(name user){ require_auth(user); print("Hello, ", name{user});}
[[eosio::action]] void hello::sayhi(name user){ hi_action wrapper{"hello"_n,{get_self(), "active"_n}}; wrapper.send(user); print("action_wrapper");}
复制代码

在该示例中,定义了一个名为 hi_action 的 action_wrapper 对象,它将 hi 操作封装起来。在 sayhi 操作中,创建了一个 hi_action 对象 wrapper,并将其发送给指定的用户。需要注意的是,在调用 send 函数时需要指定调用方的授权信息,可以通过 eosio::permission_level 类来进行指定。在这个例子中,我们使用了合约账户的 active 权限进行授权。


(2)action_wrapper 的 send 和普通 action 的 send 有什么区别


当使用 action_wrapper 时,参数在编译时被检查,这可以帮助避免运行时可能出现的潜在错误。而使用 action 方法直接发送操作时,没有这样的编译时检查。


使用 action 方法不需要在合约中包含目标合约的头文件,但是在使用 action_wrapper 声明操作时,需要在合约中包含目标合约的头文件或声明操作的函数签名。


尽管更加推荐使用 action_wrapper,但如果您确切知道您在做什么,使用 action 方法也不会出现问题。


(3) 使用 action_wrapper 的好处


方便操作调用:使用 action_wrapper 可以方便地调用操作。通过封装操作成一个对象,可以直接调用封装后的对象,而不必每次都手动指定操作的名称、所属合约等参数,从而提高操作调用的方便性。


提高安全性:使用 action_wrapper 可以提高合约的安全性。在调用操作时,需要指定操作的名称、所属合约等参数,如果这些参数不正确,就有可能导致操作执行失败或者执行了不正确的操作。通过使用 action_wrapper,可以避免手动指定这些参数,从而减少出错的可能性。


提高效率:使用 action_wrapper 可以提高代码的执行效率。在调用操作时,如果需要反复指定操作的名称、所属合约等参数,就会增加代码的执行时间。通过使用 action_wrapper,可以避免这种额外的开销,从而提高代码的执行效率。


(三)权限


对于一些特定的 action,不希望他人也可以操作,这时候就需要加入权限的校验。


1、函数说明


(1)require_auth 函数

void require_auth(    capi_name name)
复制代码

验证指定账户是否与调用动作的账户相符,不相符则直接报错失败。


参数说明:


name-需要被验证的账户名


示例:

[[eosio::action]] void hi(eosio::name user ) {   require_auth( user );   print( "Hello, ", name{user} );}
复制代码

该示例实现了一个名为 hi 的 action,要求只有传递进来的参数 user 才能执行该 action。在这个动作函数中,使用 require_auth 函数来检查当前执行该 action 的账户是否有 user 的授权,如果没有授权则会触发一个默认的授权错误信息,阻止该 action 执行。如果授权成功,则会输出一条以 Hello,为开头,user 为结尾的消息。


(2)require_auth2 函数

void eosio::require_auth2(capi_name name, capi_name permission)
复制代码

验证指定账户和权限是否与调用动作的账户和权限相符,不相符则直接报错失败。


参数说明:


name-需要被验证的账户名


permission-需要被验证的权限级别


示例:

#include <capi/eosio/action.h>
[[eosio::action]]void hi( eosio::name user ) { require_auth2(user.value, "active"_n.value); print( "Hello, ", name{user} );}
复制代码

这段代码是一个 EOSIO 智能合约的动作(action)函数,用于向指定用户发送一条问候消息。在这个动作函数中,使用了 require_auth2 函数来确保只有拥有 active 权限的指定用户才能调用该动作函数。


如果在调用这个动作函数的时候,提供的用户权限不符合要求,那么将会抛出异常并导致动作函数执行失败。因为这个函数中没有提供自定义的错误信息,因此当授权失败时,错误信息将会是默认的授权错误信息。


(3)has_auth 函数

bool eosio::has_auth(    name n)
复制代码

验证指定账户是否与调用动作的账户相符


参数说明:


n-需要被验证的账户名


示例:

[[eosio::action]] void hi( eosio::name user ) {   if(has_auth( user )){      print("Hello, ", eosio::name{user} );      }else{      print("This is not ",eosio::name{user} );   }}
复制代码

该示例定义了一个 hi 的 action,接收一个 eosio::name 类型的参数 user。在执行这个 action 的时候,它会检查当前执行者是否拥有传入的 user 账户的权限,如果有,它会输出"Hello, "后面跟上 user 账户的名字,否则输出"This is not "后面跟上 user 账户的名字。这个操作可以确保只有传入的 user 账户可以执行这个 action,而不受执行者所使用的权限(如 owner、active、code)的影响。另外,这个 action 的错误信息是自定义的,可以提供更好的用户体验。


(4)check 函数

void eosio::check(    bool pred,    const char * msg)
复制代码

断言,如果 pred 为假,则使用提供的消息进行反馈。


示例:

#include <capi/eosio/action.h>
void hi( name user ) { check(has_auth(user), "User is not authorized to perform this action."); print( "Hello, ", name{user} );}
复制代码

该示例定义了一个 hi 的 action,接收一个 eosio::name 类型的参数 user,使用 has_auth 函数来检查账户是否被授权执行此动作,如果没有授权,则使用 check 函数抛出一个自定义错误信息"User is not authorized to perform this action."。在授权检查通过后,程序会输出"Hello, " 后接收到的账户名。


注意:


只有被指定为参数的账户可以执行此动作,不论该账户使用哪个权限签署交易。


(5)is_account 函数

bool eosio::is_account(    name n)
复制代码

检查账户是否存在。


参数说明:


n-需要验证的账户名


示例:

#include <capi/eosio/action.h>
void hi( name user ) { check(is_account(user), "The provided name is not an existing account"); print( "Hello, ", name{user} );
复制代码

该示例通过 is_account 函数来检查账户是否存在。如果传入的账户名不存在,那么就会抛出一个异常并在错误信息中包含相应的提示。


2、require_auth、require_auth2 和 has_auth 的区别


has_auth、require_auth 和 require_auth2 都是 EOSIO 中用于权限检查的函数。


has_auth:has_auth 用于验证指定账户是否与调用动作的账户相符。如果指定账户与调用动作的账户相符,则返回 true,否则返回 false,这个函数不会抛出异常。


require_auth:指定账户必须与调用动作的账户相符,否则该函数会抛出异常,不会往下执行。


require_auth2:require_auth2 在 require_auth 基础上,增加了对账户权限的限制。


总之,require_auth 函数要求调用者必须是指定账户,否则合约执行失败;require_auth2 函数要求调用者必须是指定账户的指定权限,否则合约执行失败;has_auth 函数返回调用者是否是指定账户,不会抛出异常。


-END-

用户头像

BSN研习社

关注

还未添加个人签名 2021-11-05 加入

还未添加个人简介

评论

发布
暂无评论
合约编写基础知识介绍基础篇_BSN研习社_InfoQ写作社区