深入解读EOS源代码之——区块链内核

EOS进入大众视野并且受到热议已经有一段时间了,各种热捧和争议过后,是时候让我们静下来搞清楚EOS到底是一个什么样的产品。本文从技术角度深入的分析EOS底层设计,从源代码入手,一层层揭开EOS区块链底层逻辑的神秘面纱。

本文的目的是深入解析EOS的源代码,所以是“探底”的工作,而不是展现“全貌”,读者最好对EOS的一些顶层设计原理有所了解,如果您想充分了解这部分内容,请参阅EOS的白皮书(https://github.com/EOSIO/Documentation),含中文翻译。

EOS区块链内核

为了理解EOS区块链的基本原理,首先需要了解这它的数据结构和关键算法,数据结构和算法是构成整个骨架的基本元素,对于深入理解EOS至关重要。下文从一些基本概念开始,逐渐深入底层,一步步探究其中逻辑与思路。

基本概念

  • name

    name在EOS里可以理解为标识符,这与一般意义的英文单词name不同,name的长度是13个字节。源代码中所有形式为xxx_name的名称都可以理解为此类型,一般的,xxx表示其功能或者目的,_name表明这是一个标识符。

  • sha256

    sha256是一个长度为32字节的数据结构,通常用16进制表示,可以理解由32个表示16进制的字符随机组成的字符串,这样的数据结构通常用来表示校验码或者标识符(ID)。ID与name的区别在于name是人类可读的,而ID只是一串随机的字符序列。

  • scope

    scope实际是一个上层设计的概念,并不属于底层实现的范畴,只是相关scope的说明并不多,在这里略作解释。scop可以理解为权限,也就是规定当前这个操作所要求的授权列表,scope可以包含零个,一个,或者多个授权要求。

  • merkle

    merkle是指默克尔树,这是一种利用Hash算法总是输出固定长度的数这一特性而设计的树型数据结构,它的特点是消耗空间小,利用Hash的特点可以用很小的存储空间来验证大量数据的签名,而不需要把数据全部保存下来,就可以保证数据的完整性和一致性。有关默克尔树的详细解释,请参考维基百科(https://en.wikipedia.org/wiki/Merkle_tree

  • digest

    digest中文含义是摘要,属于密码学范畴。对于EOS中的区块来说,摘要就是对整个区块数据做Hash运算得到的结果,由于Hash的结果长度固定,而且单向不可逆,完全符合摘要的要求。下面是源代码中对digest函数的定义:

    digest_type block_header::digest()const
       {
          return digest_type::hash(*this);
       }
  • block_id vs block_num

    很多人把区块id和区块序号混为一谈,其实它们是不一样的概念,区块id是一个与区块内容有关的标识,而区块序号仅仅是从0计数的区块编号。鲜为人知的是,这两个概念之间在底层实现上是有关系的,请看下面的代码:

   block_id_type signed_block_header::id()const
   {
      block_id_type result = fc::sha256::hash(*this);
      result._hash[0] &= 0xffffffff00000000;
      result._hash[0] += fc::endian_reverse_u32(block_num()); // store the block num in the ID, 160 bits is plenty for the hash
      return result;
   }

不难看出id实际是由两部分组成:对区块内容做Hash运算,然后与区块序号进行拼接而得到的。所以id实际包含了区块序号,通过id可以计算得出序号。

  • action vs message

    值得指出的是,目前在EOS中message和action的概念是一样的,实际上action是自3.0以后的新规范,message的概念已经可以抛弃,由于3.0版本发布到目前为止只有几天的年龄,互联网上的大部分文字和材料依然使用message的概念,为了缓解读者的困扰,在此特做说明,下文均采用action代替message。

区块

区块是EOS和其它所有区块链技术的核心数据结构,要掌握任何一个区块链技术,首先需要理解的就是区块的设计。

区块的目的

在EOS的源代码中,设计者给出了区块这个结构的主要目的:

   /**
    *  The block_summary defines the set of transactions that were successfully applied as they
    *  are organized into cycles and shards. A shard contains the set of transactions IDs which
    *  are either user generated transactions or code-generated transactions.
    *
    *
    *  The primary purpose of a block is to define the order in which messages are processed.
    *
    *  The secodnary purpose of a block is certify that the messages are valid according to 
    *  a group of 3rd party validators (producers).
    *
    *  The next purpose of a block is to enable light-weight proofs that a transaction occured
    *  and was considered valid.
    *
    *  The next purpose is to enable code to generate messages that are certified by the
    *  producers to be authorized. 
    *
    *  A block is therefore defined by the ordered set of executed and generated transactions,
    *  and the merkle proof is over set of messages delivered as a result of executing the
    *  transactions. 
    *
    *  A message is defined by { receiver, code, function, permission, data }, the merkle
    *  tree of a block should be generated over a set of message IDs rather than a set of
    *  transaction ids. 
    */
    struct signed_block_summary : public signed_block_header {
      ...
   };

翻译成中文就是:区块的

  1. 主要目的是定义处理消息的顺序。
  2. 次要目的是通过第三方生产者验证消息的有效性。
  3. 第三个目的是提供轻量级的交易验证机制。
  4. 第四个目的是授权给智能合约代码,以产生新的消息。

区块的结构

因此区块是由按顺序组织的交易来构成的。区块像是一个容器,它自顶向下依次包含以下这些结构:

Block:  区块

    => regions:  区域 

        => cycles:  周期
      
            => shards:  片区

                => transactions:  交易

                    => actions:  操作

然而从区块的直接设计来看,它的定义就是按顺序构成的交易集合:

/**
    * This structure contains the set of signed transactions referenced by the
    * block summary. This inherits from block_summary/signed_block_header and is
    * what would be logged to disk to enable the regeneration of blockchain state.
    *
    * The transactions are grouped to mirror the cycles in block_summary, generated
    * transactions are not included.  
    */
   struct signed_block : public signed_block_summary {
      digest_type                  calculate_transaction_merkle_root()const;
      vector<signed_transaction>   input_transactions; /// this is loaded and indexed into map<id,trx> that is referenced by summary
   };

这与上面表述的结构不一致,如何理解呢?实际上,从signed_block的定义不难看出,这个区块的定义“继承”了signed_block_summary的定义,而signed_block_summary的定义就是上述的自上而下的包含结构,所以两种区块的不同表示并不冲突,可以理解为包含结构是底层结构的一种映射,实际的区块仅仅需要定义交易的先后顺序即可,而cycle,shard这些概念是为了有机的组织一定数量的交易而设计出来的,是交易组合的不同视图和抽象,目的是为了更方便高效的执行运算,包括区块的验证和签名等等。

区块的存储形式 - 区块日志

区块日志是存储区块的二进制文件,区块日志的特性是只能从末尾追加(append only),区块日志包含两类文件:

  • 区块文件,结构如下:
    +---------+----------------+---------+----------------+-----+------------+-------------------+
    | Block 1 | Pos of Block 1 | Block 2 | Pos of Block 2 | ... | Head Block | Pos of Head Block |
    +---------+----------------+---------+----------------+-----+------------+-------------------+
    

区块文件包含区块的内容已经每个区块的位置信息。区块位置信息是固定的8字节宽度,这样便于在连续读取区块的时候,按照读一个区块,向后跳跃8个字节,读一个区块,向后跳跃8个字节的模式快速加载区块内容。

  • 索引文件,结构如下:
    +----------------+----------------+-----+-------------------+
    | Pos of Block 1 | Pos of Block 2 | ... | Pos of Head Block |
    +----------------+----------------+-----+-------------------+

区块索引的目的在于提供一个基于区块序号的快速随机搜索方法,如果一直区块序号,使用索引文件可以快速定位目标区块在区块文件中的具体位置。索引文件不是必须的,没有索引文件区块链仍然可以运行,索引文件的主要作用是通过少量空间换取速度提升。索引文件可以通过顺序读取区块文件来重新构建。
如果读者运行过eos全节点的eosiod程序,可以发现这个程序启动时会创建上述两个文件,如下图所示

blockchain@syphml11:~/eos/build/programs/eosd/data-dir/blocks$ ls -lh
total 137M
-rw-rw-r-- 1 blockchain blockchain 6.9M Jan 29 18:29 blocks.index
-rw-rw-r-- 1 blockchain blockchain 130M Jan 29 18:29 blocks.log

区块中的关键算法

我们现在了解了区块的结构,而结构的设计是为了运算的目的,下面我们来介绍与区块相关的关键算法。

计算交易的默克尔树根

这样的表述或许有些过于术语化,对于这一步运算,读者需要理解的是对于一个区块的主要运算实际就是做Hash验证,而默克尔树就是为了提高验证效率而设计的数据结构,这个树型结构的根部是一个Hash值,这个Hash值前后是否一致就表明区块数据的有效性,这里需要强调的是由于交易的id已经包含了交易内容的Hash值,所以在计算默克尔树的时候并不需要重新计算交易的Hash值,只需要通过交易的id计算默克尔树的各个结点即可。源代码如下:

   digest_type   signed_block::calculate_transaction_merkle_root()const {
      vector<digest_type> ids; 
      ids.reserve(input_transactions.size());

      for( const auto& t : input_transactions ) 
         ids.emplace_back( t.id() );

      return merkle( std::move(ids) );
   }

实际上,在不同视图下的默克尔树的计算原理都类似,下面分别是在shard和region这两个视图下的默克尔树计算:

 void shard_trace::calculate_root() {
      static const size_t GUESS_ACTS_PER_TX = 10;

      vector<digest_type> action_roots;
      action_roots.reserve(transaction_traces.size() * GUESS_ACTS_PER_TX);
      for (const auto& tx :transaction_traces) {
         for (const auto& at: tx.action_traces) {
            digest_type::encoder enc;

            fc::raw::pack(enc, at.receiver);
            fc::raw::pack(enc, at.act.account);
            fc::raw::pack(enc, at.act.name);
            fc::raw::pack(enc, at.act.data);
            fc::raw::pack(enc, at.region_id);
            fc::raw::pack(enc, at.cycle_index);
            fc::raw::pack(enc, at.data_access);

            action_roots.emplace_back(enc.result());
         }
      }
      shard_root = merkle(action_roots);
   }

   digest_type block_trace::calculate_action_merkle_root()const {
      vector<digest_type> shard_roots;
      shard_roots.reserve(1024);
      for(const auto& rt: region_traces ) {
         for(const auto& ct: rt.cycle_traces ) {
            for(const auto& st: ct.shard_traces) {
               shard_roots.emplace_back(st.shard_root);
            }
         }
      }
      return merkle(shard_roots);
   }

上面这段代码值得注意的是shard视图下的默克尔树构建,shard之下已经是交易了,这一层的运算EOS采取的是直接对交易内容中的每一个action进行打包,再构建默克尔树,所以这一层面的计算不是基于id的,而是基于内容的。

交易

上文提到,区块的基本组成是交易,交易的基本单位是操作。下面我们来介绍交易的数据结构。

交易的数据结构

交易状态

当一笔交易被某个区块引用时,区块生产者针对这笔交易会做出相应的操作,而操作的不同结果会导致这笔交易的不同状态,交易一共有三种状态:

  • 成功执行
  • 软失败:执行未成功,但是异常处理机制成功捕获错误并且执行
  • 硬失败:执行未成功,异常处理机制也执行失败

交易数据结构视图

    +-----------+--------------+------------+--------+----+----------+----------+----+-----------+
    | block num | block prefix | expiration | region | id | action 1 | action 2 |... | signature |
    +-----------+--------------+------------+--------+----+----------+----------+----+-----------+
    +------------------head  ------------------------+--------------------body-------------------+

交易头

交易头的长度是固定的,可以避免解析时的动态内存分配,便于快速解析交易部分信息。值得指出的是,所有的交易都有一个期限,这个期限限定了一个交易必须在规定时间内被纳入区块链,如果我们发现一个交易的时限已经过去,就可以放心的放弃这个交易,因为所有生产者都不会将它纳入任何区块。

除了占用4个字节的期限,交易头还包含2个字节的区块序号和4个字节的区块前缀,以及2个字节的区域(region)标识。交易头的总长度是12个字节。

交易体

交易体包含了3部分内容:

  • 交易ID
  • 交易操作
  • 交易签名

交易ID是通过对交易内容本身经过Hash运算得出,所以每个交易的ID是与其内容一一对应的。交易的主体是由操作构成的,关于操作的细节,本文的后面部分会有详细解读。一个交易在纳入区块之前必须含有签名,用以验证交易的合法性,除此之外,交易体也包含了签名的摘要,同样用于交易的验证机制。

延迟型交易

交易分为两种类型:一种是账户发起的普通交易,一种是由代码生成的自动交易,自动交易可以设置一个延迟时间,这样的交易叫延迟型交易,这种交易不会立即被执行,而是等到设定时间到时才会被执行。

交易中的关键算法

在交易这一视图层的算法相对简单,不涉及复杂的运算和策略,其中值得一提的是从签名中提取公钥的计算。这个过程并不复杂,可以总结为下面三个环节:

  • 遍历交易的所有签名
  • 通过签名和签名摘要(digest)抽取公钥
  • 把公钥和签名组成的配对放入缓存,以便下次快捷访问

以下是这部分计算的源代码:

flat_set<public_key_type> signed_transaction::get_signature_keys( const chain_id_type& chain_id )const
{ try {
   using boost::adaptors::transformed;

   constexpr size_t recovery_cache_size = 100000;
   static recovery_cache_type recovery_cache;
   const digest_type digest = sig_digest(chain_id);

   flat_set<public_key_type> recovered_pub_keys;
   for(const signature_type& sig : signatures) {
      recovery_cache_type::index<by_sig>::type::iterator it = recovery_cache.get<by_sig>().find(sig);

      if(it == recovery_cache.get<by_sig>().end() || it->trx_id != id()) {
         public_key_type recov = public_key_type(sig, digest);
         recovery_cache.emplace_back( cached_pub_key{id(), recov, sig} ); //could fail on dup signatures; not a problem
         recovered_pub_keys.insert(recov);
         continue;
      }
      recovered_pub_keys.insert(it->pub_key);
   }

   while(recovery_cache.size() > recovery_cache_size)
      recovery_cache.erase(recovery_cache.begin());

   return recovered_pub_keys;
} FC_CAPTURE_AND_RETHROW() }

操作

EOS区块链中的交易是由一个个操作组成的,操作可以理解成一个能够更改区块链全局状态的方法,操作的顺序是确定的,一个交易内的操作要么全部执行成功,要么都不执行,这与交易的本意(transaction)是一致的。操作是区块链的最底层逻辑,相当于区块链这个大脑的神经元,区块链的智能最终也是通过一个个操作的组合来实现的。从EOS的源代码中不难看出,作者对于操作的设计是下了一番功夫的。

操作的设计原则

在源代码中,EOS的作者给出了以下几大设计原则:

  • 独立原则 操作本身须包含足以解释操作含义的信息,而不需要依赖区块链提供的上下文信息来帮助诠释操作。所以,即便一个操作的当前状态可以通过区块链上的数据推导得出,我们也需要将状态信息纳入操作数据中,以便每个操作是容易理解的。这个原则体现的是区块的可解释性,这一点非常重要,这个底层的设计原则将影响整个区块链的使用效率。

  • 余额可计算原则 一个账户当前的余额计算,仅仅依赖于与这个账户相关的信息便可得出,而不需要解析整个区块链才能获得。这个原则针对的是比特币的设计,由于比特币的余额计算需要扫描区块链中的所有交易才能精准的计算出一个账户的余额,这使得一个非常基础的计算落地起来都变得相当繁琐,EOS的这个设计目的在于提升运算效率。

  • 明确费用原则 区块链的交易费用随时间变化而变化,所以,一个签名过的交易须明确的认同这个交易所需要支付的费用,这个费用是在交易形成之前就已经设定并且明确好了的,这一点也非常重要,因为明确的费用协议才能保证余额的正确计算。

  • 明确授权原则 每个操作须包含足够的授权信息以标明是哪一个账户拥有授权这个操作的权力,这种明确授权的设计思路带来几个好处:

    • 便于集中管理
    • 可以优化授权管理
    • 便于并行处理
  • 关联账户原则 每个操作须包含足够的足够关联账户信息,以保证这个操作能够遍历所有相关联的账户,也就是这个操作能够影响的所有账户,这个原则的目的同样是为了确保账户的余额能够得到及时和准确的运算。

操作的设计思路

操作的来源

一个操作可以通过两种途径产生:

  1. 由一个账号产生,通过签名来授权,即显性方式。
  2. 由代码生成,即隐形方式。

底层逻辑

操作的设计遵循React Flux设计模式(https://github.com/facebook/flux/tree/master/examples/flux-concepts),简单的说就是每一个操作将会被赋予一个名称,然后被分发给一个或者多个handler。在EOS环境中,每个操作对应的handler是通过scope和name来定义的,默认的handler也可以再次将操作分发给下一级的多个handler。所以,每个EOS应用可以实现自己的handler,当操作被分发到这个应用时,相应的handler的代码就会被执行。

操作的设计思路中另一重要概念是授权。每一个操作的执行都必须确保具备了指定账户的授权。授权通过许可(permission)的方式声明,对于一个授权动作,账户可以要求任意数量的许可,许可的验证是独立于代码执行的,只有所有规定的许可被成功验证之后,对应的代码才能够被执行。我们能感受到这里面设计者对安全性的追求是很极致的,设计者将安全特性深深的嵌入了区块链的底层设计逻辑,同时又不让安全机制成为性能和结构的累赘,让它自成体系,独立管理。

操作的数据结构

操作的数据结构定义如下:

struct action {
      account_name               account;
      action_name                name;
      vector<permission_level>   authorization;
      bytes                      data;

     ...
   };

理解了操作的设计思路的读者一定会惊呼:竟然如此简洁!

一个操作仅由四个部分构成:

  1. 账户:操作的来源
  2. 名称:操作的标识
  3. 授权:执行操作的许可列表
  4. 数据:执行操作需要用到的信息

这样简洁的结构不需要过多解释了。

操作的关键算法

在操作这个视图层,比较复杂的算法逻辑是与授权相关的部分,这部分内容最好的资料是EOS的官方文档,读者可以查看有关这部分内容的中文翻译:https://github.com/BlockchainTranslator/EOS/blob/master/wiki/Accounts-%26-Permissions.md

链控制器

读者现在应该对EOS区块链内核的底层结构和基本逻辑有了了解,上述内容是内核的基本组成部分,如同一个机器的零部件,然而,要让机器运转起来,还需要把这些零部件串起来的控制中心,而链控制器(chain_controller)就是这个控制中心。下面我们来详细介绍EOS的链控制器。

链控制器的基本功能

首先,我们需要理解链控制器存在的主要目的是作为外部世界与区块链内核之间的交互界面,所以它有着承上启下的作用,承上为支撑区块链与外部的交互,启下为管理区块链内部的状态变更。所以,从理解的不同角度,链控制器可以被理解为以下两个概念:

  1. 从外部视角,链控制器是一个数据库,这与链外对区块链的理解是一致的,从狭义的角度看,区块链就像一个不可更改的数据库,而链控制器可以看做这个数据库的管家,外部世界不用直接与区块链底层通信,而是通过这个管家来与区块链交互。
  2. 从内部视角,链控制器是一个协调者,区块链内部各个部件均独立运作,这与上述的设计原则是一致的,这样的话,各个部件之间如何调度与协调,就需要一个有全局视角的角色来统一管理,这就是链控制器的第二个作用。

用作者的原话来说,链控制器的目的是以一种可以扩展的方式来跟踪区块链的状态变化

链控制器的基本要素

为了维护可靠的区块链状态,链控制器管理着两个不用类型的数据库:

  1. 区块日志 前文已经介绍过了。它的作用是存储已经经过验证的不可逆转的区块链主干。
  2. 块数据库 这是一个结构化数据库,关于块数据库的详细解读,将会在另外的文章里面说明,在这里读者只需要明白这是一个带索引的可以增删改查的数据库。它的作用是维护和管理未经验证的区块链主干与分支两个子数据库,主干数据库存储的是当前长度最长的一条链,而分支存储的是因为不同分叉而造成的分支链,随着时间的推移,主链是有可能被分支替代的,每当一个新的区块要写入时,EOS都会重新计算各分支和主干的长度,并且决定要不要切换主干。最后,执行错误或者验证不通过的区块最终会被抛弃,并从数据库中清理出去。

除了存储部分,链控制器还需精确维护一个消息机制,以保证对区块链中的其他成员广播最新的区块状态变更,这个消息机制是区块链网络能够正常和自主运行的基础。

事务处理机制

熟悉数据库的读者一定知道,对于数据库的写入操作,维持一个事务的原子性是很关键的,也就是说,对于一批写入操作,要么全都成功执行,要么都不执行,一定不能出现只执行了其中一部分的情况,因为这样将导致数据的不一致性,这样的错误是致命的。EOS中对于块数据库的写操作采用同样的设计原则,事务的使用主要体现在两个层面:

  1. 区块 整个区块的内容是一个整体,要么全都写入,要么都不写入
  2. 交易 一个交易是一个整体,其中的操作要么都执行,要么都不执行

当事务中的某个步骤出现错误时,整个事务就会启动回滚机制,整个区块链将会恢复到这个事务发生之前的状态。

链控制器中的关键过程

链控制器有着统筹全局,协调资源的作用,它管理的过程中最重要的有三个:

链控制器的初始化

链控制器的初始化是区块链的起点,它的核心过程如下所示:

/*
*  initialize the controller
*/
chain_controller::chain_controller( const chain_controller::controller_config& cfg )
:_db( cfg.shared_memory_dir, 
      (cfg.read_only ? database::read_only : database::read_write), 
      cfg.shared_memory_size), 
 _block_log(cfg.block_log_dir) 
{
   _initialize_indexes();

   for (auto& f : cfg.applied_irreversible_block_callbacks)
      applied_irreversible_block.connect(f);

   contracts::chain_initializer starter(cfg.genesis);
   starter.register_types(*this, _db);

   // Behave as though we are applying a block during chain initialization (it's the genesis block!)
   with_applying_block([&] {
      _initialize_chain(starter);
   });

   _spinup_db();
   _spinup_fork_db();

   if (_block_log.read_head() && head_block_num() < _block_log.read_head()->block_num())
      replay();
} /// chain_controller::chain_controller

它包含了6个环节:

  1. 初始化数据库索引
  2. 与需要广播的对象建立连接,以便每次新区块的加入可以被区块链网络收听到
  3. 初始化创世区块的配置,创世区块指的是区块链中的第一个区块,是之后所有区块的老祖宗,其名因此而来
  4. 初始化区块链,详见下文
  5. 启动数据库
  6. 如果区块日志中已经有内容,那么重新播放所有历史区块

区块链的初始化

上面提到的区块链的初始化过程的核心代码如下所示:

/*
*  initialize the blockchain
*/
void chain_controller::_initialize_chain(contracts::chain_initializer& starter)
{ try {
   if (!_db.find<global_property_object>()) {
      _db.with_write_lock([this, &starter] {
         auto initial_timestamp = starter.get_chain_start_time();
         FC_ASSERT(initial_timestamp != time_point(), "Must initialize genesis timestamp." );
         FC_ASSERT( block_timestamp_type(initial_timestamp) == initial_timestamp,
                    "Genesis timestamp must be divisible by config::block_interval_ms" );

         // Create global properties
         const auto& gp = _db.create<global_property_object>([&starter](global_property_object& p) {
            p.configuration = starter.get_chain_start_configuration();
            p.active_producers = starter.get_chain_start_producers();
         });

         _db.create<dynamic_global_property_object>([&](dynamic_global_property_object& p) {
            p.time = initial_timestamp;
            p.recent_slots_filled = uint64_t(-1);
            p.virtual_net_bandwidth = gp.configuration.max_block_size * (config::blocksize_average_window_ms / config::block_interval_ms );
            p.virtual_act_bandwidth = gp.configuration.max_block_acts * (config::blocksize_average_window_ms / config::block_interval_ms );
         });

         // Initialize block summary index
         for (int i = 0; i < 0x10000; i++)
            _db.create<block_summary_object>([&](block_summary_object&) {});

         auto acts = starter.prepare_database(*this, _db);

         // create a block for our genesis transaction to send to applied_irreversible_block below
         signed_block block{};
         block.producer = config::system_account_name;
         block_trace btrace{block};
         btrace.region_traces.emplace_back();
         auto& rtrace = btrace.region_traces.back();
         rtrace.cycle_traces.emplace_back();
         auto& ctrace = rtrace.cycle_traces.back();
         ctrace.shard_traces.emplace_back();
         auto& strace = ctrace.shard_traces.back();

         signed_transaction genesis_setup_transaction; // not actually signed, signature checking is skipped
         genesis_setup_transaction.actions = move(acts);
         block.input_transactions.emplace_back(genesis_setup_transaction);

         ilog( "applying genesis transaction" );
         with_skip_flags(skip_scope_check | skip_transaction_signatures | skip_authority_check | received_block | genesis_setup, 
         [&](){ 
            transaction_metadata tmeta( genesis_setup_transaction );
            transaction_trace ttrace = __apply_transaction( tmeta );
            strace.append(ttrace);
         });

         // TODO: Should we write this genesis block instead of faking it on startup?
         strace.calculate_root();
         applied_block(btrace);
         applied_irreversible_block(block);

         ilog( "done applying genesis transaction" );
      });
   }
} FC_CAPTURE_AND_RETHROW() }

上面这段代码不难理解,它描述了这样一个初始化过程:

  1. 确定区块链创建时间
  2. 初始化全局变量
  3. 初始化区块概要索引
  4. 创建创世区块(genesis)
  5. 将创世区块写入区块链
  6. 计算创世区块的默克尔树根
  7. 广播新区块(创世区块)的加入

区块的创造

区块的创造过程相对比较繁琐,下面是核心部分的源代码:

      // verify that the block signer is in the current set of active producers.
      shared_ptr<fork_item> new_head = _fork_db.push_block(new_block);
      //If the head block from the longest chain does not build off of the current head, we need to switch forks.
      if (new_head->data.previous != head_block_id()) {
         //If the newly pushed block is the same height as head, we get head back in new_head
         //Only switch forks if new_head is actually higher than head
         if (new_head->data.block_num() > head_block_num()) {
            wlog("Switching to fork: ${id}", ("id",new_head->data.id()));
            auto branches = _fork_db.fetch_branch_from(new_head->data.id(), head_block_id());

            // pop blocks until we hit the forked block
            while (head_block_id() != branches.second.back()->data.previous)
               pop_block();

            // push all blocks on the new fork
            for (auto ritr = branches.first.rbegin(); ritr != branches.first.rend(); ++ritr) {
                ilog("pushing blocks from fork ${n} ${id}", ("n",(*ritr)->data.block_num())("id",(*ritr)->data.id()));
                optional<fc::exception> except;
                try {
                   auto session = _db.start_undo_session(true);
                   _apply_block((*ritr)->data, skip);
                   session.push();
                }
                catch (const fc::exception& e) { except = e; }
                if (except) {
                   wlog("exception thrown while switching forks ${e}", ("e",except->to_detail_string()));
                   // remove the rest of branches.first from the fork_db, those blocks are invalid
                   while (ritr != branches.first.rend()) {
                      _fork_db.remove((*ritr)->data.id());
                      ++ritr;
                   }
                   _fork_db.set_head(branches.second.front());

                   // pop all blocks from the bad fork
                   while (head_block_id() != branches.second.back()->data.previous)
                      pop_block();

                   // restore all blocks from the good fork
                   for (auto ritr = branches.second.rbegin(); ritr != branches.second.rend(); ++ritr) {
                      auto session = _db.start_undo_session(true);
                      _apply_block((*ritr)->data, skip);
                      session.push();
                   }
                   throw *except;
                }
            }
            return true; //swithced fork
         }
         else return false; // didn't switch fork
      }

这个过程可以理解为:

  1. 检查区块生产者的合法性
  2. 如果当前主干已经不是最长链,需要切换到最长的分叉链上切换过程如下
    1. 弹出一定数量的区块,直到碰到分叉处
    2. 将分叉中的新区块写入主干数据库
    3. 切换后的分叉成为主干
    4. 如果上述过程失败,则恢复状态
  3. 如果不需要切换分支,则往主干数据库中写入区块
  4. 返回结果,以表明这次写入是否造成了分支切换

交易的写入

交易的写入过程相对简单一些,源代码如下所示:

// record a tx
transaction_trace chain_controller::__apply_transaction( transaction_metadata& meta ) {
   transaction_trace result(meta.id);
   for (const auto &act : meta.trx.actions) {
      apply_context context(*this, _db, act, meta);
      context.exec();
      fc::move_append(result.action_traces, std::move(context.results.applied_actions));
      fc::move_append(result.deferred_transactions, std::move(context.results.generated_transactions));
      fc::move_append(result.canceled_deferred, std::move(context.results.canceled_deferred));
   }

   uint32_t act_usage = result.action_traces.size();

   for (auto &at: result.action_traces) {
      at.region_id = meta.region_id;
      at.cycle_index = meta.cycle_index;
      if (at.receiver == config::system_account_name &&
          at.act.account == config::system_account_name &&
          at.act.name == N(setcode)) {
         act_usage += config::setcode_act_usage;
      }
   }

   update_usage(meta, act_usage);
   record_transaction(meta.trx);
   return result;
}

这个过程可以总结为:

  1. 遍历交易中的所有操作,对于每个操作:
    1. 获取操作的相关信息,并且执行操作
    2. 记录这次操作执行
  2. 更新因这次操作而消耗的资源统计数据
  3. 记录交易的内容
  4. 返回结果

总结

希望从底层代码的解读中,读者能以一个更为深入的角度来理解EOS的设计思路,也希望这样解释角度可以帮助读者在EOS平台上搭建新的应用或者改造EOS的行为以便符合新的需求,有了这些认知,EOS在读者面前或许不再披着神秘面纱,更重要的是,读者现在有了一个入手的突破点,从这里开始,可以逐步的将EOS纳为己用,创造出更精彩的应用。

推荐阅读更多精彩内容

  • 关于EOS的信息介绍官网 http://block.one/ | https://eos.io/开源 https...
    大圣2017阅读 78,414评论 5 149
  • 草案:2017 年 6 月 26 日 (@dayzh (https://steemit.com/@dayzh)) ...
    区块链生存指南阅读 991评论 0 4
  • 一、首先需要解决“蛇”的威胁。 解决蛇的最好的办法就是“以静制动”。蛇视力不好,习性是爱静,蛇只有受到惊吓的时...
    在一方阅读 137评论 0 0
  • 一:下山 这个世界怎么了。 吕望一页一页的撕着手里薄薄的黄皮书,他的身体颠簸,因为他倒骑在一头驴背上,他中午吃了鸡...
    林喜喜阅读 86评论 2 2
  • 今天周末,在家里窝了一天,吃了一天的外卖,看到镜子里的自己,肿的跟猪头一样,我在减肥这条路上走了又停停了又走,但是...
    乔儿J阅读 17评论 0 0