【精】EOS智能合约:system系统合约源码分析
系统合约在链启动阶段就会被部署,是因为系统合约赋予了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。总体看上去,这个文件是负责权限的结构。下面先看他都定义了哪些结构体。
权限等级权重
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"},这样的一个组合构成了一个权限对象。
公钥权重
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均支持。
等待权重
struct wait_weight {
uint32_t wait_sec;
uint16_t weight;
EOSLIB_SERIALIZE( wait_weight, (wait_sec)(weight) )
};
该结构体没有什么特别的,陌生的部分仍旧只有第一个参数wait_sec,但通过字面含义即可理解,就是等待的秒数。
权力
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 指某项许可。所以某人需要拥有很多别人授权的许可,才能称之为有权利的人。(希望我解释清楚了♫ ♫♬♪♫ )
区块头
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结构体由两个文件定义:
- libraries\chain\include\eosio\chain\block_header.hpp,这个明显是链上的定义,因为路径中包含了chain的字样。
- eosio.system\include\eosio.system\native.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
定义在libraries\eosiolib\producer_schedule.hpp。该结构体定义了有效生产者集合的出块顺序、账户名以及签名密钥。
struct producer_schedule {
// 时间计划的版本号,按顺序递增。
uint32_t version;
// 此计划的生产者列表,包括其签名密钥
std::vector<producer_key> producers;
};
陌生的部分是producer_key,该结构体定义在libraries\eosiolib\privileged.hpp,是用来映射生产者及其签名密钥,用于生产者计划。
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定义的状态表中。下面是源码内容:
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,该结构位于libraries\eosiolib\ignore.hpp。
ignore
告诉数据流忽略此类型,但允许abi生成器添加正确的类型。当前非忽略类型不能在方法定义中成功忽略类型,即允许
void foo(float,ignore
)
但不允许
void foo(float,ignore
,int)。
因为int已经被声明为忽略类型,所以后面不能再作为非忽略类型出现了。ignore结构体源码如下:
template <typename T>
struct [[eosio::ignore]] ignore {};
其他[[eosio::action]]
动作 | 返回值 | 参数 | 解释 |
---|---|---|---|
updateauth | void | ignore ignore ignore ignore |
更新账户的某项权限内容 |
deleteauth | void | ignore ignore |
删除账户的某项权限内容 |
linkauth | void | ignore ignore ignore ignore |
连接其他账户 |
unlinkauth | void | ignore ignore ignore |
解除某账户的连接 |
canceldelay | void | ignore<permission_level> canceling_auth ignore<capi_checksum256> trx_id |
取消某个延迟交易 |
onerror | void | ignore<uint128_t> sender_id ignore<std::vector |
处理错误 |
setabi | void | name account const std::vector |
设置账户的abi内容 |
setcode | void | name account uint8_t vmtype uint8_t vmversion const std::vector |
设置账户的code内容 |
二、name.hpp
name结构体定义在libraries\eosiolib\name.hpp,源码注释如下:
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.system\include\eosio.system\exchange_state.hpp。也是system合约的依赖之一。该文件处理资产方面的工作,主要部分是exchange_state结构体,该结构体使用Bancor算法在两种不同资产类型中间创造一个50对50的中继,bancor交易所的状态完全包含在这个结构体中,此API没有任何额外的副作用。
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。具体实现源码的机制如下:
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结构体的源码部分。
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] = '\0';
auto change = amount % p10;
for (int64_t i = p - 1; i >= 0; --i)
{
fraction[i] = (change % 10) + '0';
change /= 10;
}
printi(amount / p10);
prints(".");
prints_l(fraction, uint32_t(p));
prints(" ");
symbol.print(false);
}
EOSLIB_SERIALIZE(asset, (amount)(symbol))
};
symbol_type
直接通过源码注释分析,如下:
/**
* @brief 存储关于符号相关的信息的结构体
*/
struct symbol_type
{
symbol_name value; // uint64_t类型的符号名称
symbol_type() {}
symbol_type(symbol_name s) : value(s) {} // 符号的类型
bool is_valid() const { return is_valid_symbol(value); } // 符号是否有效
uint64_t precision() const { return value & 0xff; } // 符号类型中包含对资产精度的要求,即小数点后几位数。
uint64_t name() const { return value >> 8; } // 返回代表符号名称的uint64_t的值
uint32_t name_length() const { return symbol_name_length(value); } // 返回符号名称的长度
operator symbol_name() const { return value; } //重载符号对象的()运算符,返回符号名称的uint64_t值
void print(bool show_precision = true) const
{ // 打印符号信息,包含uint64_t转字符的算法。
if (show_precision)
{
::eosio::print(precision()); // 打印符号的精度
prints(",");
}
//uint64_t转字符
auto sym = value;
sym >>= 8;
for (int i = 0; i < 7; ++i)
{
char c = (char)(sym & 0xff);
if (!c)
return;
prints_l(&c, 1);
sym >>= 8;
}
}
EOSLIB_SERIALIZE(symbol_type, (value))
};
extended_asset
extended_asset,顾名思义是asset资产的延展类型,主要是在asset的基础上增加了资产拥有者的相关字段。内容不多仍旧通过源码分析一下:
struct extended_asset : public asset
{
account_name contract; // 资产拥有者
// 获得资产的扩展符号
extended_symbol get_extended_symbol() const { return extended_symbol(symbol, contract); }
// 默认构造器,构造一个扩展资产对象
extended_asset() = default;
// 通过给定的数量和扩展符号构造一个扩展资产对象。
extended_asset(int64_t v, extended_symbol s) : asset(v, s), contract(s.contract) {}
// 通过给定的资产以及拥有者账户名构造一个扩展资产。
extended_asset(asset a, account_name c) : asset(a), contract(c) {}
// 打印相关信息
void print() const
{
asset::print();
prints("@");
printn(contract);
}
/**
* 运算符重载,包括符号取反,-,+
* 主要是对资产拥有者的操作,其他的操作于asset一致。
*/
EOSLIB_SERIALIZE(extended_asset, (amount)(symbol)(contract))
};
五、eosio.system.hpp
下面查看system合约的主要头文件eosio.system.hpp,该文件包含了合约的属性,定义了大量结构体用于支撑system合约的业务功能,下面重点浏览system合约的成员属性。
成员 | 权属 | 名称 | 解释 |
---|---|---|---|
_voters | 私有属性 | voters_table实例 | 投票状态表,表名为voters, 结构为voter_info结构体。 |
_producers | 私有属性 | producers_table实例 | 生产者信息状态表,表名为produceers, 包含一个自定义索引prototalvote, 结构为producer_info结构体。 |
_global | 私有属性 | global_state_singleton实例 | 全局状态单例状态表,表名为global, 结构为eosio_global_state结构体, 继承自eosio::block-chain_parameters 与genesis.json内容高度匹配。 |
_gstate | 私有属性 | eosio_global_state结构体实例 | 就是上面这个状态表的数据结构实例。 |
_rammarket | 私有属性 | rammarket实例 | 内存市场状态表,定义在exchange_state.hpp头文件中。 表名为rammarket,结构为使用了bancor算法的 exchange_state结构体。 |
下面继续介绍system合约的成员函数内容,
成员 | 权属 | 解释 |
---|---|---|
update_elected_producers | 私有函数 | 只有一个参数是时间戳,按照时间戳更新已入选的生产节点名单。 |
update_votes | 私有函数 | 更新投票信息。包含参数有投票者、代理、生产者投票内容, 以及支持或反对的标识。 |
changebw | 私有函数 | 更改某账户的资源量,包含出资者、接收者、cpu资源量、net资源量, 以及是否以转账的形式更改。 即抵押资源量的token也属于接收者了。 |
get_default_parameters | 私有函数 | 获得默认参数 |
get_core_symbol | 私有函数 | 通过内存账户的token符号获得链上主币符号。 |
current_time_point | 私有函数 | 获得当前时间点time_point类型。 |
current_block_time | 私有函数 | 获得当前区块时间block_timestamp类型。 |
update_producer_votepay_share | 私有函数 | 更新生产者投票支付份额 |
update_total_votepay_share | 私有函数 | 更新总投票支付份额 |
propagate_weight_change | 私有函数 | 代理权重更改,传入投票者账户。 |
下面分析system合约的公共成员函数,
成员 | 权属 | 解释 |
---|---|---|
onblock | 公共函数 | 在producer_pay.cpp中实现,是由eosio创世账户发起, 用于更新生产者生产区块信息以及上链的账号名称拍卖信息。 传入时间戳和生产者 |
delegatebw | 公共函数 | 与私有函数changebw的参数完全相同,用于抵押资源的主要方法。 |
undelegatebw | 公共函数 | 与抵押函数相反,是用来解除抵押的方法。 |
buyram | 公共函数 | 为账户购买内存资源。有出资方 |
buyrambytes | 公共函数 | 上面是以token的方式购买内存资源,这一个是以内存量字节的方式购买。 |
sellram | 公共函数 | 卖出内存资源。 |
refund | 公共函数 | 在抵押动作未完成时,发起退款 |
regproducer | 公共函数 | 注册成为备用生产者。 |
unregprod | 公共函数 | 解除备用生产者的注册 |
setram | 公共函数 | 设置最大内存量,为链增加内存容量,注意只能增加不能降低。 |
voteproducer | 公共函数 | 为生产者投票,校验投票者签名,然后调用了私有函数update_votes函数。 |
regproxy | 公共函数 | 注册成为代理 |
setparams | 公共函数 | 设置链参数eosio::blockchain_parameters |
claimrewards | 公共函数 | 生产者认领出块奖励 |
setpriv | 公共函数 | 设置账户是否为特权账户 |
rmvproducer | 公共函数 | 移除失效生产者并标记 |
bidname | 公共函数 | 拍卖账户名称 |
六、cpp实现精选
更新已入选生产节点
该功能是通过system合约的私有函数update_elected_producers实现。在voting.cpp中被定义实现。
/**
* @brief 更新已入选生产节点
*
* @param block_time 区块时间
*/
void system_contract::update_elected_producers( block_timestamp block_time ) {
_gstate.last_producer_schedule_update = block_time; // 将参数区块时间赋值给全局状态变量:最后计划出块更新时间
auto idx = _producers.get_index<N(prototalvote)>(); // 获得producers表的索引prototalvote,该索引能够给producers表按照投票总数排序,详细分析见下一个部分。
std::vector< std::pair<eosio::producer_key,uint16_t> > top_producers; // 声明有效出块节点集合。
top_producers.reserve(21); // 定义有效出块节点集合的数量为21。
// 从prototalvote索引结果集中筛选出21个插入top_producers集合。这些生产者要满足是active的同时总票数大于0(最基本的校验)。
for ( auto it = idx.cbegin(); it != idx.cend() && top_producers.size() < 21 && 0 < it->total_votes && it->active(); ++it ) {
top_producers.emplace_back( std::pair<eosio::producer_key,uint16_t>({{it->owner, it->producer_key}, it->location}) );
}
// 如果有效出块集合的数量小于全局标志位:最后计划生产者数量(见下方),则中断返回。(适用于总数不足21个节点的情况)
if ( top_producers.size() < _gstate.last_producer_schedule_size ) {
return;
}
// 根据名称为top_producers排序。
std::sort( top_producers.begin(), top_producers.end() );
// 新建producers集合,copy一份top_producers
std::vector<eosio::producer_key> producers; // 声明生产者集合
producers.reserve(top_producers.size()); // 将producers设置为与top_producers一样大小。
for( const auto& item : top_producers ) // 遍历copy元素
producers.push_back(item.first);
bytes packed_schedule = pack(producers); // 将超级节点集合打包成字节
if( set_proposed_producers( packed_schedule.data(), packed_schedule.size() ) >= 0 ) { // 设置计划生产者,更新最后计划生产者数量
_gstate.last_producer_schedule_size = static_cast<decltype(_gstate.last_producer_schedule_size)>( top_producers.size() );
}
}
二级索引排序
prototalvote索引的定义在multi_index状态表producers_table。
typedef eosio::multi_index< N(producers), producer_info,
indexed_by<N(prototalvote), const_mem_fun<producer_info, double, &producer_info::by_votes> >
> producers_table;
producers_table状态表的声明中,定义了表名为producers,数据结构为producer_info,然后定义了二级索引,名称为prototalvote,该索引的提取器是操作数据结构produer_info对象,提取类型为double,提取规则是produer_info的by_votes方法。
N是一个宏,可以把base32编码后的字符串转换为uint64。
最新版本的eosio.contracts已经改为"useraaaaaaaa"_n的方式代替了N("useraaaaaaaa")。
/**
* @brief 用于从X的base32编码字符串解释生成编译的uint64 t
*
* @param X - 代表名称的字符串
* @return constexpr uint64_t - 64位无符整型值,可代表一个名称
*/
#define N(X) ::eosio::string_to_name(#X)
下面研究multi_index的二级索引indexed_by的定义源码。
template<uint64_t IndexName, typename Extractor>
struct indexed_by {
enum constants { index_name = IndexName };
typedef Extractor secondary_extractor_type;
};
结构体indexed_by是用来为multi_index状态表创建索引实例的。EOS中支持指定最多16个二级索引。接收两个参数,一个是索引名称,另一个是提取器。提取器采用了const_mem_fun模板,该模板有效定义了提取器的数据范围,数据类型以及提取规则(方法)。回到producers_table表的数据结构produer_info结构体中。
struct producer_info
{
account_name owner; // producer账户名
double total_votes = 0; // 当前producer的总投票数
eosio::public_key producer_key; // 当前producer的公钥
bool is_active = true; // 当前producer是否有效
std::string url; // 当前producer的介绍url,可以是官网
uint32_t unpaid_blocks = 0; // 未领奖励的区块数量
uint64_t last_claim_time = 0; // 上一次认领奖励的时间
uint16_t location = 0; // 当前producer的位置
uint64_t primary_key() const { return owner; } // producer_info结构体的主键,将被状态表producer_table作为第一索引。
double by_votes() const { return is_active ? -total_votes : total_votes; } // 按投票(排序),注意排序并不是在此实现,此方法只是为了区分,将失效producer的总票数置为其相反数
bool active() const { return is_active; } // 判断是否有效
void deactivate()
{
producer_key = public_key();
is_active = false;
} // 将当前生产者设置为失效的动作。
// 注意:明确序列化宏不是必要的,用在此处是为了提高编译效率
EOSLIB_SERIALIZE(producer_info, (owner)(total_votes)(producer_key)(is_active)(url)(unpaid_blocks)(last_claim_time)(location))
};
系统合约管理出块
eosio.system的onblock可以管理生产者的出块动作,参与每0.5秒的出块工作。下面通过注释分析该动作的源码。
/**
* @brief system合约的出块动作
*
* @param timestamp 时间戳
* @param producer 生产者
*/
void system_contract::onblock( block_timestamp timestamp, account_name producer ) {
using namespace eosio;
// 该动作是由eosio创世账户执行,要先校验是否有该账户权限。
require_auth(N(eosio));
// 当总激活抵押数小于最低激活抵押额时,停止动作。
if( _gstate.total_activated_stake < min_activated_stake )
return;
// 当预投票开始时,更新时间为当前时间。
if( _gstate.last_pervote_bucket_fill == 0 )
_gstate.last_pervote_bucket_fill = current_time();
// 在生产者集合中查询传入的生产者账号
auto prod = _producers.find(producer);
if ( prod != _producers.end() ) { // 成功查到结果
_gstate.total_unpaid_blocks++; // 全局未结算区块数加一
_producers.modify( prod, 0, [&](auto& p ) { // 当前生产者未结算数加一
p.unpaid_blocks++;
});
}
// 注意:每分钟只更新区块生产者一次,0.5秒更新一次区块时间。
if( timestamp.slot - gstate.last_producer_schedule_update.slot > 120 ){update_elected_producers( timestamp ); // 更新已入选生产节点,见上小节
// 账户名称拍卖工作的更新操作,注意:每天只能交易一次。
if( (timestamp.slot - _gstate.last_name_close.slot) > blocks_per_day ) {
name_bid_table bids(_self,_self); // 拍卖账户名的状态表实例。
auto idx = bids.get_index<N(highbid)>(); // 得到二级索引的结果集,按照出价高低排序。
auto highest = idx.begin();
/**
* @brief 判断是否符合拍卖结束条件。
* 条件包括:
* 1,状态表不为空
* 2,出价大于0
* 3,出价的时间在一秒钟之内
* 4,抵押激活时间大于0
* 5,当前时间至少超过抵押激活14天的时间
*/
if( highest != idx.end() &&
highest->high_bid > 0 &&
highest->last_bid_time < (current_time() - useconds_per_day) && _gstate.thresh_activated_stake_time > 0 &&
(current_time() - _gstate.thresh_activated_stake_time) > 14 * useconds_per_day ) {
_gstate.last_name_close = timestamp;// 记录成功交易时间
idx.modify( highest, 0, [&]( auto& b ){
b.high_bid = -b.high_bid; // 该笔拍卖报价已兑现,则置相反数,可作为记录的同时不参与其他有效报价。
});
}
}
}
}
通过源码分析,onblock动作不仅管理了生产者的结算动作,还管理了链上账户名拍卖工作。与上面producers_table中的二级索引prototalvote的功能相同,name_bid_table状态表的二级索引highbid也是用来对结果集进行排序的,具体声明如下:
typedef eosio::multi_index< N(namebids), name_bid,
indexed_by<N(highbid), const_mem_fun<name_bid, uint64_t, &name_bid::by_high_bid > > > name_bid_table;
二级索引highbid同样使用了const_mem_fun模板定义了提取器内容,其中提取规则也就是排序依赖为name_bid结构体的by_high_bid函数。
struct name_bid {
account_name newname;
account_name high_bidder;
int64_t high_bid = 0; // 若该项值为负数,则证明已获得拍卖名字,等待认领。
uint64_t last_bid_time = 0;
auto primary_key()const { return newname; } // 主键
uint64_t by_high_bid()const { return static_cast<uint64_t>(-high_bid); } // 返回报价字段的值
};
初始化主币
EOSIO在将合约迁移到一个新创建的repo 以后,为系统合约加入了主币初始化init的操作。下面仍旧通过源码分析该操作的内容。
/**
* @brief 初始化主币
*
* @param version 版本号
* @param core 初始化的主币对象
*/
void system_contract::init( unsigned_int version, symbol core ) {
require_auth( _self ); // 判断是否拥有合约主人身份。
eosio_assert( version.value == 0, "unsupported version for init action" ); // 对于初始化动作,版本号只能为0
auto itr = _rammarket.find(ramcore_symbol.raw()); // 在内存表中查找主币对象,如果已查到说明初始化操作已完成,退出当前进程
eosio_assert( itr == _rammarket.end(), "system contract has already been initialized" );
// 此处调用了token的get_supply函数,获得主币供应量
auto system_token_supply = eosio::token::get_supply(token_account, core.code() );
// 校验token的符号以及小数点精确位数是否一致。
eosio_assert( system_token_supply.symbol == core, "specified core symbol does not exist (precision mismatch)" );
// 校验主币的供应量是否大于0
eosio_assert( system_token_supply.amount > 0, "system token supply must be greater than 0" );
_rammarket.emplace( _self, [&]( auto& m ) { // 内存市场状态表新增数据
m.supply.amount = 100000000000000ll;
m.supply.symbol = ramcore_symbol;
m.base.balance.amount = int64_t(_gstate.free_ram());
m.base.balance.symbol = ram_symbol;
m.quote.balance.amount = system_token_supply.amount / 1000;
m.quote.balance.symbol = core;
});
}
初始化主币的操作在节点启动时会被调用到,这个操作一般被执行成功一次就不会再被调用。初始化主币的命令时:
$ cleos push action eosio init '["0", "4,SYS"]' -p eosio@active
传入了两个参数,第一个参数时0,上面介绍了是版本的含义。第二个参数的值为“4,SYS”,是token符号对象。SYS定义了主币的符号名称,4是主币的小数点精度,这个值可以是0到18。前面在token转账的过程中,校验了token的符号对象,校验工作就包含了对符号名称以及小数点精度位数的校验。
非常规账户竞拍
前面介绍system合约的onblock动作以及init动作都涉及到了账户竞拍的逻辑。在EOS中,常规账户的名称要求为必须12个字符同时中间不能包含点,而非常规账户名则可以少于12个字符并且可包含点,加入后缀。这种非常规账户的名称显然是稀有且具备个性的,因此EOS加入了这一部分的竞拍市场机制。该动作是由system系统合约的bidname完成。下面仍旧分析其源码实现。
/**
* @brief 账户名拍卖
*
* @param bidder 竞拍者
* @param newname 标的账户名
* @param bid 报价
*/
void system_contract::bidname( name bidder, name newname, asset bid ) {
require_auth( bidder ); // 校验竞拍者是否本人操作
// 校验标的账户名是否符合高级后缀。
eosio_assert( newname.suffix() == newname, "you can only bid on top-level suffix" );
eosio_assert( (bool)newname, "the empty name is not a valid account name to bid on" );//校验标的是否为空
eosio_assert( (newname.value & 0xFull) == 0, "13 character names are not valid account names to bid on" );//13个字符长度的标的不允许竞拍
// 常规账户长度为12位且不包含点,只有非常规账户才可以参与竞拍,即小于12个字符的,或者包含点的。
eosio_assert( (newname.value & 0x1F0ull) == 0, "accounts with 12 character names and no dots can be created without bidding required" );
eosio_assert( !is_account( newname ), "account already exists" );// 校验标的账户是否已存在。
eosio_assert( bid.symbol == core_symbol(), "asset must be system token" );// 校验报价资产必须是主币
eosio_assert( bid.amount > 0, "insufficient bid" );// 校验报价必须正数
// 经过以上重重校验,可以进行实际拍卖环节。首先发起转账,将竞拍报价从竞拍者手中转账到eosio.names账户(该账户主管名称拍卖)
INLINE_ACTION_SENDER(eosio::token, transfer)(
token_account, { {bidder, active_permission} },
{ bidder, names_account, bid, std::string("bid name ")+ newname.to_string() }
);
// 创建当前合约的name_bid_table状态表的实例bids
name_bid_table bids(_self, _self.value);
print( name{bidder}, " bid ", bid, " on ", name{newname}, "\n" );
auto current = bids.find( newname.value ); // 先查询是否已存在该标的的历史报价数据
if( current == bids.end() ) { // 如果不存在历史报价数据,则新建
bids.emplace( bidder, [&]( auto& b ) { // 添加该标的的首单竞拍相关字段到状态表。
b.newname = newname;
b.high_bidder = bidder;
b.high_bid = bid.amount;
b.last_bid_time = current_time_point();
});
} else { // 如果已经存在历史报价数据,则处理该标的的最高报价
// 历史最高报价high_bid已被置为负数,则说明该已成功交易,竞拍关闭。
eosio_assert( current->high_bid > 0, "this auction has already closed" );
// 此次新的报价必须高于该标的的历史最高报价的10%,这是竞拍规则。
eosio_assert( bid.amount - current->high_bid > (current->high_bid / 10), "must increase bid by 10%" );
// 如果该标的的当前最高报价已经是当前竞拍者本人,则不需要执行下面的逻辑。
eosio_assert( current->high_bidder != bidder, "account is already highest bidder" );
// 获得竞拍退款状态表big_refund_table的实例refunds_table,传入当前竞拍动作。
bid_refund_table refunds_table(_self, newname.value);
auto it = refunds_table.find( current->high_bidder.value );
if ( it != refunds_table.end() ) {
// 如果在竞拍退款表中找到当前竞拍价格相同的,则更新该条数据对象,增加退款金额为最高报价,以主币形式结算。
refunds_table.modify( it, same_payer, [&](auto& r) {
r.amount += asset( current->high_bid, core_symbol() );
});
} else {
// 如果未找到相同最高报价的,则新增一条数据对象,插入当前最高报价者以及报价价格。
refunds_table.emplace( bidder, [&](auto& r) {
r.bidder = current->high_bidder;
r.amount = asset( current->high_bid, core_symbol() );
});
}
// 打包交易,插入bidrefund动作,传入最高报价者以及标的。
transaction t;
t.actions.emplace_back( permission_level{_self, active_permission},
_self, "bidrefund"_n,
std::make_tuple( current->high_bidder, newname )
);
t.delay_sec = 0;// 定义延迟时间
// 定义延迟id
uint128_t deferred_id = (uint128_t(newname.value) << 64) | current->high_bidder.value;
cancel_deferred( deferred_id ); // 按延迟id取消延迟交易
t.send( deferred_id, bidder ); // 发送延迟交易
// 最后修改name_bid_table状态表的实例bids,将当前竞拍动作更新到该标的对象,包括最高报价者、最高报价以及时间。
bids.modify( current, bidder, [&]( auto& b ) {
b.high_bidder = bidder;
b.high_bid = bid.amount;
b.last_bid_time = current_time_point();
});
}
}
创建账户
创建账户的操作一直都是由system合约的newaccount动作承担的,下面仍旧通过源码分析研究其逻辑。
/**
* @brief 创建账户,包括资源管理以及名称竞拍的逻辑。
*
* @param creator 创建者
* @param newact 被创建的账户,如果包含点“.”,则其创建者也必须包含相同后缀。
* @param owner owner权限
* @param active active权限
*/
void native::newaccount( name creator,
name newact,
ignore<authority> owner,
ignore<authority> active ) {
if( creator != _self ) { // 创建者不能是当前合约账户。
uint64_t tmp = newact.value >> 4; // 将新账户名由字符转为无符号int
bool has_dot = false;// 定义标志位,是否包含点“.”
for( uint32_t i = 0; i < 12; ++i ) {// 遍历12次,因为名称最长12个字符
has_dot |= !(tmp & 0x1f); // 检查是否有点“.”存在,同时还可以检查账户的长度是否少于12位,有则更新has_dot标志位为true。
tmp >>= 5; // 移到下一位检查
}
if( has_dot ) { // 非常规账户
auto suffix = newact.suffix(); // 后缀
if( suffix == newact ) { // 创建者的后缀必须相同
// 在竞拍状态表中寻找创建者拥有的非常规账户,是否包含待创建账户
name_bid_table bids(_self, _self.value);
auto current = bids.find( newact.value );
eosio_assert( current != bids.end(), "no active bid for name");
// 校验当前待创建账户作为竞拍标的,其最高竞拍价是否是创建者报出的。
eosio_assert( current->high_bidder == creator, "only highest bidder can claim" );
// 如果high_bid字段不是负数,说明竞拍未结束,该非常规账户还不属于创建者。
eosio_assert( current->high_bid < 0, "auction for name is not closed yet" );
bids.erase( current ); // 通过以上校验,该竞拍标的属于创建者,创建者创建成功,删除标的历史对象。
} else {
eosio_assert( creator == suffix, "only suffix may create this account" );
}
}
}
// 为新用户分配资源,初始化添加到用户资源状态表
user_resources_table userres( _self, newact.value);
userres.emplace( newact, [&]( auto& res ) {
res.owner = newact;
res.net_weight = asset( 0, system_contract::get_core_symbol() );
res.cpu_weight = asset( 0, system_contract::get_core_symbol() );
});
set_resource_limits( newact.value, 0, 0, 0 );
}
结束语
感受过中医按摩的朋友应该比较了解,这种按摩手法讲究的是疏通经络,反复地从头到脚捋你的经络,直到老师傅认为你的经络通了,通了的表现就是整个人轻松了,气色红扑扑的。本文也又点中医按摩的意思,从头到脚,致力于将一条经络上出现的疙疙瘩瘩的小结揉碎吸收,希望最后达到整条经络通畅的目的。本文较长,适合心平气和之人亦或是查阅的朋友来看。