zoukankan      html  css  js  c++  java
  • 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 odeos目录下,这里面有关于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(" 	,"));
             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
    ";
          return false;
       }
    
       return true;
    }
  • 相关阅读:
    QuartzQuartz定时任务
    jdbc模糊查询、分页查询、联合查询
    PreparedStatement
    web服务器简述
    JDBC基本操作
    RMI
    Http编程
    2020毕业季业务开发宝典
    程序设计流程图
    系统概要框图
  • 原文地址:https://www.cnblogs.com/wangzxblog/p/11857892.html
Copyright © 2011-2022 走看看