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 加载节点生成块所需的功能 详情链接
- chain_plugin:;
- net_plugin:
- 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; }