<audio id="audio" title="第36讲 | 搭建你的迷你区块链（实践篇）" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/fa/80/fa62b36d3b16ed11fcc1eb54c5b0f480.mp3"></audio>

上一篇文章中，我们介绍了实现一个迷你区块链的大致思路。今天，我们将通过代码编写，以及简单的功能测试，来完成我们的迷你区块链Tinychain。

除了正常的测试案例之外，我们还可以构造一些极端测试案例，来观察Tinychain的分叉合并，挖矿难度调整等情况。

## 代码编写

通过前文的分析，我们已经了解到了实践一个迷你区块链的大致思路。接下来，我将从顶层到底层来搭建区块链。

### 代码编写1 Server

从链的顶层设计来看，我们需要一个入口，那么我们就从入口开始：我需要先为整个服务做一些基础设置，最后再来Server.run()。

所以，我们的代码大概是这样子的。

```
// server setup
node my_node;
mgbubble::RestServ Server{&quot;webroot&quot;, my_node};
auto&amp; conn = Server.bind(&quot;0.0.0.0:8000&quot;);
mg_set_protocol_http_websocket(&amp;conn);
log::info(&quot;main&quot;)&lt;&lt;&quot;httpserver started&quot;;
Server.run();

```

我们首先生成一个node实例，然后被Server装载进去，最后设置好Server启动。

这个Server主要有两个功用，第一是向本地用户服务，也就是接受命令行，接受本地RPC调用；第二是接受外部网络传送进来是的新交易，和新的区块。所以Server是整个节点的入口。

### 代码编写2 node

那么这里的node其实就是区块链的node，里面包含了区块链的基本设置，这些一般都是硬编码在代码中的，例如一般区块链都有个“魔法数”，实际上就是区块链ID，这个ID会被放在所有消息的开头，如果区块链ID不匹配，则抛弃接收到的消息。

这里的区块链ID我们设置在这里。

```
blockchain(uint16_t id = 3721)9273_(id) {
    id_ = id;
    create_genesis_block();
}

```

代码中所展示的id_就是区块链ID，在Tinychain的案例中，我也是硬编码的。

在一个node当中，至少要包含network、blockchain、miner三个模块。

```
public:
    void miner_run(address_t address);
    blockchain&amp; chain() { return blockchain_; }
    network&amp; p2p() { return network_; }

```

```
private:
    network network_;
    blockchain blockchain_;
    miner miner_{blockchain_};

```

network也就是P2P网络类，blockchain是区块链的核心类，miner是共识模块下的核心类，三者被聚合到node中。

同时，node也会提供一些blockchain和miner的接口，方便Server层调用。

### 代码编写3 blockchain

一个blockchain实例，应当包含下面的内容。

```
    uint16_t id_;
    block genesis_block_;
    chain_database chain_;
    key_pair_database key_pair_database_;
    memory_pool_t pool_;

```

genesis**block** 就是创世区块，这个是预先生成好的。genesis_block的信息也是被硬编码在代码中，我在Tinychain的例子为了方便测试，每个genesis_block都是可以自行生成的。

chain**database chain** 是相对于memory**pool而言的，chain**就是已经经过确认，并且在本地持久化存储的区块数据（由于时间有限，Tinychain的案例中还未实现持久化存储，可以后续升级替换）。

memory_pool 是指还未经过确认，暂时驻留在内存中的交易池，交易池中的交易会在挖矿时，被导入到新的区块中。

```
    // 装载交易
    new_block.setup(pool);

```

这里的pool就是交易池。

key_pair_database 是指专门存储用户的私钥的数据库，同时提供私钥管理。

同时blockchain也负责统一对外提供上述功能的接口。

```
    // 获取当前节点高度
    uint64_t height() { return chain_.height(); }
    // 获取当前节点最新区块
    block get_last_block();
    // 查询指定区块
    bool get_block(sha256_t block_hash, block&amp; out);
    // 查询指定交易
    bool get_tx(sha256_t tx_hash, tx&amp; out);
    // 查询目标地址的余额
    bool get_balance(address_t address, uint64_t balance);
    // 获取当前区块链的ID
    auto id() {return id_;}
    // 获得交易池数据
    memory_pool_t pool() { return pool_; }
    // 区块打包成功以后，用于清空交易池
    void pool_reset() { pool_.clear(); }
    // 从网络中收集未确认的交易到交易池
    void collect(tx&amp; tx) {
        pool_.push_back(tx);
    }

    void merge_replace(block_list_t&amp; block_list)；

```

除了上述接口之外，blockchain还负责当发现自己处于较短的分叉链上时，自动合并到最长链。

### 代码编写4 network

在network中，可用的地址簿代表了可用的其他对等节点，至少是连接过成功一次的。

```
public:
    void broadcast(const block&amp; block);
    void broadcast(const tx&amp; transaction); 
    void process(event_t ev, func_t f);

```

```
private:
    endpoint_book_t book_;
    channels_t channels_;

```

地址簿会随着网络的变化进行更新，实时状态的地址簿是驻留在内存中的，当节点关闭是，会被刷到持久化存储中。

channels代表了已经激活的连接，这些连接可以被broadcast接口使用，当本地节点产生新的区块和交易时，会调起这些channels。

当P2P网络产生了新的事件时，会通过process接口处理新到达的交易和区块，这一事件会传导给blockchain模块。

### 代码编写5 consensus

consensus的含义为共识，共识会在两种情况下产生，第一是对本地生产的交易进行验证，第二是外来的区块和交易进行验证。

无论是哪种情况，他们遵循的验证规则是一样的。validate_tx和validate_block分别承担了这样的功能。

```
bool validate_tx(const tx&amp; new_tx) ;

bool validate_block(const tx&amp; new_block) ;

```

除了验证区块之外，还涉及到提供基础挖矿设施。我们知道挖矿分为两种，一种叫做solo挖矿，另外一种叫做联合挖矿。其实无论哪种挖矿类型，都必须用到miner类。

```
public:
    //开始挖矿
    void start(address_t&amp; addr);
    inline bool pow_once(block&amp; new_block, address_t&amp; addr);
    // 填写自己奖励——coinbase
    tx create_coinbase_tx(address_t&amp; addr);

private:
    blockchain&amp; chain_;

```

miner类展示了在solo挖矿情况下，支持开始挖矿以及计算自己的coinbase的过程。

实际pow_once的挖矿代码如下，pow_once被start调用，start里面是一个死循环，死循环里面包了pow_once函数。

```
bool miner::pow_once(block&amp; new_block, address_t&amp; addr) {

    auto&amp;&amp; pool = chain_.pool();

    auto&amp;&amp; prev_block = chain_.get_last_block();

    // 填充新块
    new_block.header_.height = prev_block.header_.height + 1;
    new_block.header_.prev_hash = prev_block.header_.hash;

    new_block.header_.timestamp = get_now_timestamp();

    new_block.header_.tx_count = pool.size();

    // 难度调整:
    // 控制每块速度，控制最快速度，大约10秒
    uint64_t time_peroid = new_block.header_.timestamp - prev_block.header_.timestamp;
    //log::info(&quot;consensus&quot;) &lt;&lt; &quot;target:&quot; &lt;&lt; ncan;

    if (time_peroid &lt;= 10u) {
        new_block.header_.difficulty = prev_block.header_.difficulty + 9000;
    } else {
        new_block.header_.difficulty = prev_block.header_.difficulty - 3000;
    }
    // 计算挖矿目标值,最大值除以难度就目标值
    uint64_t target = 0xffffffffffffffff / prev_block.header_.difficulty;

    // 设置coinbase交易
    auto&amp;&amp; tx = create_coinbase_tx(addr);
    pool.push_back(tx);

    // 装载交易
    new_block.setup(pool);
    // 计算目标值
    for ( uint64_t n = 0; ; ++n) {
        //尝试候选目标值
        new_block.header_.nonce = n;
        auto&amp;&amp; jv_block = new_block.to_json();
        auto&amp;&amp; can = to_sha256(jv_block);
        uint64_t ncan = std::stoull(can.substr(0, 16), 0, 16); //截断前16位，转换uint64 后进行比较

        // 找到了
        if (ncan &lt; target) {
            //log::info(&quot;consensus&quot;) &lt;&lt; &quot;target:&quot; &lt;&lt; ncan;
            //log::info(&quot;consensus&quot;) &lt;&lt; &quot;hash  :&quot; &lt;&lt; to_sha256(jv_block);
            new_block.header_.hash = can;
            log::info(&quot;consensus&quot;) &lt;&lt; &quot;new block :&quot; &lt;&lt; jv_block.toStyledString();
            log::info(&quot;consensus&quot;) &lt;&lt; &quot;new block :&quot; &lt;&lt; can;
            return true;
        }
    }

```

上面的代码从一开始到for循环之前，都可以提取出来，做成叫做getblocktemplate的接口，getblocktemplate是一种JSON-RPC调用。

通过这个调用，就可以把挖矿的状态信息分享给其他矿机，矿机拿到blocktemplate以后直接进行nonce部分暴力搜索即可。

### 代码编写6 database

database是偏底层的接口，主要的功能有两个，第一是提供区块和私钥的持久化存储，第二是提供交易和区块的查询接口。

上文blockchain中的blockchain_database和keypair_database都是从database派生过来的。

```

key_pair_database

// 相当于是本地钱包的私钥管理
class key_pair_database
{
public:
    key_pair get_new_key_pair()；
    const key_pair_database_t&amp; list_keys() const；
private:
    key_pair_database_t key_pair_database_;
};


blockchain_database

public:
    uint64_t height();

    auto get_last_block();

    bool get_block (const sha256_t block_hash, block&amp; b);

    bool get_tx (const sha256_t tx_hash, tx&amp; t);

    bool push_block (const block&amp; b);

    bool pop_block (cconst sha256_t block_hash);

private:
    chain_database_t chain_database_;

```

### 代码编写7 commands

commands提供了开发者命令行交互接口。

```

    bool exec(Json::Value&amp; out);

    static const vargv_t commands_list;

private:
    vargv_t vargv_;
    node&amp; node_;

```

首先得有一个可识别的命令列表，接着是执行接口，例如命令行发起生成新key_pair的过程，执行getnewkey命令。

先被command解析，接着执行exec，执行的时候需要用到node对象。

实际上command类比较繁琐，因为一个功能复杂的钱包，维护的命令和种类可能多达几十种。

同时命令又可以被JSON-RPC调用，所以一般命令行客户端本身就是一个轻量级的http-client。

```

    std::string url{&quot;127.0.0.1:8000/rpc&quot;};
    // HTTP request call commands
    HttpReq req(url, 3000, reply_handler(my_impl));

```

### 代码编写8  基础类

基础类是实际生成公私钥对、构建交易tx的基本单元类，构建区块的基本单元类。

```
key_pair:
class key_pair
{
public:
    key_pair()  {
        private_key_ = RSA::new_key();
        public_key_ = private_key_.public_key();
    }

    address_t address()；
    sha256_t public_key() const；
    uint64_t private_key() const；

    // ...一些序列化接口(tinychain中是Json)
private:
    private_key_t private_key_;
    public_key_t public_key_;

tx:
public:
    input_t inputs() const { return inputs_; }
    output_t outputs() const { return outputs_; }
    sha256_t hash() const { return hash_; }

private:
    input_t inputs_;
    output_t outputs_;
    sha256_t hash_;

block:
class block
{
public:
    typedef std::vector&lt;tx&gt; tx_list_t;

    struct blockheader {
        uint64_t nonce{0};
        uint64_t height{0};
        uint64_t timestamp{0};
        uint64_t tx_count{0};
        uint64_t difficulty{0};
        sha256_t hash;
        sha256_t merkel_root_hash; //TODO
        sha256_t prev_hash;

    };
    // ... 一些其他接口和序列化函数
    std::string to_string() {
        auto&amp;&amp; j = to_json();
        return j.toStyledString();
    }

    sha256_t hash() const { return header_.hash; }

    void setup(tx_list_t&amp; txs) {tx_list_.swap(txs);}

private:
    blockheader header_;
    tx_list_t tx_list_;

```

## 首次运行

我们编写完基础类和基本结构的代码之后，就可以运行试一试。

编译成功是这样子的。

<img src="https://static001.geekbang.org/resource/image/32/81/32c5b165815109bf8bb088ea26840781.png" alt="">

我们可以看到有Tinychain和Cli-tinychain。

<img src="https://static001.geekbang.org/resource/image/51/75/513ed7a30252931af514559789292a75.png" alt="">

Tnychain就是我们的核心程序，cli-tinychain就是我们的命令行客户端。

实际上我在Server里还嵌入了一个可视化的Websocket界面。

<img src="https://static001.geekbang.org/resource/image/62/88/62e883f4177cd5d117025cee207ec188.png" alt="">

只需要在Tinychain可执行文件同目录底下创建webroot文件夹，将etc底下的index放入webroot下，接着打开浏览器127.0.0.1:8000就可以看到了。

实际上这个页面我想做成区块的监视页面，只是还没改造完成，目前支持发送命令。

我们开始首次运行Tinychain。

<img src="https://static001.geekbang.org/resource/image/1e/c9/1e9c369b71436faf3e4778d4ec6358c9.png" alt="">

运行后，等node和server全部started，就可以开始操作命令行了。

也可以通过日志进行监视，但是需要在代码处详细打桩，这次我偷懒了，没有好好打，所以不多，直接查看同目录下debug.log和error.log即可。

## 首次挖矿

我们通过./tinychain启动之后，开始第一次挖矿。

```
 ✘ chenhao@chenhaodeMacBook-Pro  ~/workspace/tinychain/build/bin   master  ./tinychain
20180610T232347 INFO [main] started
20180610T232347 INFO [node] node started
20180610T232347 INFO [main] httpserver started
20180610T232356 INFO [consensus] new block :{
 &quot;header&quot; :
 {
  &quot;difficulty&quot; : 9001,
  &quot;hash&quot; : &quot;&quot;,
  &quot;height&quot; : 1,
  &quot;merkel_header_hash&quot; : &quot;&quot;,
  &quot;nonce&quot; : 0,
  &quot;prev_hash&quot; : &quot;00b586611d6f2580e1ea0773ec8b684dc4acf231710519e6272ed7d0c61ed43e&quot;,
  &quot;timestamp&quot; : 1528644236,
  &quot;tx_count&quot; : 0
 },
 &quot;txs&quot; :
 [
  {
   &quot;hash&quot; : &quot;cddf6e838eff470d81155cb4c26fd3a7615b94a00e82f99b1fd9f583d7bc0659&quot;,
   &quot;inputs&quot; :
   [
    {
     &quot;hash&quot; : &quot;00000000000000000000000000000000&quot;,
     &quot;index&quot; : 0
    }
   ],
   &quot;outputs&quot; :
   [
    {
     &quot;address&quot; : &quot;122b03d11a622ac3384904948c4d808&quot;,
     &quot;value&quot; : 1000
    }
   ]
  }
 ]
}

20180610T232356 INFO [consensus] new block :0de5c36420aab2f7fc9413cfbd21bece697a349106771dc58b25a6a099d6aa86
20180610T232357 INFO [consensus] new block :{
 &quot;header&quot; :
 {
  &quot;difficulty&quot; : 18001,
  &quot;hash&quot; : &quot;&quot;,
  &quot;height&quot; : 2,
  &quot;merkel_header_hash&quot; : &quot;&quot;,
  &quot;nonce&quot; : 6048,
  &quot;prev_hash&quot; : &quot;0de5c36420aab2f7fc9413cfbd21bece697a349106771dc58b25a6a099d6aa86&quot;,
  &quot;timestamp&quot; : 1528644236,
  &quot;tx_count&quot; : 0
 },
 &quot;txs&quot; :
 [
  {
   &quot;hash&quot; : &quot;cddf6e838eff470d81155cb4c26fd3a7615b94a00e82f99b1fd9f583d7bc0659&quot;,
   &quot;inputs&quot; :
   [
    {
     &quot;hash&quot; : &quot;00000000000000000000000000000000&quot;,
     &quot;index&quot; : 0
    }
   ],
   &quot;outputs&quot; :
   [
    {
     &quot;address&quot; : &quot;122b03d11a622ac3384904948c4d808&quot;,
     &quot;value&quot; : 1000
    }
   ]
  }
 ]
}

```

刚开始挖矿会比较快，随着难度提升，会趋向于稳定到10秒种左右一个块，如果长时间不出块，难度会自动降下来。曾经元界的代码在难度调整上有缺陷，遭受了严重的“难度坠落”攻击。

我们可以通过这个位置观察难度调整的情况。
<img src="https://static001.geekbang.org/resource/image/e9/43/e91d8eb7857043c606266e591d5f8f43.png" alt="">

## 第一笔交易

我们保持挖矿，接下来发送一笔交易。
我们先通过getnewkey命令获得一个新公私钥对以及对应的地址。

<img src="https://static001.geekbang.org/resource/image/76/a5/76de5f90f37883321783c99032ec62a5.png" alt="">

接着发送第一笔交易。

<img src="https://static001.geekbang.org/resource/image/7f/da/7f84d975e3d38641c50804e1982560da.png" alt="">

探测到接下来被打包到区块中。

<img src="https://static001.geekbang.org/resource/image/a2/9c/a236bed57775c050b6e6d43b2156979c.png" alt="">

## 分叉与合并

区块链分叉是数据全网不一致的表现，通常是矿工节点行为不一致导致的，常见的有网络分区和协议不兼容，如果同时产生，那么必然会出现两条比较长的分叉链。

在现实情况中，分叉1个是最常见的，2个已经非常罕见了，3个以上基本是网络分区造成的。

如果我们要在Tinychain中实践网络分区和分叉，我们需要构建局域网多节点私链环境，可以通过docker来试验。

通过本文，你可以看到即使是搭建一个迷你区块链，它的工作量也是巨大的，其中不仅仅只是组合几个基础组件那么简单，还要涉及各个模块的设计和交互等详细的工作。

由于在短时间内全部搭建以及实现Tinychain所有功能是不可行的，在这里，我只为你提供了一些实践的思路。

目前Tinychain缺失了P2P网络实现、RSA公私钥对集成、共识模块的交易和区块的验证等内容，我会在后续逐渐完善，你也可以跟我一起补充。

## 总结

好了，通过今天的代码实践，我们实现了迷你区块链Tinychain，并且，通过运行与测试Tinychain，我们了解到了一个最简单区块链的运行原理，希望通过今天的文章，可以帮你加深对区块链技术的理解。

区块链技术只是作为基础设施，服务于广大的开发者和业务需求。目前区块链的发展远远不止Tinychain中所展现的样子，我们还需要去考虑区块链2.0智能合约，如何设计Token经济等一些问题。

随着区块链的发展和应用规模，区块链安全问题也日益突出，所以今天的问题是，如果要攻击Tinychain，可以采取什么手段呢？你可以给我留言，我们一起讨论。

感谢你的收听，我们下次再见。

