精读《全链路体验浏览器挖矿》

精读《全链路体验浏览器挖矿》

本期精读的文章是: coinhive官方文档Monero官方文档

懒得看文章? 没关系.

咦, 怎么是官方文档?

本期精读有所不同, 注重实操, 先操作获取感性认识, 然后再介绍相关的概念, 由浅入深, 力求不纠缠细节, 但不遮盖环节. 阅读本文需要对加密货币有一些基本常识 (如果你是一个开发者但是完全没了解过加密货币, 可以参考这里). 希望同学们看完本文能对加密货币领域有一个更深更切实的体感. 如果要了解更多细节, 文末总结的延伸阅读链接列表是最好的开始.

要注意一点, 文中很多说明是默认基于XMR和BTC的, 他们两个又同源, 机制非常相似. 所以很多命题判断并不适用于所有的成千上万的加密货币. 正相反, 新的币种层出不穷, 几乎所有的惯例都被打破, 所有可能性都被尝试. 这一点以下不再做说明.

1 引言

首先, tl;dr干货. 10行代码, 5分钟, 不需部署不需构建直接浏览器开挖.

  • 本地创建文件test.html, 粘贴如下代码:
<!doctype html>
<html>
<head>
    <title>Mining</title>
</head>
<body>
    <div class="coinhive-miner" 
        style="width: 256px; height: 310px"
        data-key="MUtCJzIDhrs01ERrf3qlqdawo35N0CYD">
        <em>Loading...</em>
    </div>
    <script src="https://authedmine.com/lib/simple-ui.min.js" async></script>
</body>
</html>
  • 本地双击打开. 等待JS加载, 点击widget "Start Mining". 开始挖矿! 如下图


不错, 数字已经在跳动, 风扇开始工作, 永无尽头的挖矿已经开始了. 那么重要的是, 挖出来的加密货币在哪呢? 原来上面的代码里用的还是我的API key, 所以还没挖到你自己那里 :P 继续下面的步骤

  • 在coinhive注册账号并登陆. 它是做什么的? 别急, 后面会详细讲. 在coinhive/settings找到自己的API keypair, 把public key复制出来, 形如MUtCJzIDhrs01ERrf3qlqdawo35N0CYD
  • 替换上面代码中的data-key部分, 重新开始挖矿. 好了, 现在挖出的Monero (这是啥? 详见下一节) 已经会到你自己的coinhive账户中. 用下图来说明, 你名下总计算过的Hash个数为264K, 当前难度换算为0.00002个Monero(Symbol:XMR). 当前难度为66G一个block, 一个block reward 5.87 XMR, 得到一个XMR是11.268G. 264K/11.268G = 0.0000234. 这就是你目前的收益.


  • 查bitfinex可得现在XMR价格在375美元 (当你看到本文的时候, 价格可能早就又波动到不知哪里去了), 所以你 (以及你忠实勤劳的电脑) 获得的实际收益为0.000234 * 375 USD = 0.008775 USD, 快到一分钱了 :)

怎么样, 有没有一种浏览器点开即玩一刀999级的感觉. 以上操作的便捷直接, 是建立在无数前人大量的开发和基础设施建设之上的. 越是领域早期的工作, 越步履维艰, 收货也越丰厚. 如今加密货币已经走到了一个成年期, 逐渐稳定成熟起来.

接下来我们聚焦到上面过程的每个环节, 了解下拼图的每一块是怎样被构成全图的.

2 聚焦

让我们从最终端最接近用户的环节开始, 逐一聚焦, 最后走完整条链路.

2.1 从浏览器说起

本文标题叫浏览器挖矿, 也是和贴合前端的部分. 那么为什么可以在浏览器里挖矿? 为什么可以很多用户在多个终端浏览器同时为同一个人 (你) 挖矿?

我们知道, 挖矿是对加密货币产生机制的俗称. 主流大多采取Proof of Work (PoW) 机制. 最常见的PoW方式就是由网络中所有节点作为矿工, 每个节点都基于blockchain前面block已有信息计算一个新信息. 这个新信息的计算方式往往是某种hash function, 并且人为地被设置为需要巨大计算力和时间才能完成 (其具体难度一般也会实时调整). 当一个节点幸运地 (也依靠强大的算力) 第一个计算出结果后, 会把这个结果广播到网络中. 其他所有节点会验证这个结果 (我们知道非对称加密算法, 验证便宜而计算昂贵), 一旦证实就会停下手里的计算, 承认这个计算结果. 新的计算结果创造出新的块, 区块链的高度增加一层, 然后计算继续下去. 每一个块的生成一般在2-10分钟. 这个过程就被叫做挖矿.

既然是通用计算, 既然是算一个hash值, 那么民用级CPU和GPU, 浏览器或任何沙盒, 虚拟机, 移动设备当然就都可以. 在我们的例子中, 计算过程被做成分布式, 每个用户可以各自计算, 结果按chunk发回master汇总. 这样就实现了终端用户 - 浏览器 - 共同贡献计算资源 - 换为XMR的过程.

这就是对整个链路的一个描述. 从中我们会生出一些疑问, 比如:

给我看看具体算什么hash? 为什么要算XMR而不是比特币或者其他? 既然第一个算出的赢家通吃所有, 为什么我的收益却是线性的? 这种描述来看岂不是算力最大的一方永远都能算出结果而其他人颗粒无收吗?

要看具体算法, 没有问题. bitcoin在这里, XMR则看CryptoNote Standard 008

读完两个算法我们就有了以上疑问的答案:

2.1.1 为什么要算XMR而不是比特币或者其他

XMR不是唯一选择, 但是BTC是一个不可选的选择. 因为double SHA-265在专业级GPU上会比CPU上快10^4倍 (更多信息). 这样一百万用户合力浏览器挖矿还不如一架子双路Titan, 就失去了分布到终端用户的意义. 而CryptoNight在GPU上只比同价值CPU快2倍. 另外CryptoNight算法也被设计为不适用ASIC.

怎么实现的? CryptoNight算法开宗明义地写明, 运算主体是Memory-Hard Loop, 而不是Computation-Hard Loop. 每个循环都要在内存中检索. 实际运行CryptoNight时, CPU都会用最快, 最接近ALU也是容量最小的L3 Cache. 换到GPU, 显存虽然很大, 却没有L3 Cache一样的极致读写速度优化, 而且由于内存读写成了瓶颈, GPU中的大量ALU也没了用武之地. 下图简略地描述了CryptoNight循环体的结构:



2.1.2 算力最大的一方永远都能算出结果

看了比特币具体算法, 你应该明白了hash是靠不停改变nonce来生成的. 随机取一个值, 算了不对, 再随机取另一个nonce值... 既然是随机取, 就不会存在赢家恒赢.

2.1.3 为什么我的收益却是线性的

这是一个隐蔽但是却非常重要的问题. 答案是, 本来确实是赢家通吃. 如果你的算力足够大, 挖矿时间足够长之后总会轮到你, 但是收益会有大幅波动.

就是因为如此, 矿工们逐渐建立了矿池组织. 大家把算力都投入到一起, 合力算, 然后不管这次实际是谁算出来, 都按照贡献的算力比例分配收益. 矿池是一个加密货币建立之初, 完全推崇去中心化时没有预料到的结构, 也产生了深远的影响. 现在来自中国矿池的算力早已超过网络50%, 他们会在挖出的块中打上矿池标记, 而这些矿池在加密货币的分叉, 路线图中也扮演举足轻重的角色.

所以你的算力并不是直接投入XMR网络中, 而是投入一个矿池. 在我们的例子里矿池就是coinhive, 只不过是一个比较特殊的矿池, 特殊在矿池成员都运作在浏览器中. 这就是为什么你会得到线性收益而不是all or none.

自古以来各行业都会自发产生行业工会, 建立类似保险和行业守则 / 规范这些人人为我我为人人的机制. 在crypto行业也不例外. 这是意料之外而情理之中.

2.2 在浏览器里发生了什么, 或, coinhive干了什么

好, 我们搞清了一些基本的Monero挖矿机制, 下面来看看coinhive. 已经知道coinhive帮我们接入它的矿池, 让再小的算力也能按比例得到产出. 但是还有什么呢? 最关键的一点, coinhive是怎样把一个完整的mining过程拆分成小块, 让一个或许并不强大的设备上的浏览器, 也能快速接收task, 快速完成并且即时上传的呢?

废话不多说, 打开源码. 本项目没有开源, 构建完成后的在coinhive.com/lib/coinhi. 先做初步format处理, 发现有些工作完成在后端, worker shard一侧. 以下用松散的伪码总结一下client side的main success scenario流程 (注意很多地方简化了):

 - start
  - loadWorkerResource
  - load worker-asmjs.min.js
  - CRYPTONIGHT_WORKER_BLOB = createObjectURL(Blob(response_of_worker-asmjs.min.js))
  - _startNow -> _connectAfterSelfTest
  - selfTest -> verify(testJob),
    testJob = {
      verify_id: "1",
      nonce: "204f150c",
      result: "6a9c7dea83b079ce0e012907dd6929bcb0aeec3c1f06c032ca7c3386432bca00",
      blob: "0606c6d8cfd005cad45b0306350a730b0354d52f1b6d671063824287ce4a82c971d109d56d1f1b00000000ee2d1d4fd7c18bdc1b24abb902ac8ecc3d201ffb5904de9e476a7bbb0f9ec1ab04"
    };
    verify = if (!this._isReady) {
        this.verifyJob = job
    } else {
        this.worker.postMessage(job)
    }
    // 实例化若干个JobThread, 每个对应一个worker, worker实际执行asmjs.min.js
  - _connect // verify成功, 终于建立连接. 根据public key固定hash到一个shard池然后随机选一个shard, 建立websocket
  - websocket.onmessage: if (type==job) work()
  - work:
    do { hash(input, output) } while !(meetTarget(output));
    websocket.postMessage({nonce, output}) // hash done successfully. submit
 

看一下这个过程, 结合cns003 XMR blockchain specs. XMR的整体hash input很小, 是:

- size of [block_header, Merkle root hash, and the number of 
    transactions] in bytes (varint)
  - block_header,
  - Merkle root hash,
  - number of transactions (varint).

这样用websocket发过来毫无问题. 之后就是完全独立的计算, 调整nonce来算不同的hash结果. target就是当前难度的一个指示.

这样整条链路就比较清晰了. 再思考一下以下问题:

2.2.1 为什么XMR适宜分布式客户端计算

因为能够利用每个用户的CPU和其中的高速L3 Cache. 这是中心化执行难以具备的条件.

任何时候当考虑要不要把某项操作推到客户端进行时, 都要想明白可以利用客户端的哪个资源, 这个资源在客户端是否有明显优势, 是否比后端中心化执行更有利. 很多时候答案是, 优势并不明显. 那么引入的网络通信成本, 法规成本, 额外开销可能就并不值得.

2.3 最后的步骤

到了这里, 最后剩余步骤就很标准模式化了. coinhive作为矿场, 代管着用户生产的加密货币. 用户发请求提出XMR, 就需要提到一个自己的钱包地址保管, 比如MyMonero. 也可以直接提到Exchange交易所, 在其中交易成其他币种, 包括法币, 然后电汇等等形式提现.

如果对钱包或者交易所感兴趣 (这两个也是很大的话题, 比如钱包分硬件钱包和软件钱包, 离线冷钱包和线上热钱包, private key和recover seeds. 交易所有多种多样的交易对, 有杠杆, 期货, 空和多, 多种挂单类型等等), 可以看这里这里.

3 更多讨论

讨论地址是:精读《全链路体验浏览器挖矿》 · Issue #55 · dt-fe/weekly

如果你想参与讨论,请点击这里,每周都有新的主题,每周五发布。

4 延伸阅读

piotrpasich.com/introdu

en.wikipedia.org/wiki/M

righto.com/2014/02/bitc

cryptonote.org/cns/cns0 cns001-008是Specs集合

en.bitcoin.it/wiki/Why_

en.wikipedia.org/wiki/A

github.com/txbits/txbit

编辑于 2018-01-07

文章被以下专栏收录