【精】EOS智能合约:system系统合约源码分析

系统合约在链启动阶段就会被部署,是由于系统合约赋予了EOS链资源、命名拍卖、基础数据准备、生产者信息、投票等能力。本篇文章将会从源码角度详细研究system合约。html

关键字:EOS,eosio.system,智能合约,name类型,native.hpp,newaccount,bidname,core token init,onblock,更新已入选生产节点ios

eosio.system 概览

笔者使用的IDE是VScode,首先来看eosio.system的源码结构。以下图所示。git

image

本文分析的源码来自于eosio.contractsgithub

1、native.hpp

该文件能够分为两个部分,前一个部分是定义了一些结构体,后一个部分是帮助eosio.system合约声明action。整体看上去,这个文件是负责权限的结构。下面先看他都定义了哪些结构体。算法

权限等级权重

struct permission_level_weight {
  permission_level  permission;
  uint16_t          weight;

  EOSLIB_SERIALIZE( permission_level_weight, (permission)(weight) )
};

注意,合约中定义的结构体通常都会在末尾加入EOSLIB_SERIALIZE宏,将结构体的字段属性序列化,这行代码不是必须的,但加上了可以加快解析的速度,从而提高编译效率。json

权限等级权重结构体只有两个字段,一个是permission_level类型的对象permission,另外一个是16位的无符整型类型的权重。permission_level是定义在eosiolib/action.hpp文件中的一个结构体。它是经过一个帐户名以及其权限名构建的,例如{"useraaaaaaaa","active"},这样的一个组合构成了一个权限对象。api

公钥权重

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,但经过字面含义便可理解,就是等待的秒数。less

权力

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 <int>)

但不容许

void foo(float,ignore <int>,int)。

由于int已经被声明为忽略类型,因此后面不能再做为非忽略类型出现了。ignore结构体源码以下:

template <typename T>
struct [[eosio::ignore]] ignore {};

其余[[eosio::action]]

动做 返回值 参数 解释
updateauth void ignore<name> account<br>ignore<name> permission<br>ignore<name> parent<br>ignore<authority> auth 更新帐户的某项权限内容
deleteauth void ignore<name> account<br>ignore<name> permission 删除帐户的某项权限内容
linkauth void ignore<name> account<br>ignore<name> code<br>ignore<name> type<br>ignore<name> requirement 链接其余帐户
unlinkauth void ignore<name> account<br>ignore<name> code<br>ignore<name> type 解除某帐户的链接
canceldelay void ignore<permission_level> canceling_auth<br>ignore<capi_checksum256> trx_id 取消某个延迟交易
onerror void ignore<uint128_t> sender_id<br>ignore<std::vector<char>> sent_trx 处理错误
setabi void name account<br>const std::vector<char>& abi 设置帐户的abi内容
setcode void name account<br>uint8_t vmtype<br>uint8_t vmversion<br>const std::vector<char>& code 设置帐户的code内容

2、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) )  
};

3、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标准货币体系与源码实现分析】

4、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))
};

5、eosio.system.hpp

下面查看system合约的主要头文件eosio.system.hpp,该文件包含了合约的属性,定义了大量结构体用于支撑system合约的业务功能,下面重点浏览system合约的成员属性。

成员 权属 名称 解释
_voters 私有属性 voters_table实例 投票状态表,表名为voters,<br>结构为voter_info结构体。
_producers 私有属性 producers_table实例 生产者信息状态表,表名为produceers,<br>包含一个自定义索引prototalvote,<br>结构为producer_info结构体。
_global 私有属性 global_state_singleton实例 全局状态单例状态表,表名为global,<br>结构为eosio_global_state结构体,<br>继承自eosio::block-chain_parameters<br>与genesis.json内容高度匹配。
_gstate 私有属性 eosio_global_state结构体实例 就是上面这个状态表的数据结构实例。
_rammarket 私有属性 rammarket实例 内存市场状态表,定义在exchange_state.hpp头文件中。<br>表名为rammarket,结构为使用了bancor算法的<br>exchange_state结构体。

下面继续介绍system合约的成员函数内容,

成员 权属 解释
update_elected_producers 私有函数 只有一个参数是时间戳,按照时间戳更新已入选的生产节点名单。
update_votes 私有函数 更新投票信息。包含参数有投票者、代理、生产者投票内容,<br>以及支持或反对的标识。
changebw 私有函数 更改某帐户的资源量,包含出资者、接收者、cpu资源量、net资源量,<br>以及是否以转帐的形式更改。<br>即抵押资源量的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创世帐户发起,<br>用于更新生产者生产区块信息以及上链的帐号名称拍卖信息。<br>传入时间戳和生产者
delegatebw 公共函数 与私有函数changebw的参数彻底相同,用于抵押资源的主要方法。
undelegatebw 公共函数 与抵押函数相反,是用来解除抵押的方法。
buyram 公共函数 为帐户购买内存资源。有出资方
buyrambytes 公共函数 上面是以token的方式购买内存资源,这一个是之内存量字节的方式购买。
sellram 公共函数 卖出内存资源。
refund 公共函数 在抵押动做未完成时,发起退款
regproducer 公共函数 注册成为备用生产者。
unregprod 公共函数 解除备用生产者的注册
setram 公共函数 设置最大内存量,为链增长内存容量,注意只能增长不能下降。
voteproducer 公共函数 为生产者投票,校验投票者签名,而后调用了私有函数update_votes函数。
regproxy 公共函数 注册成为代理
setparams 公共函数 设置链参数eosio::blockchain_parameters
claimrewards 公共函数 生产者认领出块奖励
setpriv 公共函数 设置帐户是否为特权帐户
rmvproducer 公共函数 移除失效生产者并标记
bidname 公共函数 拍卖帐户名称

6、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 );  
}

结束语

感觉过中医按摩的朋友应该比较了解,这种按摩手法讲究的是疏通经络,反复地从头到脚捋你的经络,直到老师傅认为你的经络通了,通了的表现就是整我的轻松了,气色红扑扑的。本文也又点中医按摩的意思,从头到脚,致力于将一条经络上出现的疙疙瘩瘩的小结揉碎吸取,但愿最后达到整条经络通畅的目的。本文较长,适合心平气和之人亦或是查阅的朋友来看。


别撒手,快完事了(从最近几篇文章的表现来看,好吧,我认可过气了 ↖(▔▽▔)↗

更多文章请转到醒者呆的博客园

相关文章
相关标签/搜索