系统合约在链启动阶段就会被部署,是因为系统合约赋予了EOS链资源、命名拍卖、基础数据准备、生产者信息、投票等能力。本篇文章将会从源码角度详细研究system合约。 关键字:EOS,eosio.system,智能合约,name类型,native.hpp,newaccount,bidname,core token init,onblock,更新已入选生产节点
eosio.system 概览
笔者使用的IDE是VScode,首先来看eosio.system的源码结构。如下图所示。
本文分析的源码来自于eosio.contracts。
一、native.hpp
该文件可以分为两个部分,前一个部分是定义了一些结构体,后一个部分是帮助eosio.system合约声明action。总体看上去,这个文件是负责权限的结构。下面先看他都定义了哪些结构体。
权限等级权重
代码语言:javascript复制struct permission_level_weight {
permission_level permission;
uint16_t weight;
EOSLIB_SERIALIZE( permission_level_weight, (permission)(weight) )
};
注意,合约中定义的结构体一般都会在末尾加入EOSLIB_SERIALIZE宏,将结构体的字段属性序列化,这行代码不是必须的,但加上了能够加快解析的速度,从而提升编译效率。
权限等级权重结构体只有两个字段,一个是permission_level类型的对象permission,另一个是16位的无符整型类型的权重。permission_level是定义在eosiolib/action.hpp文件中的一个结构体。它是通过一个账户名以及其权限名构建的,例如{"useraaaaaaaa","active"},这样的一个组合构成了一个权限对象。
公钥权重
代码语言:javascript复制struct key_weight {
eosio::public_key key;
uint16_t weight;
EOSLIB_SERIALIZE( key_weight, (key)(weight) )
};
这个结构体的结构与前面的相似,所以陌生的部分只有eosio::public_key,这是定义在eosiolib/crypto.hpp中的结构体,它代表了EOS中一个公钥对象,该对象可以是K1类型或者R1类型。
secp256k1和secp256r1是两种椭圆曲线数学模型,均属于公钥生成算法。私钥生成公钥的算法也即ECC的字面含义椭圆曲线,是通过该数学模型生成的一种正向快速逆向困难的算法,目前这个算法包括secp256k1和secp256r1 ,secp256k1是比特币首先使用的,而secp256r1据说更有优势,但也有被爆漏洞的历史,由于比特币没有使用secp256r1,因此还有“比特币躲过secp256r1子弹”的说法。目前这两种EOS均支持。
等待权重
代码语言:javascript复制struct wait_weight {
uint32_t wait_sec;
uint16_t weight;
EOSLIB_SERIALIZE( wait_weight, (wait_sec)(weight) )
};
该结构体没有什么特别的,陌生的部分仍旧只有第一个参数wait_sec,但通过字面含义即可理解,就是等待的秒数。
权力
代码语言:javascript复制struct authority {
uint32_t threshold = 0;
std::vector<key_weight> keys;
std::vector<permission_level_weight> accounts;
std::vector<wait_weight> waits;
EOSLIB_SERIALIZE( authority, (threshold)(keys)(accounts)(waits) )
};
这个结构体比较有趣了,它包含四个属性,其中第一个是32位无符整型类型的阈值,初始化位0。剩余三个属性即以上介绍到的三个结构体的集合对象。所以,这也说明了一个账户的权力是由一个阈值、多个密钥、多个权限、多个等待组成的。下面又到了当春乃发生的“authority”和“permission”的区别问题。
authority 指有权利的人。permission 指某项许可。所以某人需要拥有很多别人授权的许可,才能称之为有权利的人。(希望我解释清楚了♫ ♫♬♪♫ )
区块头
代码语言:javascript复制struct block_header {
uint32_t timestamp;
name producer;
uint16_t confirmed = 0;
capi_checksum256 previous;
capi_checksum256 transaction_mroot;
capi_checksum256 action_mroot;
uint32_t schedule_version = 0;
std::optional<eosio::producer_schedule> new_producers;
EOSLIB_SERIALIZE(block_header, (timestamp)(producer)(confirmed)(previous)(transaction_mroot)(action_mroot)
(schedule_version)(new_producers))
};
这个结构体有意思了,好像在很多地方都见过block_header的声明,怎么这里又冒出来一个。有这种感觉很正常,因为之前一直研究的内容都集中在链上,之前看到的block_header是链上的声明,并不是智能合约的。通过全文检索可以查到,block_header结构体由两个文件定义:
- librarieschainincludeeosiochainblock_header.hpp,这个明显是链上的定义,因为路径中包含了chain的字样。
- eosio.systemincludeeosio.systemnative.hpp,另外这一个就是本文介绍的这个结构体了,这是专门服务于智能合约的代码。
所以由此可见,EOS中很多底层的基础结构体都是分两套的,一套给链使用,另一个套给智能合约使用,而他们的定义方式似乎从原来的一模一样发展到今天的些许不同。而目前EOSIO的架构体系中,eosio.contracts作为单独的项目已经从eos分隔出来,并且代码已经发生了不同。因此这种两套体系的概念的困惑会越来越小。
回到native.hpp的区块头结构体。
- 时间戳,uint32_t类型
- 生产者,name类型
- confirmed,已确认数,uint16_t,初始化为0。
- 前一个区块的hash,是capi_checksum256类型的
- 事务Merkle树根,Merkle数的内容请点击以及点击。概况来讲,是为了校验区块内打包的事务的真伪以及完整性的。
- action的merkle树根,校验区块内所有action的真伪以及完整性。
- 计划版本,schedule_version,uint32_t类型,初始化为0。
- 后续计划出块者。producer_schedule类型。
producer_schedule
定义在librarieseosiolibproducer_schedule.hpp。该结构体定义了有效生产者集合的出块顺序、账户名以及签名密钥。
代码语言:javascript复制struct producer_schedule {
// 时间计划的版本号,按顺序递增。
uint32_t version;
// 此计划的生产者列表,包括其签名密钥
std::vector<producer_key> producers;
};
陌生的部分是producer_key,该结构体定义在librarieseosiolibprivileged.hpp,是用来映射生产者及其签名密钥,用于生产者计划。
代码语言:javascript复制struct producer_key {
name producer_name;
// 此生产者使用的区块签名密钥
public_key block_signing_key;
// 重载运算符小于号,producer_key的两个对象进行小于号比较时,返回的是其name类型的生产者账户的比较。
friend constexpr bool operator < ( const producer_key& a, const producer_key& b ) {
return a.producer_name < b.producer_name;
}
EOSLIB_SERIALIZE( producer_key, (producer_name)(block_signing_key) )
};
一个问题:name类型是EOS中账户类型,那么它的对象是如何比较的?请转到第二大节。
abihash
native.hpp除了声明以上必要结构体以外,还协助eosio.system合约定义了一个状态表abihash。该状态表只有两个字段,一个是账户名,另一个是hash,该hash是当前账户的abi。在EOS中,一个账户除了通过命令
cleos get account xxxxxxxxxxxx
获得自身属性之外,还可以通过分别通过命令get code和get abi获得该账户部署的合约的abi hash以及code hash,这两个hash是用来校验其部署的智能合约的内容是否发生改变。其中abi hash就是存储在native.hpp定义的状态表中。下面是源码内容:
代码语言:javascript复制struct [[eosio::table("abihash"), eosio::contract("eosio.system")]] abi_hash {
name owner;
capi_checksum256 hash;
uint64_t primary_key()const { return owner.value; } // 以账户的值作为该表的主键。
EOSLIB_SERIALIZE( abi_hash, (owner)(hash) )
};
注意:通过[eosio::table("abihash"), eosio::contract("eosio.system")]的方式可以为合约定义一个状态表,而不再需要原始的typedef multi_index的方式了。这种方式适用于只有主键的情况,如果有多级索引,仍旧需要multi_index。
native合约类
先展示位于native.hpp文件中的native合约类以及位于eosio.system.hpp文件中的system_contract的区别。
class [eosio::contract("eosio.system")] native : public eosio::contract class [eosio::contract("eosio.system")] system_contract : public native
eosio::contract是EOS中所有智能合约的基类,native合约类继承于它,然后system_contract合约类继承于native,而他们二者共同组成了eosio.system智能合约。这种方式让原本单一的智能合约架构变得丰富。作为基类的native,它都声明了eosio.system的哪些属性呢?下面仔细观瞧。
[eosio::action] newaccount
我们常用的system newaccount功能就是在native中声明的。该action在创建新帐户后调用,此代码强制实施新帐户的资源限制规则以及新帐户命名约定。规则包含两个:
- 帐户不能包含'.' 强制所有帐户的符号长度为12个字符而没有“.” 直到实施未来的帐户拍卖流程。
- 新帐户必须包含最少数量的token(如系统参数中所设置),因此,此方法将为新用户执行内联buyram购买内存,其金额等于当前新帐户的创建费用。
[[eosio::action]]
void newaccount( name creator,
name name,
ignore<authority> owner,
ignore<authority> active);
陌生的部分是ignore,该结构位于librarieseosiolibignore.hpp。
ignore
告诉数据流忽略此类型,但允许abi生成器添加正确的类型。当前非忽略类型不能在方法定义中成功忽略类型,即允许
void foo(float,ignore
但不允许
void foo(float,ignore
因为int已经被声明为忽略类型,所以后面不能再作为非忽略类型出现了。ignore结构体源码如下:
代码语言:javascript复制template <typename T>
struct [[eosio::ignore]] ignore {};
其他[eosio::action]
动作 | 返回值 | 参数 | 解释 |
---|---|---|---|
updateauth | void | ignore | 更新账户的某项权限内容 |
deleteauth | void | ignore | 删除账户的某项权限内容 |
linkauth | void | ignore | 连接其他账户 |
unlinkauth | void | ignore | 解除某账户的连接 |
canceldelay | void | ignore<permission_level> canceling_authignore<capi_checksum256> trx_id | 取消某个延迟交易 |
onerror | void | ignore<uint128_t> sender_idignore<std::vector | 处理错误 |
setabi | void | name accountconst std::vector | 设置账户的abi内容 |
setcode | void | name accountuint8_t vmtypeuint8_t vmversionconst std::vector | 设置账户的code内容 |
二、name.hpp
name结构体定义在librarieseosiolibname.hpp,源码注释如下:
代码语言:javascript复制struct name {
public:
enum class raw : uint64_t {};
// 构建一个新的name对象,初始化默认为0
constexpr name() : value(0) {}
// 使用给定的unit64_t类型的值构建一个新的name对象。
constexpr explicit name( uint64_t v )
:value(v)
{}
// 使用给定的一个范围的枚举类型,构建一个新的name对象。
constexpr explicit name( name::raw r )
:value(static_cast<uint64_t>(r))
{}
// 使用给定的字符串构建一个新的name对象。
constexpr explicit name( std::string_view str )
:value(0)
{
if( str.size() > 13 ) { // 字符串最长不能超过12
eosio::check( false, "string is too long to be a valid name" );
}
if( str.empty() ) {
return;
}
// 将字符串转为uint64_t
auto n = std::min( (uint32_t)str.size(), (uint32_t)12u );
for( decltype(n) i = 0; i < n; i ) {
value <<= 5;
value |= char_to_value( str[i] );
}
value <<= ( 4 5*(12 - n) );
if( str.size() == 13 ) {
uint64_t v = char_to_value( str[12] );
if( v > 0x0Full ) {
eosio::check(false, "thirteenth character in name cannot be a letter that comes after j");
}
value |= v;
}
}
// 将一个Base32符号的char转换为它对应的值。
static constexpr uint8_t char_to_value( char c ) {
if( c == '.')
return 0;
else if( c >= '1' && c <= '5' )
return (c - '1') 1;
else if( c >= 'a' && c <= 'z' )
return (c - 'a') 6;
else // 字符中出现了不允许的内容。
eosio::check( false, "character is not in allowed character set for names" );
return 0; // 流程控制将不会到达这里,这一行是为了防止warn信息。
}
// 返回一个name对象的长度,运算方法。
constexpr uint8_t length()const {
constexpr uint64_t mask = 0xF800000000000000ull;
if( value == 0 )
return 0;
uint8_t l = 0;
uint8_t i = 0;
for( auto v = value; i < 13; i, v <<= 5 ) {
if( (v & mask) > 0 ) {
l = i;
}
}
return l 1;
}
// 返回一个name对象的后缀,完整的运算方法。
constexpr name suffix()const {
uint32_t remaining_bits_after_last_actual_dot = 0;
uint32_t tmp = 0;
for( int32_t remaining_bits = 59; remaining_bits >= 4; remaining_bits -= 5 ) { // remaining_bits必须有符号整数
// 从左到右依次遍历name中的字符,共12次
auto c = (value >> remaining_bits) & 0x1Full;
if( !c ) { // 如果当前字符是点
tmp = static_cast<uint32_t>(remaining_bits);
} else { // 如果当前字符不是点
remaining_bits_after_last_actual_dot = tmp;
}
}
uint64_t thirteenth_character = value & 0x0Full;
if( thirteenth_character ) { // 如果第13个字符不是点
remaining_bits_after_last_actual_dot = tmp;
}
if( remaining_bits_after_last_actual_dot == 0 ) // 除了潜在的前导点之外,name中没有实际的点
return name{value};
// 此时,remaining_bits_after_last_actual_dot必须在4到59的范围内(并且限制为5的增量)。
// 除了4个最低有效位(对应于第13个字符)之外,对应于最后一个实际点之后的字符的剩余位的掩码。
uint64_t mask = (1ull << remaining_bits_after_last_actual_dot) - 16;
uint32_t shift = 64 - remaining_bits_after_last_actual_dot;
return name{ ((value & mask) << shift) (thirteenth_character << (shift-1)) };
}
// 将name类型转为raw枚举类型:基于name对象的值,返回一个raw枚举类型的实例。
constexpr operator raw()const { return raw(value); }
// 显式转换一个name的uint64_t值为bool,如果name的值不为0,返回true。
constexpr explicit operator bool()const { return value != 0; }
// 根据给定的char缓冲区,以字符串的类型写入name对象。参数begin:char缓冲区的开头,参数end:刚好超过char缓冲区的位置,作为结尾。
char* write_as_string( char* begin, char* end )const {
static const char* charmap = ".12345abcdefghijklmnopqrstuvwxyz";
constexpr uint64_t mask = 0xF800000000000000ull;
if( (begin 13) < begin || (begin 13) > end ) return begin;
auto v = value;
for( auto i = 0; i < 13; i, v <<= 5 ) {
if( v == 0 ) return begin;
auto indx = (v & mask) >> (i == 12 ? 60 : 59);
*begin = charmap[indx];
begin;
}
return begin;
}
// 将name对象转为一个字符串返回。
std::string to_string()const {
char buffer[13];
auto end = write_as_string( buffer, buffer sizeof(buffer) );
return {buffer, end};
}
// 重载运算符等于号,给定两个name对象,如果他们的value相等,则返回true,说明对象也相等。
friend constexpr bool operator == ( const name& a, const name& b ) {
return a.value == b.value;
}
// 重载运算符符不等于,如果给定的两个name对象的value不相等,则返回true,说明对象也不相等。
friend constexpr bool operator != ( const name& a, const name& b ) {
return a.value != b.value;
}
// 重载运算符小于号,原理同上。
friend constexpr bool operator < ( const name& a, const name& b ) {
return a.value < b.value;
}
uint64_t value = 0; // 其实name对象只有一个有效属性,就是value,以上都是name对象的构造方式、限制条件、各种转型以及运算符重载。
EOSLIB_SERIALIZE( name, (value) )
};
三、exchange_state.hpp
该文件位于eosio.systemincludeeosio.systemexchange_state.hpp。也是system合约的依赖之一。该文件处理资产方面的工作,主要部分是exchange_state结构体,该结构体使用Bancor算法在两种不同资产类型中间创造一个50对50的中继,bancor交易所的状态完全包含在这个结构体中,此API没有任何额外的副作用。
代码语言:javascript复制namespace eosiosystem {
using eosio::asset;
using eosio::symbol;
typedef double real_type;
// 使用Bancor算法在两种不同资产类型中间创造一个50对50的中继。bancor交易所的状态完全包含在这个结构体中。使用此API没有任何副作用。
struct [[eosio::table, eosio::contract("eosio.system")]] exchange_state {
asset supply; // 资产供应
struct connector { // 连接器
asset balance; // 资产余额
double weight = .5; // 权重
EOSLIB_SERIALIZE( connector, (balance)(weight) )
};
connector base; // 基本连接器
connector quote; // 引用连接器
uint64_t primary_key()const { return supply.symbol.raw(); } // 该table主键
asset convert_to_exchange( connector& c, asset in ); // 通过连接器c将输入资产in转换为发行资产issued。
asset convert_from_exchange( connector& c, asset in ); // 通过连接器c将输入资产in转换为输出资产out
asset convert( asset from, const symbol& to ); // 核心功能:将一种资产转为另一种符号的等价资产。例如将10 SYS的资产转为EOS是20 EOS,币币交易。
EOSLIB_SERIALIZE( exchange_state, (supply)(base)(quote) )
};
// 内存市场状态表
typedef eosio::multi_index< "rammarket"_n, exchange_state > rammarket;
}
convert函数是exchange最重要的功能,它实现了完全按照boncor市场机制交换token。具体实现源码的机制如下:
代码语言:javascript复制asset exchange_state::convert(asset from, const symbol &to)
{
auto sell_symbol = from.symbol; // 原来的符号,作为卖出币
auto ex_symbol = supply.symbol; // 中转币的符号
auto base_symbol = base.balance.symbol; // base连接器资产的符号
auto quote_symbol = quote.balance.symbol; // quote连接器资产的符号
if (sell_symbol != ex_symbol)
{ // 如果卖出币不是中转币
if (sell_symbol == base_symbol)
{ // 如果卖出币等于base连接器资产
from = convert_to_exchange(base, from); // 通过base连接器转换卖出币
}
else if (sell_symbol == quote_symbol)
{ // 如果卖出币等于quote连接器资产
from = convert_to_exchange(quote, from); // 通过quote连接器转换卖出币
}
else
{ // 其他卖出币无任何连接器的情况视为无效币币兑换行为。
eosio_assert(false, "invalid sell");
}
}
else
{ // 如果卖出币是中转币
if (to == base_symbol) // 如果买入币等于base连接器资产
{
from = convert_from_exchange(base, from); // 通过base连接器转换卖出币
}
else if (to == quote_symbol) // 如果买入币等于quote连接器资产
{
from = convert_from_exchange(quote, from); // 通过quote连接器转换卖出币
}
else
{ // 其他卖出币无任何连接器的情况视为无效币币兑换行为。
eosio_assert(false, "invalid conversion");
}
}
if (to != from.symbol) // 如果经过一轮转换以后,from和to资产仍旧没有统一符号,则再次调一遍转换。
return convert(from, to);
return from; // 最后成功得到转换为等价的to币
}
这部分可以参照之前的一篇文章【EOS标准货币体系与源码实现分析】。
四、asset.hpp
asset.hpp是合约中关于资产方面的数据结构的定义。该文件包含asset结构体以及extended_asset结构体。下面首先分析asset结构体的源码部分。
代码语言:javascript复制struct asset
{
int64_t amount; // 资产数量
symbol_type symbol; // 资产符号名称,详见以下symbol_type源码分析。
static constexpr int64_t max_amount = (1LL << 62) - 1; // 资产数量最大值,取决于int64_t类型的取值范围。
// 通过给定的符号名称以及资产数量构建一个新的资产对象。
explicit asset(int64_t a = 0, symbol_type s = CORE_SYMBOL)
: amount(a), symbol{s}
{
eosio_assert(is_amount_within_range(), "magnitude of asset amount must be less than 2^62");
eosio_assert(symbol.is_valid(), "invalid symbol name");
}
// 检查资产数量是否在范围以内,是否超过了最大限额。
bool is_amount_within_range() const { return -max_amount <= amount && amount <= max_amount; }
// 检查资产对象是否有效,有效资产的数量应该小于等于最大限额同时它的符号名称也是有效的。
bool is_valid() const { return is_amount_within_range() && symbol.is_valid(); }
// 设置资产的数量
void set_amount(int64_t a)
{
amount = a;
eosio_assert(is_amount_within_range(), "magnitude of asset amount must be less than 2^62");
}
/**
* 以下为资产对象的运算符重载,包含
* 取负,-=, =, ,-,*=,*(数乘以资产,资产乘以数),/(资产除以数,资产除以资产),/=,==,!=,<,<=,>,>=
* 源码部分省略。
*/
// 打印资产
void print() const
{
int64_t p = (int64_t)symbol.precision();
int64_t p10 = 1;
while (p > 0)
{
p10 *= 10;
--p;
}
p = (int64_t)symbol.precision();
char fraction[p 1];
fraction[p] = '