【精】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 );  
}

結束語

感覺過中醫按摩的朋友應該比較瞭解,這種按摩手法講究的是疏通經絡,反覆地從頭到腳捋你的經絡,直到老師傅認爲你的經絡通了,通了的表現就是整我的輕鬆了,氣色紅撲撲的。本文也又點中醫按摩的意思,從頭到腳,致力於將一條經絡上出現的疙疙瘩瘩的小結揉碎吸取,但願最後達到整條經絡通暢的目的。本文較長,適合心平氣和之人亦或是查閱的朋友來看。


別撒手,快完事了(從最近幾篇文章的表現來看,好吧,我認可過氣了 ↖(▔▽▔)↗

更多文章請轉到醒者呆的博客園

相關文章
相關標籤/搜索