您正在查看: EOS-优秀转载 分类下的文章

源码解析 区块的分叉处理

在区块链上, 分叉是一个老话题了, EOS 也会出现分叉的情况, 下面我们来讲讲在分叉情况它是如何切换到最长链的。

EOS 的生产者是根据 active_schedule 的顺序来生产区块的,但是在决定当前区块生产者的时候是根据 block_time 来的选择的。 假如当前 BP1 生产了一个 block_num 为 100 的块,而BP2和BP3都接受到了,此时根据block_time是 BP2 生产 block_num 为 101 的块, 但 BP3 因为网络原因没收到 BP2 产生的块, 导致它也开始生产 block_num 为 101 的块了, 这时候出现了分叉。 那么哪条链会被最终确认呢,换句话说 2个 block_num 为 101 的块谁会被不可逆,谁会被抛弃, 这要看两条链上的生产者数量, 生产者多的那条链增长速度会比较快, 所以区块更快被确认,比另一条链先不可逆, 这时候就会切换到长的链,而丢弃短的链了。 一般情况下,其他节点接受完BP2的块后都在等待BP3的出块,BP3的出块将BP2的分支覆盖掉了, 基本所有节点都切换到了BP3的链上并开始生产,所以 BP2 的块被丢弃的可能性很大。

先讲下分叉的大致流程,当一个节点收到同个 block_num 的会都 insert 进 fork_multi_index_type 对应的表里,根据 block_state 的 header.previous 形成多叉树(不了解多叉树的读者可以百度一下哈)。每进一个需要分叉的块都会先切换到该块对应的链上。一旦 block_num 最小的确定是哪个块不可逆后,其他分叉下相同 block_num 的块就会被删除。至于为什么不直接把其他分叉全删了。 如果该分叉还有人在生产块,那么该块在接入节点得时候就会出现 unlinkable_block 的错误了,这块不详解,感兴趣的可以自行了解。

下面分析代码,因为分叉只有接受别人的块才会出现,所以从 push_block 开始看起。

void push_block( const signed_block_ptr& b, controller::block_status s ) {
   // ...

      if ( read_mode != db_read_mode::IRREVERSIBLE ) {
         maybe_switch_forks( s );
      }

   // ...   
   } FC_LOG_AND_RETHROW( )
}
void maybe_switch_forks( controller::block_status s = controller::block_status::complete ) {
   auto new_head = fork_db.head();

   // 无分叉情况,正常叠加块
   if( new_head->header.previous == head->id ) {
      try {
         apply_block( new_head->block, s );
         fork_db.mark_in_current_chain( new_head, true );
         fork_db.set_validity( new_head, true );
         head = new_head;
      } catch ( const fc::exception& e ) {
         fork_db.set_validity( new_head, false ); // Removes new_head from fork_db index, so no need to mark it as not in the current chain.
         throw;
      }
   // 头 id 不同,出现分叉情况
   } else if( new_head->id != head->id ) {
      ilog("switching forks from ${current_head_id} (block number ${current_head_num}) to ${new_head_id} (block number ${new_head_num})",
           ("current_head_id", head->id)("current_head_num", head->block_num)("new_head_id", new_head->id)("new_head_num", new_head->block_num) );
      // 获取两个分支上对应的块
      auto branches = fork_db.fetch_branch_from( new_head->id, head->id );

      // 将前一分支的块都标记成不在当前使用分支。
      for( auto itr = branches.second.begin(); itr != branches.second.end(); ++itr ) {
         fork_db.mark_in_current_chain( *itr , false );
         pop_block();
      }
      EOS_ASSERT( self.head_block_id() == branches.second.back()->header.previous, fork_database_exception,
                 "loss of sync between fork_db and chainbase during fork switch" ); // _should_ never fail
      // 切换分支, apply 当前分支的块
      for( auto ritr = branches.first.rbegin(); ritr != branches.first.rend(); ++ritr) {
         optional<fc::exception> except;
         try {
            apply_block( (*ritr)->block, (*ritr)->validated ? controller::block_status::validated : controller::block_status::complete );
            head = *ritr;
            fork_db.mark_in_current_chain( *ritr, true );
            (*ritr)->validated = true;
         }
         catch (const fc::exception& e) { except = e; }
         if (except) {
            elog("exception thrown while switching forks ${e}", ("e",except->to_detail_string()));

            // ritr currently points to the block that threw
            // if we mark it invalid it will automatically remove all forks built off it.
            fork_db.set_validity( *ritr, false );

            // pop all blocks from the bad fork
            // ritr base is a forward itr to the last block successfully applied
            auto applied_itr = ritr.base();
            for( auto itr = applied_itr; itr != branches.first.end(); ++itr ) {
               fork_db.mark_in_current_chain( *itr , false );
               pop_block();
            }
            EOS_ASSERT( self.head_block_id() == branches.second.back()->header.previous, fork_database_exception,
                       "loss of sync between fork_db and chainbase during fork switch reversal" ); // _should_ never fail

            // re-apply good blocks
            for( auto ritr = branches.second.rbegin(); ritr != branches.second.rend(); ++ritr ) {
               apply_block( (*ritr)->block, controller::block_status::validated /* we previously validated these blocks*/ );
               head = *ritr;
               fork_db.mark_in_current_chain( *ritr, true );
            }
            // 抛出一个切换分支的异常。
            throw *except;
         } // end if exception
      } /// end for each block in branch
      ilog("successfully switched fork to new head ${new_head_id}", ("new_head_id", new_head->id));
   }
} /// push_block
void fork_database::prune( const block_state_ptr& h ) {
   auto num = h->block_num;

   auto& by_bn = my->index.get<by_block_num>();
   auto bni = by_bn.begin();
   while( bni != by_bn.end() && (*bni)->block_num < num ) {
      prune( *bni );
      bni = by_bn.begin();
   }

   auto itr = my->index.find( h->id );
   if( itr != my->index.end() ) {
      irreversible(*itr);
      my->index.erase(itr);
   }

   // 当 num 对应的块不可逆化, 其他分支下同等 block_num 就会被删除
   auto& numidx = my->index.get<by_block_num>();
   auto nitr = numidx.lower_bound( num );
   while( nitr != numidx.end() && (*nitr)->block_num == num ) {
      auto itr_to_remove = nitr;
      ++nitr;
      auto id = (*itr_to_remove)->id;
      remove( id );
   }
} 

其实分叉处理就是一个塑造出一个多叉树, 当多叉树上有节点成为不可逆的时候就可以裁剪。
转载自:https://eos.live/detail/16153

【EOSIO Explorer】EOSIO开发者浏览器功能体验

启动EOSIO Explorer

使用如下命令启动EOSIO Explorer,会同时启动后台服务和前端页面:

cd eosio-explorer
yarn eosio-explorer start

EOSIO Explorer功能介绍

启动后的主界面如下图所示:

Header栏展示了所有功能,分为两部分:INSPECT(检查)和INTERACT(交互);

INSPECT(检查)的功能相当于区块浏览器,有以下功能:

  • INFO:即主页展示的区块链信息,包括连接的Nodeos和MongoDB地址、区块链基本信息等;
  • BLOCKS:区块列表;
  • TRANSACTIONS:交易列表;
  • ACTIONS:Action列表;
  • ACCOUNTS:账户列表;
  • SMART CONTRACT:智能合约列表;

INTERACT(交互)包含如下功能:

  • MANAGE ACCOUNTS:管理账户;
  • DEPLOY CONTRACTS:部署合约;
  • PUSH ACTIONS:调用Action;

下面详细介绍这些功能。

账户管理

在账户管理页面可以直接创建账户,会自动生成一对公私钥进行创建:

回到账户管理页面查看账户列表:

区块信息查询

上面创建账户的操作,实际是调用eosio::newaccount Action,调用信息可以在INSPECT的各个tab中查看。
查看交易列表:

查看Action列表:

查看Action详情:

查询账户信息:

编译/部署合约

DEPLOY CONTRACTS界面提供一个编辑器,可以把合约源文件上传(支持拖拽)到编辑器中:

填写好cpp源文件根路径,点击“GENERATE ABI”生成ABI,如果代码存在错误,会在右侧Compiler Errors框提示:

ABI和WASM生成好后选择一个账户来部署合约:

成功部署合约后,右侧会展示部署信息:

同时可以在SMART CONTRACT界面查询合约信息:

调用合约

在PUSH ACTIONS界面可以调用合约的Action:

小结

使用EOSIO Explorer,不需要使用命令,就执行了公私钥的生成、账户创建、合约的编译/部署/调用等操作,省去大量繁琐的命令操作时间,让开发者可以把注意力集中在智能合约逻辑的开发上;

EOSIO Explorer对于熟练的EOS智能合约开发人员是很好的工具,但对于合约开发初学者,还是建议先学习命令行操作,等理解原理后再使用可视化开发工具。

转载地址:https://bihu.com/article/1900672350

【EOSIO Explorer】安装和使用EOSIO开发者浏览器

EOSIO Labs最近发布了一款开发者浏览器:EOSIO Explorer,提供全新的web图形交互界面来帮助开发智能合约。

安装EOSIO Explorer

从EOSIO Explorer的Github地址克隆项目到本地,然后安装:

git clone https://github.com/EOSIO/eosio-explorer.git
cd eosio-explorer
yarn

yarn和npm类似,也是一个nodejs包管理工具,如果没有安装yarn,先使用如下命令安装:

sudo apt remove cmdtest
sudo apt remove yarn
sudo npm install -g yarn

注:npm和yarn命令对比

npm yarn
npm install yarn
npm install react --save yarn add react
npm uninstall react --save yarn remove react
npm install react --save-dev yarn add react --dev
npm update --save yarn upgrade

安装时可能遇到的问题

EOSIO Explorer基于Puppeteer,Puppeteer是Google Chrome团队官方的无界面(Headless)Chrome 工具,安装Puppeteer时可能会报如下错误:

ERROR: Failed to download Chromium r650583! Set "PUPPETEER_SKIP_CHROMIUM_DOWNLOAD" env variable to skip download.

这是因为墙的原因无法下载chromium-browser,先设置不要默认下载:

cd eosio-explorer
echo 'puppeteer_skip_chromium_download=true'>.npmrc

然后到淘宝镜像站选择系统对应的安装包,下载前需要查询需要下载的版本:

cd eosio-explorer/node_modules/puppeteer
cat package.json

在puppeteer下的chromium_revision中可以找到:

要下载的版本是650583,以64位Linux系统为例,下载地址是:
https://npm.taobao.org/mirrors/chromium-browser-snapshots/Linux_x64/650583

下载的文件名为chrome-linux.zip,把它移动到puppeteer的项目目录中并解压,命令如下:

cd eosio-explorer/node_modules/puppeteer
mkdir .local-chromium
cd .local-chromium
mkdir linux64-650583
cd linux64-650583
wget https://npm.taobao.org/mirrors/chromium-browser-snapshots/Linux_x64/650583/chrome-linux.zip
unzip chrome-linux.zip
rm -f chrome-linux.zip

然后重新执行安装命令:

cd eosio-explorer
yarn


显示success表示安装成功,在eosio-explorer目录运行如下命令可以查看eosio-explorer版本:

yarn eosio-explorer -v

运行EOSIO Explorer

第一次运行EOSIO Explorer要进行初始化,这条命令会建立一个Docker容器并删除现有数据(包括MongoDB、本地区块链、本地存储中的数据):

yarn eosio-explorer init

如果此时报错:

Error: Chromium revision is not downloaded. Run "npm install" or "yarn install"
at Launcher.launch

则需要配置puppeteer执行地址:

cd eosio-explorer
echo 'puppeteer_executable_path=/home/songguo/eos-workspace/eosio-explorer/node_modules/puppeteer/.local-chromium/linux64-650583/chrome-linux/chrome'>>.npmrc

再次执行yarn eosio-explorer init命令,成功运行EOSIO Explorer,在本地的5111端口启动了一个React应用:

下次再启动时执行start命令:

yarn eosio-explorer start

执行start时,添加--init参数,会在重新构建应用,并清除React本地存储(不会清除区块链和MongoDB数据):

yarn eosio-explorer start --init

退出应用后,后台的Docker容器也会一直生产区块,需要使用如下命令停止正在运行的Docker容器:

yarn eosio-explorer pause_dockers

如果本地已经安装了EOSIO,使用EOSIO Explorer启动的Docker容器会占用掉8888端口,如果要让本地nodeos可用,需要删除此Docker容器:

yarn eosio-explorer remove_dockers

使用PM2管理应用

EOSIO Explorer应用启动后会在5111端口一直执行,如果要退出应用需要使用命令kill 5111端口的进程,或者使用PM2来管理应用;

PM2是nodejs进程管理工具,可以利用它来简化很多node应用管理的繁琐任务,如性能监控、自动重启、负载均衡等,安装PM2使用如下命令:

npm install -g pm2

查看运行中的进程使用pm2 list命令:

可以看到,运行中的pm2进程有两个,就是EOSIO Explorer启动的进程,停止它们使用pm2 stop all命令:

更多PM2的使用方法可以到官网查看。

小结

这篇文章介绍了EOSIO Explorer的安装和使用方法,更多EOSIO Explorer的功能在后续的文章中介绍。
转载自:https://bihu.com/article/1353071796

EOSIO 1.8.0-rc2 有哪些重要的改进

版本号:1.8.0
Github:https://github.com/EOSIO/eos/releases/tag/v1.8.0-rc2

1 扩展性提升与代码重构

在这个版本中我们看到了大量的提交 , 主要集中在代码重构和线程安全化两个方面 :

首先的代码重构 , 很多人往往不关心这些既没有增加功能也没有提升性能的代码修改 , 但是从开发的角度看 , 这些其实构成后续开发的重要基础 , 我们可以看到 , 自年初以来 , Block.one 的核心开发者一直在改善 EOS.IO 的代码结构 , 并且从设计层面上逐步在建立一个抽象层 , 最明显的是 EOS.IO.cdt 的完善 , 这些构成了 EOS.IO 的框架 , 不同于很多在一开始就给出了庞大架构的团队 ,Block.one 的开发团队一贯以务实且自低向上的思路开发 EOS.IO。

在 1.8.0 版本中 , Block.one 的开发者拆分了很多之前的巨无霸类型 , 剥离了散落在各处的一些复杂逻辑 , 比如 pending_block_state 相关的状态管理 , 就从 block_header_state 中剥离出来 . 还有之前略带“坏味”的 transaction traces, 也在这次重新整理。

另一个方面是关于多线程 , 在最近一段时间有很多关于线程安全性的提交,为下一步实现多线程相关开发做准备,Block.One 团队在 2019 年的开发中对多线程非常重视,此次重构后将很更好的实现 EOS.IO 白皮书里描述的多线程,届时 TPS 会上升到一个新的台阶。

众所周知 , 中大规模 C++软件的多线程开发一直是一个“深坑”, 为了在引入并行计算的同时保证 EOS.IO 的稳定性 , Block.One 团队并没有急于进行多线程相关新特性的开发 , 而是耐心的排查代码中的线程安全问题 , 完善基础架构支持 , 同时在特定的适合并发的独立模块 , 如 chain api 以及 p2p 模块引入对应的多线程实现

2 提升链安全性

在这个版本中 , Block.One 团队修正了之前遗留的一些资源计算问题 , EOS.IO 中资源相关的逻辑很多 , 并且极为分散 , 为了提升链性能并避免节点进行不必要的计算 , EOS.IO 中添加了很多资源相关的特化逻辑 , 这也造成了很多特定场景下用户资源计算的不明确 , 对于 EOS.IO 链来说 , 这带来了很多起潜在的安全性问题 , 前一阶段暴露的由延迟交易所触发的阻塞交易问题 , 很大程度上就是资源检查不完备的问题。

另外 , 某些场景下资源计算的问题也会导致用户权益上的损失 , 所以这方面的修改势在必行。

3 智能合约安全

智能合约安全困扰了 EOS.IO 社区很久,DAPP 开发者在过去一年的实践中发现了大量的问题,Block.One 团队吸收了这些问题并且做出了极大的改进,此次更新对智能合约开发者更友好。

举例来说 , 对于 DAPP 开发者来说 , 一个好消息就是千呼万唤的 GET_SENDER API 终于要引入 EOS.IO 中了 , DAPP 开发者可以通过这个 API 判定当前 Action 是否是通过 inline action 机制触发的 , 据此可以回避大量的基于合约触发 inline action 的攻击手段 . 这个更新,可以一定程度上部分解决了困扰社区已久的随机数问题,需要注意的是 , 即使通过这个 api 可以屏蔽当前的很多攻击手段 , 固然攻击者的攻击成本会大幅提高 , 但是不意味着 DAPP 开发者可以高枕无忧 , 对于开发者来说安全问题永远需要注意 , 没有什么一劳永逸的银弹。

4 硬分叉升级机制

这方面的更新可以使得未来的硬分叉升级更加高效的同时使得硬分叉升级过程中不影响用户使用体验。

近一段时间 , Block.One 其实开发了非常多的新功能,包括上面提到的几个功能 , 但都没有在过往的更新中出现,一个很大的原因就是开发者必须保证 EOS.IO 的兼容性 , 节点可以选择不升级节点版本 , 显然这会极大的阻挠 EOS.IO 的发展 , 这次 1.8.0 的版本与以往的一个最大区别是这个版本不兼容前面的版本 , 也就是所谓的“硬分叉”, 在这个版本之前 EOS.IO 并没有应对硬分叉的机制 , 硬分叉的过程会对用户的使用带来很大影响 , 为了使以后的更新不会像这一次一样造成很大影响 , 这次更新中添加了一系列硬分叉升级机制 , 为的是以后的硬分叉更新可以更加平滑和安。

转载自:https://www.chainnews.com/articles/083001943204.htm

EOS源码分析之八区块及数据结构

eos源码分析之八区块及数据结构

做为EOS系列的最后一篇,把区块及相关的数据结构分析一下。虽然在前面的共识中分析过出块这部分,但对EOS的区块结构及一些细节并没有深入进去。

一、区块

EOS的区块设计不同的版本变化很大,这里以4.0的为模板分析,先看一下它的数据结构:

struct block_header
{
   block_timestamp_type             timestamp;
   account_name                     producer;//帐户标识符 13字节

   uint16_t                         confirmed = 1;  

   block_id_type                    previous;//前一块的HASH

   checksum256_type                 transaction_mroot; /// mroot of cycles_summary
   checksum256_type                 action_mroot; /// mroot of all delivered action receipts

   uint32_t                          schedule_version = 0;
   optional<producer_schedule_type>  new_producers;//新生产者
   extensions_type                   header_extensions;


   digest_type       digest()const;//摘要哈希
   block_id_type     id() const;   //自己的哈希
   uint32_t          block_num() const { return num_from_id(previous) + 1; }
   static uint32_t   num_from_id(const block_id_type& id);//ID是任意数字,区块号是从零长到现在的排序号 ID=HASH+n    
};

struct signed_block_header : public block_header
{
   signature_type    producer_signature;//生产者签名
};
struct signed_block : public signed_block_header {
   using signed_block_header::signed_block_header;
   signed_block() = default;
   signed_block( const signed_block_header& h ):signed_block_header(h){}

   vector<transaction_receipt>   transactions; /// new or generated transactions交易记录
   extensions_type               block_extensions;//扩展区
};
using signed_block_ptr = std::shared_ptr<signed_block>;//重定义一个新的数据类型,方便使用



这里感觉最大的不同是把原来的交易ID直接弄成了交易内容,这样有点简单粗暴的感觉,但是确实是容易理解一些。区块的生产在前面选举后分析过,这里不再赘述,看一下产生区块中对交易的处理。

二、交易和上链



正如所有的区块链一样,交易最终打包入区块,才是真正的区块成功能,也就是说,区块生产出来的目的不是单纯生产块,而要把交易数据打包进去,然后再保存到数据库,最终上链。

1、交易


transaction_trace_ptr push_transaction( const transaction_metadata_ptr& trx,
                                        fc::time_point deadline,
                                        bool implicit,
                                        uint32_t billed_cpu_time_us  )
{
   FC_ASSERT(deadline != fc::time_point(), "deadline cannot be uninitialized");

   transaction_trace_ptr trace;//交易检索
   try {
      transaction_context trx_context(self, trx->trx, trx->id); //交易管理控制
      trx_context.deadline = deadline;
      trx_context.billed_cpu_time_us = billed_cpu_time_us;
      trace = trx_context.trace;
      try {
         if( implicit ) {
            trx_context.init_for_implicit_trx();
         } else {
            trx_context.init_for_input_trx( trx->packed_trx.get_unprunable_size(),
                                            trx->packed_trx.get_prunable_size(),
                                            trx->trx.signatures.size() );
         }

         //检查权限集合
         if( !implicit && pending->_block_status == controller::block_status::incomplete ) {
            check_actor_list( trx_context.bill_to_accounts ); // Assumes bill_to_accounts is the set of actors authorizing the transaction
         }

        //延迟状态
         trx_context.delay = fc::seconds(trx->trx.delay_sec);

         //检查权限,这个前面分析过
         if( !self.skip_auth_check() && !implicit ) {
            authorization.check_authorization(
                    trx->trx.actions,
                    trx->recover_keys( chain_id ),
                    {},
                    trx_context.delay,
                    [](){}
                    /*std::bind(&transaction_context::add_cpu_usage_and_check_time, &trx_context,
                              std::placeholders::_1)*/,
                    false
            );
         }

         //执行上下文,其实就是执行tx中的action
         trx_context.exec();
         trx_context.finalize(); // Automatically rounds up network and CPU usage in trace and bills payers if successful

         //创建恢复点
         auto restore = make_block_restore_point();

         if (!implicit) {
            transaction_receipt::status_enum s = (trx_context.delay == fc::seconds(0))
                                                 ? transaction_receipt::executed
                                                 : transaction_receipt::delayed;
            //交易填充
            trace->receipt = push_receipt(trx->packed_trx, s, trx_context.billed_cpu_time_us, trace->net_usage);
            pending->_pending_block_state->trxs.emplace_back(trx);
         } else {
            transaction_receipt_header r;
            r.status = transaction_receipt::executed;
            r.cpu_usage_us = trx_context.billed_cpu_time_us;
            r.net_usage_words = trace->net_usage / 8;
            trace->receipt = r;
         }
         //填充ACTION
         fc::move_append(pending->_actions, move(trx_context.executed));

         // call the accept signal but only once for this transaction
         if (!trx->accepted) {
            emit( self.accepted_transaction, trx);
            trx->accepted = true;
         }

         emit(self.applied_transaction, trace);// 发送成功打包交易的消息

         trx_context.squash();//不敢肯定,是不是清理回退的数据
         restore.cancel();//取消恢复

         if (!implicit) {
            unapplied_transactions.erase( trx->signed_id );
         }
         return trace;
      } catch (const fc::exception& e) {
         trace->except = e;
         trace->except_ptr = std::current_exception();
      }

      if (!failure_is_subjective(*trace->except)) {
         unapplied_transactions.erase( trx->signed_id );
      }

      return trace;
   } FC_CAPTURE_AND_RETHROW((trace))
}


2、上链



在apply_block中:

void commit_block( bool add_to_fork_db ) {
   if( add_to_fork_db ) {
      pending->_pending_block_state->validated = true;
      auto new_bsp = fork_db.add( pending->_pending_block_state );
      emit( self.accepted_block_header, pending->_pending_block_state );
      head = fork_db.head();
      FC_ASSERT( new_bsp == head, "committed block did not become the new head in fork database" );

   }

//    ilog((fc::json::to_pretty_string(*pending->_pending_block_state->block)));
   emit( self.accepted_block, pending->_pending_block_state );

   if( !replaying ) {
      reversible_blocks.create<reversible_block_object>( [&]( auto& ubo ) {
         ubo.blocknum = pending->_pending_block_state->block_num;
         ubo.set_block( pending->_pending_block_state->block );
      });
   }

   pending->push();
   pending.reset();//恢复状态,可以再次出块

}



通过fork_db的操作把数据库存储起来,然后挂到链上,形成区块链。再广播出去,清除状态,重新准备出块。



三、相关的几个数据结构



有几个数据结构比较重要:multi_index,optional和scoped_exit。



1、访问数据库的multi_index


template<uint64_t TableName, typename T, typename... Indices>
class multi_index
{
   private:

      static_assert( sizeof...(Indices) <= 16, "multi_index only supports a maximum of 16 secondary indices" );

      constexpr static bool validate_table_name( uint64_t n ) {
         // Limit table names to 12 characters so that the last character (4 bits) can be used to distinguish between the secondary indices.
         return (n & 0x000000000000000FULL) == 0;
      }

      constexpr static size_t max_stack_buffer_size = 512;

      static_assert( validate_table_name(TableName), "multi_index does not support table names with a length greater than 12");

      uint64_t _code;
      uint64_t _scope;

      mutable uint64_t _next_primary_key;

      enum next_primary_key_tags : uint64_t {
         no_available_primary_key = static_cast<uint64_t>(-2), // Must be the smallest uint64_t value compared to all other tags
         unset_next_primary_key = static_cast<uint64_t>(-1)
      };

      struct item : public T
      {
         template<typename Constructor>
         item( const multi_index* idx, Constructor&& c )
         :\__idx(idx){
            c(\*this);
         }
......
      };

      struct item_ptr
      {
         item_ptr(std::unique_ptr<item>&& i, uint64_t pk, int32_t pitr)
         : \_item(std::move(i)), \_primary_key(pk), \_primary_itr(pitr) {}

......
      };

      mutable std::vector<item_ptr> _items_vector;

      template<uint64_t IndexName, typename Extractor, uint64_t Number, bool IsConst>
      struct index {
         public:
            typedef Extractor  secondary_extractor_type;
            typedef typename std::decay<decltype( Extractor()(nullptr) )>::type secondary_key_type;
......

            constexpr static uint64_t name()   { return index_table_name; }
            constexpr static uint64_t number() { return Number; }

            struct const_iterator : public std::iterator<std::bidirectional_iterator_tag, const T> {
               public:
                  friend bool operator == ( const const_iterator& a, const const_iterator& b ) {
                     return a.\_item == b.\_item;
                  }
                  friend bool operator != ( const const_iterator& a, const const_iterator& b ) {
                     return a.\_item != b.\_item;
                  }

                  const T& operator*()const { return *static_cast<const T*>(\_item); }
                  const T* operator->()const { return static_cast<const T*>(\_item); }

......

                     return *this;
                  }

                  const_iterator& operator--() {
                     using namespace \_multi_index_detail;

......

                     return \*this;
                  }

                  const_iterator():_item(nullptr){}
               private:
                  friend struct index;
                  const_iterator( const index* idx, const item* i = nullptr )
                  : _idx(idx), _item(i) {}

                  const index* _idx;
                  const item*  _item;
            }; /// struct multi_index::index::const_iterator

            typedef std::reverse_iterator<const_iterator> const_reverse_iterator;

            const_iterator cbegin()const {
               using namespace \_multi_index_detail;
               return lower_bound( secondary_key_traits<secondary_key_type>::lowest() );
            }
......

            const T& get( secondary_key_type&& secondary, const char* error_msg = "unable to find secondary key" )const {
               return get( secondary, error_msg );
            }

            // Gets the object with the smallest primary key in the case where the secondary key is not unique.
            const T& get( const secondary_key_type& secondary, const char* error_msg = "unable to find secondary key" )const {
               auto result = find( secondary );
               eosio_assert( result != cend(), error_msg );
               return *result;
            }

            const_iterator lower_bound( secondary_key_type&& secondary )const {
               return lower_bound( secondary );
            }
            const_iterator lower_bound( const secondary_key_type& secondary )const {
               using namespace \_multi_index_detail;
......

               return {this, &mi};
            }

            const_iterator upper_bound( secondary_key_type&& secondary )const {
               return upper_bound( secondary );
            }
            const_iterator upper_bound( const secondary_key_type& secondary )const {
......

               return {this, &mi};
            }

            const_iterator iterator_to( const T& obj ) {
......
               return {this, &objitem};
            }
......

            static auto extract_secondary_key(const T& obj) { return secondary_extractor_type()(obj); }

         private:
            friend class multi_index;

            index( typename std::conditional<IsConst, const multi_index*, multi_index*>::type midx )
            :_multidx(midx){}

            typename std::conditional<IsConst, const multi_index*, multi_index*>::type _multidx;
      }; /// struct multi_index::index

......
         const item* ptr = itm.get();
         auto pk   = itm->primary_key();
         auto pitr = itm->__primary_itr;

         _items_vector.emplace_back( std::move(itm), pk, pitr );

         return *ptr;
      } /// load_object_by_primary_iterator

   public:

      multi_index( uint64_t code, uint64_t scope )
      :_code(code),_scope(scope),_next_primary_key(unset_next_primary_key)
      {}

......

            _item = &_multidx->load_object_by_primary_iterator( prev_itr );
            return *this;
         }

         private:
            const_iterator( const multi_index* mi, const item* i = nullptr )
            :_multidx(mi),_item(i){}

            const multi_index* _multidx;
            const item*        _item;
            friend class multi_index;
      }; /// struct multi_index::const_iterator

      typedef std::reverse_iterator<const_iterator> const_reverse_iterator;

      const_iterator cbegin()const {
         return lower_bound(std::numeric_limits<uint64_t>::lowest());
      }
      const_iterator begin()const  { return cbegin(); }

.......

      void erase( const T& obj ) {
         using namespace \_multi_index_detail;

......

         hana::for_each( \_indices, [&]( auto& idx ) {
            typedef typename decltype(+hana::at_c<0>(idx))::type index_type;

            auto i = objitem.__iters[index_type::number()];
            if( i < 0 ) {
              typename index_type::secondary_key_type secondary;
              i = secondary_index_db_functions<typename index_type::secondary_key_type>::db_idx_find_primary( \_code, \_scope, index_type::name(), objitem.primary_key(),  secondary );
            }
            if( i >= 0 )
               secondary_index_db_functions<typename index_type::secondary_key_type>::db_idx_remove( i );
         });
      }

};



multi_index这个数据结构同样是仿照BOOST库中的boost::multi_index;估计EOS的开发人员觉得这个太重,自己搞了一个,当然,顺带实现很多自己独立的需求。需要说明的是WIKI上的说明是比较落后的,而且EOS开发团队也声明了,这个容器对象是不断演进的,所以说现在分析的可能已经是落后的了,但可能他们的大原则不会有剧烈的变动。


EOS为每个账户都预留了数据库空间(大小与代币持有量有关),账户可以建立多个数据表。智能合约无法直接操作存储在见证人硬盘中的数据表,需要使用multi_index作为中间工具(或者叫容器),每个multi_index实例都与一个特定账户的特定数据表进行交互(取决于实例化时的参数)。


这个多索引表有几个特点:类似ORM中的映射表,行为独立的对象,列为属性;有主键和非主键,排序时默认为升序,同样主键只能唯一并为uint64_t类型;支持自定函数做为索引,但返回值受限,即只能为支持的键类型;允许多索引排序,但是二级索引不大于16,前面的代码可以看到,同时不支持二级索引的直接构建;类似双向链表可以双向迭代。


它支持主要以下几种操作:


emplace:添加一个对象(row)到表中,返回一个新创建的主键迭代器。在这个过程中创建新对象,序列化写入表中,更新二级索引,付费。如果出现异常则直接抛出。


erase:这个就简单了,直接擦除。可以用迭代器也可以引用对象来删除。删除后返回之后的迭代器,并更新相关索引及费用。


modify:类似于数据库的UPDATE,这个比较麻烦,需要提供更新对象的迭代器,更新对象的引用,帐户(需要付费的)以及更新目标对象的函数(lambada),无返回值,在操作过程中主要是要对payer的属性进行判断,然后进行费用的计算和相关退费,完成后更新索引。




get:由主键查找对象,返回对象的引用,如果没找到,抛出异常。


find:根据主键查找已存在的对象。它的返回值是一个迭代器。如果没有查到返回一个end迭代器。


迭代器有点类似于STD标准库的迭代器,可以前后遍历,这里不再赘述。


2、类boost::optional的自定义容器



/**
 *  @brief provides stack-based nullable value similar to boost::optional
 *
 *  Simply including boost::optional adds 35,000 lines to each object file, using
 *  fc::optional adds less than 400.
 */
template<typename T>
class optional
{
  public:
    typedef T value_type;
    typedef typename std::aligned_storage<sizeof(T), alignof(T)>::type storage_type;

    optional():\_valid(false){}
    ~optional(){ reset(); }

    optional( optional& o )
    :_valid(false)
    {
      if( o._valid ) new (ptr()) T( *o );
      \_valid = o._valid;
    }

......

    template<typename U>
    optional( const optional<U>& o )
    :_valid(false)
    {
      if( o._valid ) new (ptr()) T( *o );
      \_valid = o._valid;
    }

    template<typename U>
    optional( optional<U>& o )
    :_valid(false)
    {
      if( o._valid )
      {
        new (ptr()) T( *o );
      }
      \_valid = o._valid;
    }

    template<typename U>
    optional( optional<U>&& o )
    :_valid(false)
    {
      if( o._valid ) new (ptr()) T( fc::move(*o) );
      \_valid = o._valid;
      o.reset();
    }

    ......

    optional& operator=( optional&& o )
    {
      if (this != &o)
      {
        if( \_valid && o._valid )
        {
          ref() = fc::move(*o);
          o.reset();
        } else if ( !\_valid && o._valid ) {
          \*this = fc::move(*o);
        } else if (\_valid) {
          reset();
        }
      }
      return \*this;
    }


    friend bool operator < ( const optional a, optional b )
    {
       if( a.valid() && b.valid() ) return \*a < \*b;
       return a.valid() < b.valid();
    }
......

    void     reset()
    {
        if( \_valid )
        {
            ref().~T(); // cal destructor
        }
        \_valid = false;
    }
  private:
    template<typename U> friend class optional;
    T&       ref()      { return \*ptr(); }
    const T& ref()const { return *ptr(); }
    T*       ptr()      { return reinterpret_cast<T*>(&\_value);  }
    const T* ptr()const { return reinterpret_cast<const T\*>(&\_value); }

    bool         _valid;
    storage_type _value;
};



这个其实不能称做一个容器,因为它一般只盛放一个数据结构,它的主要目的标题也很清楚,其实是老大们不愿意使用BOOST的相关代码,太多了,这个才几百行,小巧实用。


这个模板类的主要作用是封装一些数据结构,防止未初始化或者无意义的数据表达不清楚。比如一些返回值是NULL,有EOF,还有一些是string::npos等等,封装起来就是为了起一个标准的作用,其实你看这个类内部,并没有太多的真正意义的自己操作的数据,大多还是原生数据结构的使用。

3、范围控制的scoped_exit


template<typename Callback>
class scoped_exit {
   public:
      template<typename C>
      scoped_exit( C&& c ):callback( std::forward<C>(c) ){}

      scoped_exit( scoped_exit&& mv )
      :callback( std::move( mv.callback ) ),canceled(mv.canceled)
      {
         mv.canceled = true;
      }

      scoped_exit( const scoped_exit& ) = delete;
      scoped_exit& operator=( const scoped_exit& ) = delete;

      ~scoped_exit() {
         if (!canceled)
            try { callback(); } catch( ... ) {}
      }

      scoped_exit& operator = ( scoped_exit&& mv ) {
         if( this != &mv ) {
            ~scoped_exit();
            callback = std::move(mv.callback);
            canceled = mv.canceled;
            mv.canceled = true;
         }

         return \*this;
      }

      void cancel() { canceled = true; }

   private:
      Callback callback;
      bool canceled = false;
};

template<typename Callback>
scoped_exit<Callback> make_scoped_exit( Callback&& c ) {
   return scoped_exit<Callback>( std::forward<Callback>(c) );
}



这个类其实也得很有趣,如果对RAII比较了解的话,这个其实有一点变相的意思,在离开某个范围时,调用这个数据结构的析构函数,然后调用指定的回调函数来处理一些相关的事情,比如清理一些内存等等。


这里的模板构造函数用到了std::forward(c)完美转发,将左右值的匹配自动完成。一些小细节处理的相当不错。


其实EOS中的数据结构和编程方式还是有些复杂的,特别是其中一些使用了比较传统的宏模板自动创建的方法(在MFC中常见,但广受诟病),所以一些代码还是比较晦涩的,不建议也这样使用。

转载自:https://github.com/XChainLab/documentation/edit/master/eos/eos%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%E4%B9%8B%E5%85%AB%E5%8C%BA%E5%9D%97%E5%8F%8A%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84.md