合约编写基础知识介绍基础篇
本文档介绍了合约编写的基础知识,包括合约初始化、action 和权限的相关知识。适用于想要了解智能合约编写基础知识的初学者和开发者,帮助其快速了解和上手 EOS 智能合约的编写。作为智能合约的基础篇,本文仅涉及合约初始化、action 和权限方面的内容。
01
智能合约介绍
区块链作为一种分布式可信计算平台,去中心化是其最本质的特征。每笔交易的记录不可篡改地存储在区块链上。智能合约中定义可以在区块链上执行的动作 action 和交易 transaction 的代码。可以在区块链上执行,并将合约执行状态作为该区块链实例不可变历史的一部分。
因此,开发人员可以依赖该区块链作为可信计算环境,其中智能合约的输入、执行和结果都是独立的,不受外部影响。
02
合约编写基础知识介绍
(一)合约初始化
1、合约的构造函数
在 EOSIO 中,智能合约通过 C++编写,并通过 WASM(WebAssembly)字节码形式部署到区块链网络上。当部署完成后,可以调用合约的 action 执行相应的操作。在合约被部署后,会自动执行其初始化函数,以初始化合约的数据和状态。
智能合约的构造函数是在合约部署时被调用的,用于初始化合约的状态和数据。构造函数是一个特殊的成员函数,它没有返回值类型,且函数名与合约名相同。
以下是一个简单的 EOS 智能合约的构造函数示例:
在上述代码中,构造函数的参数包括:
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 忽略的字段进行处理。例如:
使用以下命令调用 test 动作:
运行结果如下:
该示例中,通过_ds 数据流对象,从输入数据流中反序列化了一个 uint64_t 类型的变量 id,以及一个 person 类型的变量 p。这些数据本来被标记为 ignore,但通过对 ignore 数据进行反序列化,可以将这些数据存储到相应的变量中,并在 ACTION 函数中进一步处理这些数据。
需要注意的是,当我们使用 ignore 关键字时,虚拟机并不会跳过这个字段的解析。相反,虚拟机会将这个字段解析为一个空值,并将其留在数据流中。如果我们需要使用这个字段,我们可以使用_ds 对象来手动解析这个字段。
(二)action
一个关于 action 的打包表示方式,同时还包含了有关授权级别的元数据信息。在 EOSIO 中,当一个 action 被发送时,它将被打包成二进制格式,并在网络上进行传输。这个二进制格式包含了 action 的操作名称、操作数据以及授权级别等信息,以便 EOSIO 网络节点能够正确地解析和执行该 action。元数据信息包括了关于执行该 action 所需的授权级别、签名以及其他验证信息。
1、action 示例
这里类名和合约账户名没有关系,但是为了方便管理,合约文件名、类名以及合约账户最好一致。合约定义了一个名为"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 的操作。
为了定义 hi 动作的 Action Wrapper,可以使用 eosio::action_wrapper 模板。模板的第一个参数为 eosio::name 类型的动作名称,第二个参数为指向动作方法的引用。
要使用 action wrapper,需要包含定义 action wrapper 的头文件。在上面的例子中,可以将以下代码添加到合约的头文件中:
然后,实例化上述定义的 hi_action,将要发送该 action 的合约作为第一个参数指定。在这种情况下,假定合约部署到了 hello 账户,然后通过调用 get_self()方法获取自身账户,最后指定 active 权限(你可以根据需求修改这两个参数)来定义一个包含两个参数的结构体。
最后,调用操作包装器的 send 方法,并将 hi 操作的参数作为位置参数传递进去。
完整代码如下:
hello.hpp
hello.cpp
在该示例中,定义了一个名为 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 函数
验证指定账户是否与调用动作的账户相符,不相符则直接报错失败。
参数说明:
name-需要被验证的账户名
示例:
该示例实现了一个名为 hi 的 action,要求只有传递进来的参数 user 才能执行该 action。在这个动作函数中,使用 require_auth 函数来检查当前执行该 action 的账户是否有 user 的授权,如果没有授权则会触发一个默认的授权错误信息,阻止该 action 执行。如果授权成功,则会输出一条以 Hello,为开头,user 为结尾的消息。
(2)require_auth2 函数
验证指定账户和权限是否与调用动作的账户和权限相符,不相符则直接报错失败。
参数说明:
name-需要被验证的账户名
permission-需要被验证的权限级别
示例:
这段代码是一个 EOSIO 智能合约的动作(action)函数,用于向指定用户发送一条问候消息。在这个动作函数中,使用了 require_auth2 函数来确保只有拥有 active 权限的指定用户才能调用该动作函数。
如果在调用这个动作函数的时候,提供的用户权限不符合要求,那么将会抛出异常并导致动作函数执行失败。因为这个函数中没有提供自定义的错误信息,因此当授权失败时,错误信息将会是默认的授权错误信息。
(3)has_auth 函数
验证指定账户是否与调用动作的账户相符
参数说明:
n-需要被验证的账户名
示例:
该示例定义了一个 hi 的 action,接收一个 eosio::name 类型的参数 user。在执行这个 action 的时候,它会检查当前执行者是否拥有传入的 user 账户的权限,如果有,它会输出"Hello, "后面跟上 user 账户的名字,否则输出"This is not "后面跟上 user 账户的名字。这个操作可以确保只有传入的 user 账户可以执行这个 action,而不受执行者所使用的权限(如 owner、active、code)的影响。另外,这个 action 的错误信息是自定义的,可以提供更好的用户体验。
(4)check 函数
断言,如果 pred 为假,则使用提供的消息进行反馈。
示例:
该示例定义了一个 hi 的 action,接收一个 eosio::name 类型的参数 user,使用 has_auth 函数来检查账户是否被授权执行此动作,如果没有授权,则使用 check 函数抛出一个自定义错误信息"User is not authorized to perform this action."。在授权检查通过后,程序会输出"Hello, " 后接收到的账户名。
注意:
只有被指定为参数的账户可以执行此动作,不论该账户使用哪个权限签署交易。
(5)is_account 函数
检查账户是否存在。
参数说明:
n-需要验证的账户名
示例:
该示例通过 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-
评论