EOS智能合约深度解析

写在前面的话

看EOS源码,就像穿越千座大山,感觉已经搞懂了一个地方,但是随之而来的是更大的谜团。

公司要求我了解智能合约的相关原理,我专心用EOS的合约作为调研对象,但是到现在为止,还是有许多很大的谜团在等待着我:

1.EOS合约的结构是怎样的,有什么是必须的,什么是可选的,为什么必须的就是的必须?

2.我们知道,合约通过eosio-cpp 编译成wasm格式和可读模式abi文件,但是编译的细节又是怎样?

3.合约是怎么部署的,部署后的文件是什么类型,存储在哪里,存储的内容又是什么?是怎么生成一个hash码的?

4.怎么调用合约,合约之间的接口又是什么形式存储。

5.更改合约内容后如何重新部署

6.如何快速升级合约,是否需要升级合约?

等等。

所以,不得不去看源码,但是如何找到线头可以更快的去了解这些东西,这是非常重要的,本文旨在尝试通过笔者特有的角度试着解答这些谜题。

EOS关联的技术确实很多很多,多到不可能一个人能够知道所有,谁敢说他精通LLVM的同时,对CMAKE也非常了解,即便对CMAKE也很精通,但是C++11,C++14甚至C++17呢?

或许更大的谜题是Boost,是wasm。我没法解决这些问题,因此当我看到有这些相关的知识点时,我只能试着避过它们,对他们实现的细节,性能等我不去深究,我只在我看到的地方大概知道是干嘛的就可以了,我相信,别人也是这样干的。好了,我们一起来深究智能合约的谜团吧。

从什么地方开始?我试着从链和块的生成开始

链和块是如何生成的

通过我之前的文档,我们已经知道EOS是如何部署的,有关细节请参考:

eosio_build.sh 执行过程eosio_build_centos.sh执行过程eosio_install.sh执行过程

这里面已经完全安装了工具包和eos软件,也就是说,当执行完eosio_build.sh 和 eosio_install.sh后,一切都已经准备好了,包括LLVM,包括Boost,也许还包括MongoDB。但是还有些东西没有准备好,比如wasm 虚拟机,比如block和chain,看过官方文档的人知道,要把这一切准备好,只需要执行这样的命令:

nodeos -e -p eosio \
--plugin eosio::producer_plugin \
--plugin eosio::chain_api_plugin \
--plugin eosio::http_plugin \
--plugin eosio::history_plugin \
--plugin eosio::history_api_plugin \
--filter-on="*" \
--access-control-allow-origin='*' \
--contracts-console \
--http-validate-host=false \
--verbose-http-errors >> nodeos.log 2>&1 &
producer_plugin 加载节点生成块所需的功能 详情链接
chain_api_plugin 将chain_plugin的功能公开给http_plugin管理的RPC API接口 详情链接
http_plugin 在EOSIO节点上启用任何RPC API所需的核心插件 详情链接
history_plugin

 为区块链对象提供了一个缓存层,用于获取历史数据。它取决于chain_plugin的数据

history_api_plugin使用它来提供对区块链数据的只读访问。

详情链接
history_api_plugin  将history_plugin中的功能公开给http_plugin管理的RPC API接口,以提供对区块链数据的只读访问。 详情链接

这很关键,也许,突破口就在这里,我们只要明白命令实现的每一个细节就可以了。

nodeos是管理节点的,在${WORKSPACE}\programs\nodeos目录下,这里面有关于nodeos的所有内容,先从CMakeLists.txt文件入手,第一行就是这个

add_executable( ${NODE_EXECUTABLE_NAME} main.cpp )

其中${NODE_EXECUTABLE_NAME}就是nodeos,这个只要ctrl+r就知道了,透露下,这里的定义是在主CMakeLists.txt里面的,cleos和keosd都是在主CMakeLists.txt定义,内容如下

set( CLI_CLIENT_EXECUTABLE_NAME cleos )
set( NODE_EXECUTABLE_NAME nodeos )
set( KEY_STORE_EXECUTABLE_NAME keosd )

 ps:

    add_executable:引入一个名为< name>的可执行目标,该目标会由调用该命令时在源文件列表中指定的源文件来构建

    set:用来显式的定义变量

也就是说,当使用nodeos命令时,会执行main.cpp里面的方法,也就是执行main方法,现在开始分析main方法,关键代码如下

 

 1       app().set_version(eosio::nodeos::config::version);
 2 
 3       auto root = fc::app_path();
 4       app().set_default_data_dir(root / "eosio" / nodeos::config::node_executable_name / "data" );
 5       app().set_default_config_dir(root / "eosio" / nodeos::config::node_executable_name / "config" );
 6       http_plugin::set_defaults({
 7          .default_unix_socket_path = "",
 8          .default_http_port = 8888
 9       });
10       if(!app().initialize<chain_plugin, net_plugin, producer_plugin>(argc, argv))
11          return INITIALIZE_FAIL;
12       initialize_logging();
13       ilog("${name} version ${ver}", ("name", nodeos::config::node_executable_name)("ver", app().version_string()));
14       ilog("${name} using configuration file ${c}", ("name", nodeos::config::node_executable_name)("c", app().full_config_file_path().string()));
15       ilog("${name} data directory is ${d}", ("name", nodeos::config::node_executable_name)("d", app().data_dir().string()));
16       app().startup();
17       app().exec();

其中:

  • 第一行是设置节点的版本号;
  • 3、4、5行分别设置节点数据的存储路径和配置路径,以linux为例,一般是root/.local/eosio/nodeos/data和root/.local/eosio/nodeos/config目录下
  • 6、7、8、9设置socket链接路径和端口
  • 10行chain、net以及producer插件,如果实例化失败则返回失败错误码
    chain_plugin 处理和聚合EOSIO节点上的链数据所需的核心插件 详情链接
    net_plugin 为持久同步节点提供了一个经过身份验证的p2p协议 详情链接
    producer_plugin 加载节点生成块所需的功能 详情链接
  1. chain_plugin:;
  2. net_plugin:
  3. producer_plugin:加载节点生成块所需的功能
  • 12行实例化日志
  • 13、14、15打印日志,分别打印版本号、config文件路径、data目录路径
  • 16行启动节点
  • 17行执行节点

在main中,有几个函数需要特别分析,分别为第10行的initialize和16行的startup以及17行的exec方法,其中他们都是通过app()调用,这个对象定义在libraries/appbase/include/appbase/application.hpp第259行。

application& app();

application.hpp为所有插件和链的基础类,官方解释如下:

The AppBase library provides a basic framework for building applications from a set of plugins. AppBase manages the plugin life-cycle and ensures that all plugins are configured, initialized, started, and shutdown in the proper order.

翻译如下:AppBase库提供了从一组插件构建应用程序的基本框架。AppBase管理插件的生命周期,并确保所有插件按正确的顺序配置、初始化、启动和关闭。

application.hpp代码结构如下:

 

 包括两个class,application.class和plugin.class,其中application.class的实现类在application.cpp,具体看initialize方法,该方法定义如下:

template<typename... Plugin>
bool initialize(int argc, char** argv) {
return initialize_impl(argc, argv, {find_plugin<Plugin>()...});
}

有关typename的用法请查看文档C++ typename的起源与用法

好吧,其实这一段也很难懂,这里面包括了initialize_impl方法和{find_plugin<Plugin>()...}这一部分,涉及了可变参数解析,plugin验证以及实例化方法实现逻辑等相关功能,find_plugin源码如下

template<typename Plugin>
Plugin* find_plugin()const {
    string name = boost::core::demangle(typeid(Plugin).name());
    return dynamic_cast<Plugin*>(find_plugin(name));
}

相关函数和定义介绍:

  • boost::core::demangle:获取demangle符号名的常规方法。它接受一个变形的字符串,比如在某些实现(比如g++)上由typeid(T).name()返回的字符串,然后返回它的格式,这是人类可读的。如果请求失败(例如,如果名称不能解释为一个混乱的名称),函数将返回name。
  • typeid(var).name():运行时获取var名称
  • dynamic_cast:主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换。作用:将一个基类对象指针(或引用)cast到继承类指针,dynamic_cast会根据基类指针是否真正指向继承类指针来做相应处理,对指针进行dynamic_cast,失败返回null,成功返回正常cast后的对象指针;对引用进行dynamic_cast,失败抛出一个异常,成功返回正常cast后的对象引用。

其中find_plugin(name)方法源码如下

abstract_plugin* application::find_plugin(const string& name)const
{
   auto itr = plugins.find(name);
   if(itr == plugins.end()) {
      return nullptr;
   }
   return itr->second.get();
}

作用:根据名称获取插件,如果获取到,则返回插件,如果没有获取到,则返回空指针。

最后是initialize_impl方法,该方法比较长,但是关键代码不多,现在把关键代码贴出来

bool application::initialize_impl(int argc, char** argv, vector<abstract_plugin*> autostart_plugins) {
   //设置程序选项,这个方法非常重要,笔者之前一直搞不明白my对象是怎么设置相关变量信息的,但其实都是在这个方法实现,该方法的实现逻辑也非常复杂
//如果要一一贴出来实在是篇幅太大,只能读者去探究源码,大概功能说明下:
//1.循环读取每一个插件,这里包括
chain_plugin, net_plugin, producer_plugin,
//2.定义plugin_cli_opts和plugin_cfg_opts
//3.通过多态机制调用相关插件的set_program_options方法,初始化plugin_cli_opts和plugin_cfg_opts值
//4.my设置_app_options和_cfg_options值
set_program_options();
   //存储并解析程序选项
   bpo::variables_map options;
   bpo::store(bpo::parse_command_line(argc, argv, my->_app_options), options);
   //如果命令行参数包括 help,打印选项信息
   if( options.count( "help" ) ) {
      cout << my->_app_options << std::endl;
      return false;
   }
   //如果命令行参数包括version,打印版本信息
   if( options.count( "version" ) ) {
      cout << version_string() << std::endl;
      return false;
   }
   //打印默认配置
   if( options.count( "print-default-config" ) ) {
      print_default_config(cout);
      return false;
   }
   //my设置绝对data-dir地址
   if( options.count( "data-dir" ) ) {
      // Workaround for 10+ year old Boost defect
      // See https://svn.boost.org/trac10/ticket/8535
      // Should be .as<bfs::path>() but paths with escaped spaces break bpo e.g.
      // std::exception::what: the argument ('/path/with/white\ space') for option '--data-dir' is invalid
      auto workaround = options["data-dir"].as<std::string>();
      bfs::path data_dir = workaround;
      if( data_dir.is_relative() )
         data_dir = bfs::current_path() / data_dir;
      my->_data_dir = data_dir;
   }
   //my设置绝对config-dir地址
   if( options.count( "config-dir" ) ) {
      auto workaround = options["config-dir"].as<std::string>();
      bfs::path config_dir = workaround;
      if( config_dir.is_relative() )
         config_dir = bfs::current_path() / config_dir;
      my->_config_dir = config_dir;
   }
  //my设置绝对loggingconf地址
   auto workaround = options["logconf"].as<std::string>();
   bfs::path logconf = workaround;
   if( logconf.is_relative() )
      logconf = my->_config_dir / logconf;
   my->_logging_conf = logconf;
   //my设置绝对config-file-name地址
   workaround = options["config"].as<std::string>();
   my->_config_file_name = workaround;
   if( my->_config_file_name.is_relative() )
      my->_config_file_name = my->_config_dir / my->_config_file_name;
   //如果config-ini文件不存在,则新增config-ini文件,并写入配置信息
   if(!bfs::exists(my->_config_file_name)) {
      if(my->_config_file_name.compare(my->_config_dir / "config.ini") != 0)
      {
         cout << "Config file " << my->_config_file_name << " missing." << std::endl;
         return false;
      }
      write_default_config(my->_config_file_name);
   }
   //从config.ini中解析命令行选项,并设置到bpo中
   bpo::parsed_options opts_from_config = bpo::parse_config_file<char>(my->_config_file_name.make_preferred().string().c_str(), my->_cfg_options, false);
   bpo::store(opts_from_config, options);

   std::vector<string> set_but_default_list;

   for(const boost::shared_ptr<bpo::option_description>& od_ptr : my->_cfg_options.options()) {
      boost::any default_val, config_val;
      if(!od_ptr->semantic()->apply_default(default_val))
         continue;

      if(my->_any_compare_map.find(default_val.type()) == my->_any_compare_map.end()) {
         std::cerr << "APPBASE: Developer -- the type " << default_val.type().name() << " is not registered with appbase," << std::endl;
         std::cerr << "         add a register_config_type<>() in your plugin's ctor" << std::endl;
         return false;
      }

      for(const bpo::basic_option<char>& opt : opts_from_config.options) {
         if(opt.string_key != od_ptr->long_name())
            continue;

         od_ptr->semantic()->parse(config_val, opt.value, true);
         if(my->_any_compare_map.at(default_val.type())(default_val, config_val))
            set_but_default_list.push_back(opt.string_key);
         break;
      }
   }
   if(set_but_default_list.size()) {
      std::cerr << "APPBASE: Warning: The following configuration items in the config.ini file are redundantly set to" << std::endl;
      std::cerr << "         their default value:" << std::endl;
      std::cerr << "             ";
      size_t chars_on_line = 0;
      for(auto it = set_but_default_list.cbegin(); it != set_but_default_list.end(); ++it) {
         std::cerr << *it;
         if(it + 1 != set_but_default_list.end())
            std::cerr << ", ";
         if((chars_on_line += it->size()) > 65) {
            std::cerr << std::endl << "             ";
            chars_on_line = 0;
         }
      }
      std::cerr << std::endl;
      std::cerr << "         Explicit values will override future changes to application defaults. Consider commenting out or" << std::endl;
      std::cerr << "         removing these items." << std::endl;
   }

   if(options.count("plugin") > 0)
   {
      auto plugins = options.at("plugin").as<std::vector<std::string>>();
      for(auto& arg : plugins)
      {
         vector<string> names;
         boost::split(names, arg, boost::is_any_of(" \t,"));
         for(const std::string& name : names)
            get_plugin(name).initialize(options);
      }
   }
   try {
      for (auto plugin : autostart_plugins)
         if (plugin != nullptr && plugin->get_state() == abstract_plugin::registered)
            plugin->initialize(options);

      bpo::notify(options);
   } catch (...) {
      std::cerr << "Failed to initialize\n";
      return false;
   }

   return true;
}

 

posted @ 2019-11-15 10:11  重设代码的天空  阅读(892)  评论(0编辑  收藏  举报