挖矿算法比较

下文介绍的算法基本上是对抗 ASIC 的发展史.

 

什么是哈希?

 

消息摘要

 

SHA256

 

CPU 型, non ASIC  resistant

 

Scrypt

 

Scrypt 大量使用内存, 起初号称 GPU resistant, 结果失败了, 又号称 ASIC resistant, 结果还是失败了.

 

因为早期内存价格高, 而现在内存价格下降, 厂商经过努力也研发出了速度还可以的针对 scrypt 算法的 ASIC 矿机.

 

Scrypt 在 ASIC 对抗上做的努力也不是没有成效, Scrypt 的 ASIC 矿机速度确实是比 SHA256 ASIC 矿机速度低. 蚂蚁 S9 (SHA256) 算力可达到 14T/s, 而同期的 Scrypt 矿机蚂蚁 L3+ (Scrypt) 算力才 500M/s.

 

相关链接:

https://bitcoin.stackexchange.com/questions/49199/how-is-scrypt-asic-resistant

 

Ethash

 

以太坊的哈希算法, 基于 Dagger-Hashimoto 改造. 内存型,并且所需内存周期性增长。

 

关于 Ethash 抵抗 ASIC 属性, https://ethereum.stackexchange.com/questions/16811/is-ethereum-asic-resistant 分析的很好.

 

首先由从以太坊白皮书可知, Ethash 要求矿工从最后几个区块里随机取几个交易算出来. 由于以太坊的交易可能包含智能合约, 智能合约中的运算可能是各种类型, 要计算各种类型的计算这是 CPU 的工作, 这就使得 ASIC 无用武之地.

 

其次, 白皮书里没说的一点是 Ethash 需要内存一定要快, ASIC 需要外挂 DRAM, 而 DRAM 的速度是完全比不上显卡里的 SRAM 的.

 

这两点正中 ASIC 要害, 所以从 whattomine.com 来看 Ethash 目前还是要用显卡挖.

 

但是前段时间也确实爆出了比特大陆要推 E3 以太坊矿机,参数都出来了,从暴露的参数来看,E3 矿机配备了 72G 大的 DDR 内存,相比比特币矿机 S9 只有 512M 内存,真是鲜明对比。

 

深度分析:比特大陆入局以太坊矿机将给行业带来怎样的影响?

https://36kr.com/p/5127311.html

 

以太坊社区针对此事进行了讨论,V 神认为这件事不是太大的威胁: https://cryptoslate.com/vitalik-buterin-ethash-asics-ethereum/

 

Equihash

 

Equihash 是 Zcash 采用的哈希算法,后来也被 ZenCash,BitcoinGold 所采用。

 

Equihash 也是内存型算法,其抵抗 ASIC 的关键是找到两个合适的常数来使得算法耗内存最大,一般所说的 Equihash 算法都是指这两个常数为 <200,9> 时的 Equihash 算法。

 

2018 年 4 月, 比特大陆推出 Z9 mini 矿机针对 Equihash 算法。

 

2018 年 6 月,BitcoinGold 发现了 Equihash 算法的另一对常数 <144,5> 能够耗更多的内存: https://forum.bitcoingold.org/t/our-new-equihash-equihash-btg/1512, 这篇文章也深入介绍了 ASIC 的短板,值得一看

 

Zhash

 

Zhash 是基于 Equihash 的强化版,由 bitcoinz 项目开发引入(待确认)

 

CryptoNight

 

特点是依赖 CPU 三级缓存,L3 缓存是 ASIC 和显卡所不配备的装置。 所以 CryptoNight 不但防 ASIC,还防显卡。

 

然而,2018 年 3 月, 比特大陆推出 X3 矿机针对 CryptoNight 算法。为此,门罗币还特意进行了硬分叉来对抗 X3 矿机。

 

相关链接:

CryptoNight算法沦陷,ASIC矿机即将上市,XMR矿工告急? https://zhuanlan.zhihu.com/p/34632126

 

XMR顺利硬分叉,ASIC矿机出局,门罗社区保卫战胜利

https://mp.weixin.qq.com/s?__biz=MzUyNzMzMjE5MA==&mid=2247485689&idx=1&sn=e9d352a5ed86471e45299d518e8d23f9&chksm=fa006ec8cd77e7de2d0e59a99ea04cafaab67a8ad9cf376311e4e86abf46fcf41e1ed972303d&scene=21#wechat_redirect

 

X11, X13, X15 等

 

当 Scrypt 也被矿机攻陷, 达世币带着 X11 就出来了. X11 是 11 种算法串联计算, 上一个算法的输出作为下一个算法的输入, ASIC 上要实现这个逻辑要花一段时间.

 

X11 的 11 个算法是: blake, bmw, groestl, jh, keccak, skein, luffa, cubehash, shavite, simd, echo, 这 11 个都是 SHA3 家族算法 (bitcoin 出现时 SHA3 还未问世, 所以用的是 SHA2 家族的 SHA256 算法). 代码在 https://github.com/dashpay/dash_hash/blob/master/dash.c.

 

但 X11 最终也被矿机攻陷. 2017 年 8 月, 比特大陆发布蚂蚁 D3 矿机, 算力可达 34G/s.

 

相关链接

https://bitcointalk.org/index.php?topic=599319.0

https://bitcointalk.org/index.php?topic=599319.msg6626544#msg6626544

 

X 系列算法组成: https://getpimp.org/what-are-all-these-x11-x13-x15-algorithms-made-of/

 

另外 D3 矿机的出现还被认为毁了达世币 http://www.qukuaibiji.com/dash-kuangji.html, 看完就知道为什么各个币都要对抗 ASIC 矿机了.

 

X16R

 

RavenCoin 提出的算法. 不仅仅不 X11 多了几个算法, 这些个算法的次序还是可以改变的, 使用前一个区块的哈希值决定本区块的算法次序. 这使的再次在一段时间内不会出现 ASIC 矿机.

 

有句话这样说:

 

POW机制必然会导致中心化,专业程度越高,获得的市场份额就越多。所有的POW算法都无法阻挡ASIC矿机的出现,最多只能拖延一时。你可以用 Scrypt 算法来阻挡 SHA-256 的矿机,你可以用 X11 算法来阻挡 Scrypt 算法的矿机,顶多只能做到这样了。只要币价足够高,一定会导致ASIC矿机的出现。

 

X16R 所用的算法是 X15 + SHA512.

 

相关链接

https://bitcointalk.org/index.php?topic=3242152.0

 

Docker 简明指南

一站式文档

docs.docker.com

架构

docker 是 C/S 架构, 一个 docker daemon 运行在后台, 我们直接使用的是 docker 的前端程序. 运行前端程序时, 前端程序会将相关的指令发给 daemon, daemon 解析执行再返回给前端.

镜像与容器

An image is a filesystem and parameters to use at runtime. It doesn’t have state and never changes. A container is a running instance of an image. When you ran the command, Docker Engine:

- checked to see if you had the hello-world software image
- downloaded the image from the Docker Hub (more about the hub later)
- loaded the image into the container and “ran” it

镜像来源

任何人都可以创建自己的镜像, 并将其放到 Docker Hub 上. docker daemon 默认会知道从 Docker Hub 上找镜像.

镜像的创建

自己创建镜像需要一个 Dockerfile 描述文件, 写好 Dockerfile 后执行 docker 命令, docker daemon 就会创建相应的镜像. 再执行 docker images 命令就能看到创建的这个镜像.

可以看到创建镜像我们只提供了一个描述文件, 没有提供创建镜像所需要软件和文件等, 也没指定创建好的镜像放在那里, 实际上我们自己创建的镜像和下载的镜像是放在一个专门的目录里的, 我们不需要操心镜像在本地的位置, docker 自己能找到它.

提示

docker 镜像一般是尽可能做的小, 以节省带宽和存储. 比如 Docker Hub 中的 ubuntu 镜像, 默认是连 ifconfig, ping 这样的工具都不包含的, 初次之外默认还砍掉了很多其他组件, 所以 ubuntu 的镜像总共才 100MB. 安装以后如果需要的话可以自己 apt-get install 添加.

Docker for Mac 的问题

Mac 版的 docker 没有 docker0 接口, 宿主机无法与容器直接通信.

https://docs.docker.com/docker-for-mac/networking/#use-cases-and-workarounds

Docker 的存储理解

理解镜像, 容器, 层以及存储驱动的概念:

https://docs.docker.com/engine/userguide/storagedriver/imagesandcontainers/

要持久化存储容器里的改动, 一个是 commit 自己的改动, 构建新自己的新的镜像. 一个是挂载 data volume, 将数据存 data volume 上.

data volume 就是宿主机上的文件或目录, 容器启动时可以挂载这个文件或目录. 多个容器可以共享相同的 data volume.

https://docs.docker.com/engine/userguide/storagedriver/imagesandcontainers/#data-volumes-and-the-storage-driver

Docker 的网络绑定

https://docs.docker.com/engine/userguide/networking/default_network/binding/

数据存储

https://docs.docker.com/engine/admin/volumes/

三种方式:

  • volumes
  • bind mounts
  • tmpfs volumes

这三种方式对容器内部而言表现一样, 展现给容器内部要么是一个目录, 要么是个文件.

osx 下的 bind mounts

在 osx 系统下, 因为种种原因, bind mounts 和在 Linux 系统下不太一样. osx 系统下需要先配置要 bind mounts 的目录, 这可以在 Preferences -> File Sharing 下完成, 默认有配置四个目录:

  • /Users
  • /Volumes
  • /tmp
  • /private

这四个目录包括子目录都能直接 mount 到容器里.

你可能也发现过有些目录不在这四个之中但是也 mount 成功了, 比如:

  • docker run -it -v /etc:/dockeroot ubuntu:16.04
  • docker run -it -v /bin:/dockeroot ubuntu:16.04

也都是可以成功的. 这是因为 docker for mac 里还存在一个叫做 Moby Linux VM 的东西, 当你 mount 了一个没在 File Sharing 中配置过的目录时, 就会从 Moby Linux VM 中找这个目录. 上面两个例子
, 如果仔细看一下, 其实挂载到 /dockeroot 下的内容并不是你 osx 机器的 /etc 或者 /bin 的内容.

关于这一点可以看官方的说明, 以及后两个链接:

https://docs.docker.com/docker-for-mac/osxfs/#namespaces
https://stackoverflow.com/questions/45122459/docker-mounts-denied-the-paths-are-not-shared-from-os-x-and-are-not-known
https://github.com/docker/for-mac/issues/1970

打造自己的开发环境

使用 macbook 有半个月了, 很好, 但是在服务器开发环境方面和 linux 确实还是有点距离的, 最简单的 php + fpm + nginx 的环境都不好搞. 所以今天想借助 docker 搞一个这样的环境试一下.

方法一:

所做的修改及时 commit

方法二:

自己写 Dockerfile 或者 compose.yaml

方法三:

使用这个项目: https://github.com/phusion/baseimage-docker

企业化

docker 实际使用时, 一个容器内可能启动多个服务, 如何管理这多个服务呢?

docker 容器里一般不包含 init 程序的, CMD 命令所指定的进程就是 dokcer 里的第一个程序, 一般我们使用 python 的 supervisord 框架来作为 docker 容器内的第一个进程, 并用它来管理其它的进程

Q&A

镜像 ID 是什么

关于镜像 ID, 官方文档说的很不清楚. 下面这个链接说的比较清楚, 认真看完这个链接, 就都懂了

http://windsock.io/explaining-docker-image-ids/

如何查看某镜像的创建历史及其父镜像

http://windsock.io/explaining-docker-image-ids/

docker inspect 命令结果的含义

http://windsock.io/explaining-docker-image-ids/

docker 镜像在 osx 系统上的位置

~/Library/Containers/com.docker.docker/Data/com.docker.driver.amd64-linux/Docker.qcow2, 这是一个 qmenu 镜像文件

Dockerfile 中的 FROM 指令对应 docker history 中的 ADD file, Add file 中的 “file” 是哪来的

https://stackoverflow.com/questions/41972328/docker-history-base-image-addsha256hash

如何得到某个镜像的 base image

https://stackoverflow.com/questions/31149775/docker-how-to-get-the-name-user-repotag-of-the-base-image-used-to-build-ano

docker history 命令的输出不是完全按照 Dockerfile 中的指令逆序输出的

Dockerfile 中这么写

FROM xxx
MAINTAINER cifer

生成的镜像, docker history 的输出可能是

ADD file:xxxxxxx
MAINTAINER cifer

镜像越来越大了, 删除文件然后 commit, 镜像不会减小的?

是的, 了解了镜像的结构就会明白了, 镜像本身包含的 layer 都是只读的. 容器运行时, 我们处于一个新的可写的 layer 中, 删除文件, 然后 commit, commit 会把这个新 layer 写入镜像, 我们对文件的
删除也只是在新的 layer 中删除了, 老的 layer 中是依然包含这个文件的, 所以镜像不会变小的. 但是启动镜像之后, 你所删的文件确实是没有了.

那该怎么办呢? 使用 docker save 或者 docker export 命令

https://stackoverflow.com/questions/22655867/what-is-the-difference-between-save-and-export-in-docker

https://tuhrig.de/difference-between-save-and-export-in-docker/ 这篇文章的作者就是上面 so 链接里的提问者, 作者收到的回答之后回去写了一篇博客作为总结, 赞!

docker tag 的作用

在我们本地构建的镜像会包含多个层, 执行 docker history 会发现每个层有一个对应的 IAMGE ID, 这是因为 Dockerfile 里的每条指令都会创建一个新层并 commit.

docker tag 可以让这个名字的镜像回到某个层. 但是似乎必须是在本地构建的层才能回去.

Graphene 源码阅读 – 交易篇 – 交易费用

交易费用

操作类型不同, 所需费用也不同. 各项操作的费用记录在 global_property_object::chain_parameters::fee_schedule 中.

石墨烯代码将创世信息中的 inital_parameters::current_fees, global_property_object::chain_parameters::fee_schdule, 以及各项操作中的 struct fee_parameters_type {} 结构关联了起来.

节点启动之前, 一般我们会使用 —create-genesis-json 选项创建创世文件, 创世文件中的 inital_parameters::current_fees 信息会使用各个操作的 struct fee_parameters_type {} 结构写入, 参见:

// 代码 1.1
//  libraries/app/application.cpp

 79 namespace detail {
  80
  81    graphene::chain::genesis_state_type create_example_genesis() {
  82       auto nathan_key = fc::ecc::private_key::regenerate(fc::sha256::hash(string("nathan")));
  83       dlog("Allocating all stake to ${key}", ("key", utilities::key_to_wif(nathan_key)));
  84       graphene::chain::genesis_state_type initial_state;
  85       initial_state.initial_parameters.current_fees = fee_schedule::get_default();//->set_all_fees(GRAPHENE_BLOCKCHAIN_PRECISION);
  86       initial_state.initial_active_witnesses = GRAPHENE_DEFAULT_MIN_WITNESS_COUNT;
  87       initial_state.initial_timestamp = time_point_sec(time_point::now().sec_since_epoch() /
  88             initial_state.initial_parameters.block_interval *
  89             initial_state.initial_parameters.block_interval);
  90       for( uint64_t i = 0; i < initial_state.initial_active_witnesses; ++i )
  91       {
  92          auto name = "init"+fc::to_string(i);
  93          initial_state.initial_accounts.emplace_back(name,
  94                                                      nathan_key.get_public_key(),
  95                                                      nathan_key.get_public_key(),
  96                                                      true);
  97          initial_state.initial_committee_candidates.push_back({name});
  98          initial_state.initial_witness_candidates.push_back({name, nathan_key.get_public_key()});
  99       }
 100
 101       initial_state.initial_accounts.emplace_back("nathan", nathan_key.get_public_key());
 102       initial_state.initial_balances.push_back({nathan_key.get_public_key(),
 103                                                 GRAPHENE_SYMBOL,
 104                                                 GRAPHENE_MAX_SHARE_SUPPLY});
 105       initial_state.initial_chain_id = fc::sha256::hash( "BOGUS" );
 106
 107       return initial_state;
 108    }

然后在启动时, global_property_object::chain_parameters::fee_schdule 会用创世信息中的 inital_parameters::current_fees 初始化自己; 后续创建打包交易使用的费用信息都是从 global_property_object::chain_parameters::fee_schdule 获得, 各个操作自己的 struct fee_parameters_type {} 不再被使用.

交易费用的设置

设置交易费用一般发生在交易签名之前, 如果交易中包含多个操作, 每个操作的费用都会被计算并设置:

// 代码 1.2
// libraries/wallet/wallet.cpp

 501    void set_operation_fees( signed_transaction& tx, const fee_schedule& s  )
 502    {
 503       for( auto& op : tx.operations )
 504          s.set_fee(op);
 505    }

fee_schedule::set_fee(op) 方法以操作为参数, 负责设置每个操作的费用. set_fee() 首先调用 calculate_fee() 设置计算操作的费用, calculate_fee() 这里用到了一个 calc_fee_visitor, 这个 visitor 以 fee_scheduleop 为参数, 就是用 op 的计费方法以及 fee_schedule 的计费参数计算费用. calc_fee_visitor 里有一个 try … catch (代码 1.4) 可能不好理解, 这里的 try … catch 是因为 fee_schedule 这块代码有点问题, 除了 opaccount_create_operation 之外, 其它情况下 param.get<OpType>() 都会抛异常, 这点感兴趣可以看一下 fee_schedule 的源码便知原因.

// 代码 1.3
// libraries/chain/protocol/fee_schedule.cpp

133    asset fee_schedule::calculate_fee( const operation& op, const price& core_exchange_rate )const
134    {
135       auto base_value = op.visit( calc_fee_visitor( *this, op ) );
136       auto scaled = fc::uint128(base_value) * scale;
137       scaled /= GRAPHENE_100_PERCENT;
138       FC_ASSERT( scaled <= GRAPHENE_MAX_SHARE_SUPPLY );
139       //idump( (base_value)(scaled)(core_exchange_rate) );
140       auto result = asset( scaled.to_uint64(), asset_id_type(0) ) * core_exchange_rate;
141       //FC_ASSERT( result * core_exchange_rate >= asset( scaled.to_uint64()) );
142
143       while( result * core_exchange_rate < asset( scaled.to_uint64()) )
144         result.amount++;
145
146       FC_ASSERT( result.amount <= GRAPHENE_MAX_SHARE_SUPPLY );
147       return result;
148    }

150    asset fee_schedule::set_fee( operation& op, const price& core_exchange_rate )const
151    {
152       auto f = calculate_fee( op, core_exchange_rate );
153       auto f_max = f;
154       for( int i=0; i<MAX_FEE_STABILIZATION_ITERATION; i++ )
155       {
156          op.visit( set_fee_visitor( f_max ) );
157          auto f2 = calculate_fee( op, core_exchange_rate );
158          if( f == f2 )
159             break;
160          f_max = std::max( f_max, f2 );
161          f = f2;
162          if( i == 0 )
163          {
164             // no need for warnings on later iterations
165             wlog( "set_fee requires multiple iterations to stabilize with core_exchange_rate ${p} on operation ${op}",
166                ("p", core_exchange_rate) ("op", op) );
167          }
168       }
169       return f_max;
170    }
// 代码 1.4
libraries/chain/protocol/fee_schedule.cpp

 78    struct calc_fee_visitor
 79    {
 80       typedef uint64_t result_type;
 81
 82       const fee_schedule& param;
 83       const int current_op;
 84       calc_fee_visitor( const fee_schedule& p, const operation& op ):param(p),current_op(op.which()){}
 85
 86       template<typename OpType>
 87       result_type operator()( const OpType& op )const
 88       {
 89          try {
 90             return op.calculate_fee( param.get<OpType>() ).value;
 91          } catch (fc::assert_exception e) {
 92              fee_parameters params; params.set_which(current_op);
 93              auto itr = param.parameters.find(params);
 94              if( itr != param.parameters.end() ) params = *itr;
 95              return op.calculate_fee( params.get<typename OpType::fee_parameters_type>() ).value;
 96          }
 97       }
 98    };

calculate_fee 算出费用后, 便会调用 op.visit(set_fee_visitor(f_max)) 将具体费用设置到操作中, set_fee_visitor() 很简单, 就是将 f_max 赋值给操作的 fee 成员, 是的, 每个操作都有一个 fee 成员.

另外在 fee_schedule::set_fee 代码中还考虑到 core_exchange_rate 的变动而多循环执行了几次费用计算, 以达到费用更精确的目的.

// 代码 1.5
// libraries/chain/protocol/fee_schedule.cpp

100    struct set_fee_visitor
101    {
102       typedef void result_type;
103       asset _fee;
104
105       set_fee_visitor( asset f ):_fee(f){}
106
107       template<typename OpType>
108       void operator()( OpType& op )const
109       {
110          op.fee = _fee;
111       }
112    };

至此, 这笔操作的交易费用就被计算并设置到了操作的成员变量中.

Graphene 源码阅读 – RPC 篇 – API 注册机制

API 注册这部分感觉又是 BM 炫技的部分, 之前数据库索引篇的各种泛型, 奇异模板, 递归模板已经让我抓狂了一次, 没想到 api 注册本以为很直观的东西, 竟然也搞的那么复杂, 真是累觉不爱, 本篇不做详解, 只陈述一下概念和流程.

Graphene 的 api 被分为 login_api, database_api, network_broadcast_api, history_api, asset_api 等几类, 除了 database_api 之外都定义在 <app>/api.hpp 中, database_api 可能会因为 api 数太多所以单独放在 <app>/database_api.hpp 中.

下面自顶向下, 看一看 api 注册经过了哪些模块, 是如何被注册的.

websocket_server 与 websocket_connection

websocket_server 的作用显而易见, 它负责监听在 RPC 服务端口上, 接受客户端连接并响应客户端请求. 每当新的连接到来就会创建一个 websocket_connection 实例, 这个实例用来后续与对应的客户端通信, 这和我们所了解的原生 socket 编程中 accept 返回与客户端通信的 socket 是一样的.

websocket_server
             |
             |
             | on_connection (创建 websocket_connection 用于和客户端通信)
             |
             | websocket_connection 包含 websocket_api_connection
            V
websocket_api_connection (register login_api, database_api) 

websocket_api_connection

上面的 websocket_server, websocket_connection 是对 websocketapp 的直接封装, 而 websocket_api_connection 则是 fc 嫌 websocket_connection 的 “状态” 表现不丰富而定义的一个类, 它作为 websocket_connection 的数据成员, 扩充了每个连接的 “状态” 信息, 在 bitshares 的 RPC 通信过程中客户端后面所能够调用的 api 可能随前面所调的 api 影响, 这些状态当然是业务相关的, 而不是 websocket 协议相关的, 所以需要 websocket_api_connection 这个扩展类来做.

一个比较典型的例子就是, 连接建立后客户端首先调用 login_apidatabase 方法来开启 database_api 的访问 (注意这里说的不是 api 访问权限, 那是另一个问题), 其次才能调用 database_api 下的各个方法.

>>> {"id":1,"method":"call","params":[2,"get_chain_id",[]]}
{"id":1, "result":"error!"}

>>> {"id":1,"method":"call","params":[1,"database",[]]}
{"id":1, "result":2}

>>> {"id":1,"method":"call","params":[2,"get_chain_id",[]]}
{"id":1, "result":"correct chain id"}

websocket_api_connection 的核心成员如下:

fc::http::websocket_connection&          _connection;                                  // 指向包含它的 websocket_connection 对象
fc::rpc::state                                        _rpc_state;                                    // 通信状态, 包括 request, response, 递增请求 id

/// 下面这俩继承自父类 api_connection
std::vector< std::unique_ptr<generic_api> >                      _local_apis;       // 存储所有注册进来的 api 们
std::map< uint64_t, api_id_type >                                _handle_to_id;       // api 的 handle 实际上是 api 实例的指针, api 的 id 就是它注册进 _local_apis 的序号. 目前只在 register_api 时查重用, 不必做太多关注

fc::rpc::state

websocket_api_connection 的成员 _rpc_state 维护了与客户端通信的 request/response 队列, 以及消息 id 的自增. websocket_api_connection 在构建时就会调用 _rpc_stateadd_method 方法, 添加三个方法, 分别是 “call”, “notice”, “callback”. 这三个字段就是我们在抓包时看到的 {"id":1,"method":"call","params":[2,"get_chain_id",[]]} 中的 method 字段的值.

这三个方法的 handlers 分别是三个 lambda 定义的回调函数, 在 “call” 的回调函数中, 会解析 rpc json 消息中的 params 字段, 取出 api_id, 方法名和参数去掉用实际的 api, 这是一个复杂的反射过程, 后面会介绍.

fc::api 与 generic_api

websocket_api_connection 除了调用 _rpc_state 添加那三个方法外, 还会负责注册一下 login_api — 因为总得让客户端有最初可调用的 api 不是嘛!

注册由 websocket_api_connection::register_api 方法负责, 但是在注册之前, login_api, database_api 等 api 需要用 fc::api 包装一下, fc::api 中定义了一些宏, 为每个 api 定义了 vtable 类型, vtable 里定义了每个 api 的 visit 函数, visit 函数会将 api 中的方法们用传入的 visitor 问候个遍. fc::api 还重载了 -> 操作符, 使得对 fc::api 的调用都会变成对对应的 vtable 的调用. 被包装过的 login_api 记做 fc::api<login_api>.

register_api 会将 fc::api<login_api> push 到上面说的 _local_apis 字段中, 但是, 你也看到了 _local_apis 是个向量, 成员类型是 generic_api. 是的, 这里还有一层转换, 就是 fc::api<login_api>generic_api 的转换.

我们先来看 generic_api 的核心成员们:

fc::any                                                 _api;                                   // 指向实际的 api, 比如 login_api
std::vector< std::function<variant(const variants&)> >  _methods;    // api 中的方法们
std::map< std::string, uint32_t >                       _by_name;              // 记录方法名 => 方法 id 的映射, 方法 id 实际上就是

generic_api::api_visitor 子类型                                                        // 子类中包含反指向 generic_api 的成员

然后再来看这步转换, 转换在 generic_api 的构造函数中可以窥知一二, 在这里 fc::api<login_api> 的 visitor 接口被调用, 传入的 visitor 是 generic_api::api_visitor 这个访问者, 这个访问者会将 fc::api<login_api> 中的方法们塞入 _methods 字段, 但是, 你又看到了, _methods 的元素类型是 std::function<variant(const variants&)>, 这里又涉及到一步转换, 就是讲 fc::api<login_api> 的方法们通过 to_generic 转换成 “通用方法”, 而 to_generic 是个模板函数, 其模板参数也很复杂, 看代码时要特别留意 api 下的各个方法对应哪个 to_generic.

比如说 login_api::database() 这个方法, 这个方法的签名是:

fc::api<database_api, Transform = identity_member> login_api::database()const

而在 fc::api 的宏作用下, 这个方法的签名会变成:

std::function<fc::api<database_api, Transform = identity_member>(Args...)> login_api::database()const

以这个函数签名做参数调用 to_generic 的话, 匹配的会是如下这个变种. 这里特别注意一下这个方法的最后一句是一个 register_api 调用, 这一句不是每个 to_generic 变种都有的, 只有 login_api 下那些返回 fc::api<xxxx> 的方法才会匹配到下面这个 to_generic 方法. 这一点很重要, 这体现了客户端通过调用 login_api 的各个方法来打开对其它 api 访问通道 (再提醒一句这里指的不是 api 的访问权限, api 访问权限由另外的逻辑保证).

393    template<typename Interface, typename Adaptor, typename ... Args>
394    std::function<variant(const fc::variants&)> generic_api::api_visitor::to_generic(
395                                                const std::function<fc::api<Interface,Adaptor>(Args...)>& f )const
396    {
397       auto api_con = _api_con;
398       auto gapi = &_api;
399       return [=]( const variants& args ) {
400          auto con = api_con.lock();
401          FC_ASSERT( con, "not connected" );
402
403          auto api_result = gapi->call_generic( f, args.begin(), args.end(), con->_max_conversion_depth );
404          return con->register_api( api_result );
405       };
406    }

结语

至此, 我们看到了, api 的注册实际上就是注册进了 websocket_api_connection 的 _local_apis 字段, 进而每个方法注册进了 generic_api 的 _methods 字段.

本文到此为止, 下文将介绍 websocket_server 是如何将收到的调用请求翻译成相应 api 的方法的.

Cifer

五月 13, 2018

我们常常需要根据程序运行的宿主机的 cpu 核心数来决定程序要创建的线程或进程, 在物理机上查看 cpu 核心数可以通过 /proc/cpuinfo, 而在 docker 上就不行了.

因为 docker 容器可以利用 --cpuset-cpus 选项配置可以使用的 cpu 核数, 而 /proc/cpuinfo 反应的却是物理机的情况, 所以就不能用它来获取实际 docker 运行时实际拥有的核数了, 需要通过其他方式来获得, 详情参见:

  1. https://github.com/moby/moby/issues/20770
  2. https://stackoverflow.com/questions/47545960/how-to-check-the-number-of-cores-used-by-docker-container/47547987#47547987

Cifer

五月 4, 2018

git submodule 的那几个命令每次查完都记不住, 所以这次就在这里好好思考一下其原理, 希望以后不要再忘了.

git submodule 的原理是在父模块中记录子模块的 commit, 并监控记录的 commit 和子模块的 HEAD 的差异.

父模块所记录的子模块 commit 属于父模块的版本信息, 这一信息的改变不会实际影响子模块的 HEAD. 当 cd 到子模块中做了一些更改提交, 或者父模块从远程 fetch 最新改动, 只要是会导致父模块记录的 commit 和子模块中 HEAD 不一样时, 父模块 git status 就会显示出这一差异, git submodule update 命令会将子模块的 HEAD 指向父模块中记录的 commit id, 从而抹平这一差异.

如果子模块嵌套了子模块, 可以用 git submodule update --recursive 递归更新所有嵌套的子模块的 HEAD.

如果想让子模块的 HEAD 更新到子模块的远程最新, 就用 git submodule update —remote, 这样一来子模块的 HEAD 一般往往也就比父模块记录的 commit 要新了, 父模块 git status 自然也会显示差异, 父模块可以提交这一信息将其记录在自身 — 这就是父模块 “携带” 某个特定版本子模块的方法.

Graphene 源码阅读 – 架构篇 – 见证人配置与启动

欢迎大家继续阅读, 看完了上一节 节点实例 这个公共组件, 这节我们就来介绍一下比特股系统中最重要的部分 — 见证人, 的配置与启动过程.

见证人配置

在 bitshares 中, 见证人功能是在 witness 插件里实现的, 类似的还有 cli_wallet, delayed_node 都是通过独立的插件提供, 这些插件的公共部分就是上一节我们介绍的 节点实例.

见证人在启动时依赖 config.ini 这个配置文件, 如果 config.ini 不存在会使用代码中的默认配置创建它, 实际上 config.ini 也是所有插件的配置文件, 但本节我们只关注见证人相关的配置, 配置和解释如下:

  • enable-stale-production
    正常情况下, 当见证人节点启动时, 会去其它节点同步区块, 当区块没有同步到全网最新状态时, 是不允许见证人生产区块的, 而此配置项置为 true 的话, 会无视这一规则.
  • required-participation
    DPoS 协议中有一个参与度的概念, 如果一条链参与度太低的话说明这条链已经不被很多见证人认可, 理应不该再在此链上出块. 此配置项就是用来设定参与度阈值.
  • witness-id
    此项配置的是当前节点实例要跑的是哪个见证人
  • private-key
    此项配置的是见证人的签名密钥

运行出块

见证人的运行就是作为一个调度单元, 每隔 1 秒就被调度起来生产区块的过程. 这个过程的执行流如下:

app::startup_plugin() => witness_plugin::plugin_startup() => schedule_production_loop() => block_production_loop() => maybe_produce_block()
                                                                                                               ^                                                 |
                                                                                                               |____________________________|

这里面很重要的一个工作就是 maybe_produce_block(), 这个方法的返回结果决定了要不要出块, 我们重点看下这个方法.

首先会检查区块是否同步到最新, enable-stale-production 配置项会影响这个判断, 如果 enable-stale-production 设为 true 了, 那这里的 _production_enabled 就会是 true.

217    // If the next block production opportunity is in the present or future, we're synced.
218    if( !_production_enabled )
219    {
220       if( db.get_slot_time(1) >= now )
221          _production_enabled = true;
222       else
223          return block_production_condition::not_synced;
224    }

接下来这段代码会判断是否到达出块时间, 关于这段代码看似简短其实背后也有文章, 我们在 出块判断逻辑 一文中有过一次稍微详细的介绍, 感兴趣的可以去看看. 这里仅补充一下为什么会没达到出块时间.

实际上原因也很简单, 因为见证人的调度是每秒都会调度的, 然而出块时间却可能是 3s, 10s 甚至是 20s, 所以见证人被调度执行时, 自然是可能还没有到达出块时间的.

226    // is anyone scheduled to produce now or one second in the future?
227    uint32_t slot = db.get_slot_at_time( now );
228    if( slot == 0 )
229    {
230       capture("next_time", db.get_slot_time(1));
231       return block_production_condition::not_time_yet;
232    }

如果出块时间也满足的话, 会继续判断这轮是不是轮到自己出块, 这部分涉及到 DPoS 见证人洗牌相关问题, 我们留到共识篇再详细讨论.

244    graphene::chain::witness_id_type scheduled_witness = db.get_scheduled_witness( slot );
245    // we must control the witness scheduled to produce the next block.
246    if( _witnesses.find( scheduled_witness ) == _witnesses.end() )
247    {
248       capture("scheduled_witness", scheduled_witness);
249       return block_production_condition::not_my_turn;
250    }

再继续会相继检查是否有配置签名私钥以及当前主链的参与率是否达到要求, 这就是上面说的 private-key 以及 required-participation 配置项的作用.

252    fc::time_point_sec scheduled_time = db.get_slot_time( slot );
253    graphene::chain::public_key_type scheduled_key = scheduled_witness( db ).signing_key;
254    auto private_key_itr = _private_keys.find( scheduled_key );
255
256    if( private_key_itr == _private_keys.end() )
257    {
258       capture("scheduled_key", scheduled_key);
259       return block_production_condition::no_private_key;
260    }
261
262    uint32_t prate = db.witness_participation_rate();
263    if( prate < _required_witness_participation )
264    {
265       capture("pct", uint32_t(100*uint64_t(prate) / GRAPHENE_1_PERCENT));
266       return block_production_condition::low_participation;
267    }

最后一项要检查的是当前要出的块的理论出块时间和当前实际时间相差多大, 如果相差了 500ms 以上, 那也不会允许出块, 如果你运行过见证人, 也许有看到过

Not producing block because node didn't wake up within 500ms of the slot time.

这样的错误, 这就是我们所说的 miss 了.

当上述所有检查都没有问题就会执行出块了. 出块部分我们留作后续讨论.

Graphene 源码阅读 – 架构篇 – 节点实例启动流

大家好, 本文正式开启架构篇.

如果你还有印象的话, 我们之前在 源码结构 中曾简要的介绍过 bitshares 源码中的每个模块, 这个篇章将会和大家探讨各个模块之间的调用关系, 节点运行时模块之前如何交互, 逐步为大家呈现出一条清晰地脉络.

与整个节点的运行最相关的要数 app 模块了, 所以 app 模块就作为架构篇的第一个模块在本文介绍. 下文中 app 模块我们将其称之为节点实例模块或者直接成为节点实例, 这个名字将更能够表达此模块的含义.

节点实例

节点实例模块在整个系统中无疑是位于顶端的, 系统中各个组件都是由它触发激活并监控他们的状态. 节点在启动时会调用 app::initialize() 以及 app::startup() 方法, app::initialize() 中主要的工作就是注册插件到自己的静态变量中, app::startup() 才包含了节点启动的主要流程.

下面我们就来详细看看节点启动流程.

节点实例启动

genesis 加载进内存

第一步就是加载创世信息 genesis.json, 如果你没指定 —genesis-json (如果你要跑主链的话也不需要指定), egenesis 组件会负责加载默认创世信息 (参见 默认的 Genesis 创世信息), 创世信息加载进内存后会被存储在 genesis_state_type 类对象中, genesis_state_type 类的定义和 genesis.json 中的字段是一一对应的, 只有一个例外是 genesis_state_type 中包含一个 initial_chain_id 字段, 这个字段是在创世信息全加载进内存后对这块内存求哈希算出来的, 所以在 genesis.json 中没有对应.

initial_chain_id 是很重要的字段, 它被用来标识不同节点所运行的是不是同一条链. 所以现在我们知道了, 所以只要不同节点使用相同的创世信息, 它们跑的就一同一条链; 而你如果修改哪怕只是创世信息中的一个微不足道的字段, 其 hash 值都会改变, 你也将不能与其它节点通信 – 实际上这已经创建了一条新链.

数据库打开

如果看过之前的数据库篇, 想必对这个过程会亲切一些, 没错, 这个过程做的就是从磁盘加载对象索引, 打开区块数据文件流, 必要的话还会重建部分对象索引.

另外要注意的是, 如果是节点的第一次启动, 数据库打开这个过程的末尾还会对上面加载的创世信息进行 apply, 这个工作很重要, 创新信息的很多内容和正常链上内容无二, 所以它也要上链, 总不能每次启动节点都将它是放在内存里的. init_genesis() 方法就是做这个工作的.

init_genesis

init_genesis() 在节点第一次启动时被调用, 它读取前面加载到内存中的创世信息并 apply, 这其中包含了大量的工作, 我们选一些主要的说明一下:

创建特殊账户

这个过程会创建区块链上的一些特殊账户对象, 并且是不用走检验直接在索引上创建, 用的是数据库篇说过的 object_database::create() 方法

特殊账户的个数由创世信息的 immutable_parameters.num_special_accounts 字段指定, 对目前的 bitshares 主链来说这个值是 100.

首先创建的是 committee-account, witness_account, relaxed-committee-account, null-account, temp-account, proxy-to-self 这 6 个特殊账户, 这六个账户的 id 依次为 1.2.0 ~ 1.2.5.

实际上目前特殊账户也就这 6 个, 创世信息中指定的 100 个是为了预留以备不时之需. 紧接着要为剩下的 94 个特殊账户预留位置, 预留位置的方式很简单粗暴, 就是创建 94 个账户然后再立马把它们删除, 这样索引结构中 6 ~ 99 号 id 就也被占用了, 新创建的账户将只能从 100 号开始, 如果你用 bitshares 的区块浏览器查看, 就会发现从 1.2.6 ~ 1.2.99 这 94 个账户至今还是空的.

数据库篇 ~ 索引模型 中我们说到过创建对象时有个 set_next_id() 方法可以显式指定要创建的对象的 id, 所以将来要创建 6 ~ 99 号账户的话, 就可以发起一个账户创建交易, 其中指定账户的 id.

创建核心资产与特殊资产

核心资产也就是 BTS 资产, 特殊资产是一些名字以 “SPECIAL_” 打头的资产, 估计也是留作备用. 它们的创建套路和创建初始账户一致, 不再敖述.

创建全局对象

包括全局属性对象 (global_property_object), 动态全局属性对象 (dynamic_global_property_object), 链属性对象 (chain_property_object), 以及区块总结对象 (block_summary_object).

创建初始账户

上面创建的特殊账户是不属于个人的账户, 这里创建的 “初始账户” 实际上都是一些个人账户了, 这些账户可以说是这条链上的 “创世账户”.

不过这些账户毕竟是个人账户, 这些账户都是当初社区最早的那批参与者们, 账户信息也都是他们自己提供的. 既然是人提供的信息, 那就不是 100% 可信的, 所以这些账户的创建不能像特殊账户那样直接创建加索引, 而是要走操作创建, 检验上链的过程, 所以创建这些账户时会看到用的是 apply_operation 方法.

初始账户们由创世信息的 initial_accounts 字段指定, 总共有 90653 个账户.

创建初始资产

和初始账户一样, 初始资产也是一些 “个人” 资产. 创建过程和初始账户一样, 不再敖述.

其它

其它的还有创建初始余额, 初始见证人, 委员会, 提案, 套路和上述一致.

顺便说一句, 初始账户, 初始资产, 初始余额这几个信息几乎是占据了默认创世信息的全部, 默认创世文件 genesis.json 总共 35M 之大, 而其中 99.99% 都是这级部分信息贡献的.

p2p 网络与 rpc 服务初始化

数据库搞好之后, 下一步就是 p2p 网络的建立以及 rpc 服务的启动.

篇幅关系, 这两部分留到后面继续介绍吧, 敬请期待.

C++ 的左右值与左右值引用

左值与右值

C++ 中左值和右值的概念来源于 C, 在 C 中左值和右值的区别很简单, 能出现在赋值号左侧的就是左值, 否则就是右值. 比如变量是左值, 字面常量或者 const 定义的常量是右值.

然而在 C++ 中, 左值和右值的区别就不再是那么简单了, 甚至和 C 还会有冲突, 比如在 C 中 const 定义的常量对象是右值, 而在 C++ 中却是左值.

实际上在 C++ 中左值和右值的情况非常复杂, 有时区分他们也是非常困难的. Scott Meyers 大师在其 Effective Modern C++ 一书所说的不失为一个好方法, 在理解这句话之前, 我们一定要有一个意识, 就是左值和右值是表达式的属性, 代表着表达式的运算结果是左值还是右值.

A useful heuristic to determine whether an expression is an lvalue is to ask if you can take its address. If you can, it typically is. If you can’t, it’s usually an rvalue. A nice feature of this heuristic is that it helps you remember that the type of an expression is independent of whether the expression is an lvalue or an rvalue.

也就是说, 表达式是左值还是右值, 就看这个表达式的结果是否在内存有对应的存储区域, 有的话就是左右, 没有的话就是右值 (TODO: 补充例子), 这点也可以参考 c++ primer 5th, p121.

另外还有两个有重要的准则:

  1. 对象 (对象是最简单的表达式) 被用作左值时, 用的是对象的地址; 被用作右值时, 用的是对象的值
  2. 在需要右值的地方, 可以用左值代替 (因为地址中存储着值), 这时左值被当成右值使用, 用的是左值里存储的值. 这条准则只有一个例外, 就是左值引用不能当做右值引用使用 (下面会讲到引用)

引用

在 C++11 以前, 引用就是对象的别名, 只能绑定到左值上, 并且一旦绑定到了某个对象上就永远绑定了, 不能通过赋值改变引用绑定的对象, 因此在定义引用时必须初始化.

定义引用绑定到对象时, 一般是要求所绑定到的对象的类型和引用的类型必须一致, 不过也有两个例外:

  1. 定义的基类引用可以用派生类对象初始化 (c++ primer 5th, p534)
  2. 常量引用 (对 const 的引用) 可以用任意表达式作为其初始值, 只要该表达式的结果能转换成引用类型即可 (c++ primer 5th, p55)

上面说的引用只能绑定到左值上, 在 C++11 中扩展了引用的概念, 允许引用绑定到右值表达式这样的右值上, 这新引入的引用类型被称为右值引用, 对应的之前的引用被称为左值引用.

那么右值引用有什么用呢? 如果仅仅是让我们能够绑定到字面常量这样的右值的话, 刚刚我们也看到了, 通过常量(左值)引用我们同样也能够绑定到字面常量等右值表达式上, 为什么还要引入右值引用呢?

然而右值引用不仅仅是让我们能够不借助 const 就能引用右值表达式, 它还支持了对象移动以及完美转发这两个重要的能力. 关于这两个能力就是另外的话题了, 我们简单介绍一下对象移动.

对象移动

对象移动的目的是减少对象拷贝, 被移动的对象内容会被 “掏空”, 赋予新对象中, 这个操作依赖于 “移动构造函数” 或者 “移动操作符”, 然而如何定义这两个方法却成了一个问题, 我们知道拷贝构造函数和赋值操作符是以对象的引用 (左值引用) 为参数的, 而如果要定义移动构造函数或移动操作符, 其参数当然也得以代表对象的某种形态为参数, 对象的引用这种形态已经不能用了, 对象的非引用更不能用 (原因见 c++ primer 5th, p422), 那么只能新发明一中语义了, 这个新的语义就是右值引用, 写作 T &&. 于是移动构造函数就可以这么定义了:

Foo (Foo && other);

如此一来, 只要传递给构造函数的是右值引用, 就会调用移动构造函数, 免去拷贝所造成的开销.

然而如何传递右值引用呢? 当发生如下情况时, 该调哪个构造函数又成了一个问题:

Foo (const Foo &other);
Foo (Foo && other);

Foo(foo);    // 该调哪个构造函数?

事实上由于拷贝构造函数先出现, 所以上面第 4 行的写法当然是会调拷贝构造函数的. 为了能够调到移动构造函数, 标准库提供了一个 std::move 方法, 这个方法总是返回右值引用, 因此我们只要这么写就能够调用移动构造函数了:

Foo(std::move(foo));

除了显式的通过 std::move 方法, 有些情况下编译器也能够自动判断是否可以调移动构造函数来降低开销, 比如编译器发现源对象是个临时对象.

关于右值引用带来的另一个功能完美转发就不再敖述, 可以参考文末链接 1 关于右值引用的提案.

参考

  1. 右值引用提案: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2004/n1690.html