从零开始构建一个区块链(三): API

一、挖矿奖励

在开始改造我们的代码之前,我们先来看一下什么是挖矿奖励。

上一篇专栏讲解了挖矿的原理并且实现了POW算法,可是服务器为什么愿意去耗费自己的CPU资源来打包区块呢?答案就是挖矿时有一个奖励机制。矿工在打包一个时间段的交易后,会在区块的第一笔交易的位置创建一笔新的交易。这笔交易没有发送人,接收人可以设为任何人(一般就是自己啦),奖励的数额是多少呢?目前比特币中是12.5个BTC。这笔奖励交易是由系统保证的,可以通过任何一个其他节点的验证。

这里面几个问题。首先是奖励金额的问题。比特币刚开始发行时,每个区块的奖励是50BTC,其后每隔四年时间减半,今年刚刚减半到12.5个了。另外一个是矿工能否创建多比奖励交易或者加大奖励金额?矿工当然可以这么干,但是这么做以后广播出去的区块是无法通过其它节点验证的,因此其他节点收到区块后会丢弃该区块,而该区块最终也不会被添加到区块链中。


二、代码重构

为了把我们当前的代码改造成适合通过API对外提供的形式,我们需要做几个处理:

1. 在Blockchain类中添加属性currentTransactions,由于收集最新交易,并且准备打包到下一个区块中。

  constructor() {
    this.chain = [this.createGenesisBlock()];
    this.difficulty = 3;
    this.currentTransactions = [];
  }

2. 把Block类中的addNewTransaction方法移到Blockchain类里。

3. 把Block类和Blockchain类export出去,重命名app.js为blockchain.js。

最后的blockchain.js应该为:

const SHA256 = require('crypto-js/sha256');
class Block {
  constructor(index, timestamp) {
    this.index = index;
    this.timestamp = timestamp;
    this.transactions = [];
    this.previousHash = '';
    this.hash = this.calculateHash();
    this.nonce = 0;
  }

  calculateHash() {
    return SHA256(this.index + this.previousHash + this.timestamp + JSON.stringify(this.transactions) + this.nonce).toString();    
  }

  mineBlock(difficulty) {
    console.log(`Mining block ${this.index}`);
    while (this.hash.substring(0, difficulty) !== Array(difficulty + 1).join("0")) {
        this.nonce++;
        this.hash = this.calculateHash();
    }
    console.log("BLOCK MINED: " + this.hash);
  }

  getTransactions() {
    return this.transactions;
  }
}

class Blockchain {
  constructor() {
    this.chain = [this.createGenesisBlock()];
    this.difficulty = 3;
    this.currentTransactions = [];
  }

  addNewTransaction(sender, recipient, amount) {
    this.currentTransactions.push({
      sender,
      recipient,
      amount
    })
  }

  createGenesisBlock() {
    const genesisBlock = new Block(0, "01/10/2017");
    genesisBlock.previousHash = '0';
    genesisBlock.transactions.push({
      sender: 'Leo',
      recipient: 'Janice',
      amount: 520
    })
    return genesisBlock;
  }

  getLatestBlock() {
    return this.chain[this.chain.length - 1];
  }

  addBlock(newBlock) {
    newBlock.previousHash = this.getLatestBlock().hash;
    newBlock.mineBlock(this.difficulty);
    this.chain.push(newBlock);
  }

  isChainValid() {
    for (let i = 1; i < this.chain.length; i++){
      const currentBlock = this.chain[i];
      const previousBlock = this.chain[i - 1];

      if(currentBlock.hash !== currentBlock.calculateHash()){
        return false;
      }

      if(currentBlock.previousHash !== previousBlock.hash){
        return false;
      }
    }
    return true;
  }
}

module.exports = {
  Block,
  Blockchain
}

注意上面顺便修改了Blockchain里的方法createGenesisBlock的代码。


三、 使用Express提供API服务

为了能够提供API服务,这里我们采用Node.js中最流行的Express框架,对外提供三个接口:

  • /transactions/new 添加新的交易,格式为JSON;
  • /mine 打包目前的交易到新的区块
  • /chain 返回当前的区块链

基础代码如下:

const express = require('express');
const uuidv4 = require('uuid/v4');
const Blockchain = require('./blockchain').Blockchain;

const port = process.env.PORT || 3000;
const app = express();
const nodeIdentifier = uuidv4();
const testCoin = new Blockchain();

app.get('/mine', (req, res) => {
  res.send("We'll mine a new block.");
});

app.post('/transactions/new', (req, res) => {
  res.send("We'll add a new transaction.")
});

app.get('/chain', (req, res) => {
  const response = {
    chain: testCoin.chain,
    length: testCoin.chain.length
  }
  res.send(response);
})

app.listen(port, () => {
  console.log(`Server is up on port ${port}`);
});

下面我们完善路由'/mine'以及'/transactions/new',并添加一些日志的功能(非必需).

先来看路由/transactions/new,在这个接口中,我们接受一个JSON格式的交易,内容为

{
 "sender": "my address",
 "recipient": "someone else's address",
 "amount": 5
}

然后把该交易添加到当前区块链的currentTransactions中。这里会利用到body-parser模块,最后的代码为:

const bodyParser = require("body-parser");
const jsonParser = bodyParser.json();
app.post('/transactions/new', jsonParser, (req, res) => {
  const newTransaction = req.body;
  testCoin.addNewTransaction(newTransaction)
  res.send(`The transaction ${JSON.stringify(newTransaction)} is successfully added to the blockchain.`);
});

接下来是路由/mine。该接口实现的功能是收集当前未被打包的交易,打包到一个新的区块中;添加奖励交易(这里设置为50,接收地址为uuid);进行符合难度要求的挖矿,返回新区块信息。代码实现如下:

app.get('/mine', (req, res) => {
  const latestBlockIndex = testCoin.chain.length;
  const newBlock = new Block(latestBlockIndex, new Date().toString());
  newBlock.transactions = testCoin.currentTransactions;
  // Get a reward for mining the new block
  newBlock.transactions.unshift({
    sender: '0',
    recipient: nodeIdentifier,
    amount: 50
  })
  testCoin.addBlock(newBlock);
  testCoin.currentTransactions = [];
  res.send(`Mined new block ${JSON.stringify(newBlock, undefined, 2)}`);
});

至此代码基本完成,最后我们添加一个记录日志的中间件

app.use((req, res, next) => {
  var now = new Date().toString();
  var log = `${now}: ${req.method} ${req.url}`;
  console.log(log);
  fs.appendFile('server.log', log + '\n', (err) => {
    if (err) console.log(err);
  });
  next();  
})

完整代码请参考Github liangpeili/testcoin


四、 测试API

使用node server.js启动应用,我们使用Postman来对当前的API进行测试。

在启动应用后,当前区块链应该只有一个创世区块,我们使用/chain来获取当前区块链信息;

可以看到当前区块链只有一个区块。那怎么添加新的交易呢?我们使用以下方式:

把交易以JSON的形式添加到请求的body中,应该会返回以下结果:

接下来我们可以进行挖矿了:把当前的交易打包到新的区块,并给自己分配奖励。这次我们使用/mine接口。

返回结果如下:

可以看到交易已经被打包到新的区块中了。新的区块中包含一笔奖励交易,难度也符合要求(连续3个0).

至此三个接口全部工作正常,我们也可以继续添加交易、挖矿,一直进行下去。

有人会问:如果不添加交易是否可以挖矿呢?答案是Yes!一般在一个区块链项目的早期,交易的数量可能一天也没有几笔。但是挖矿的工作是要一直进行下去的,只不过每个区块除了奖励交易再没有其他了,这种区块一般被成为“空块”。在我们这里也可以实现,不添加交易,直接调用mine接口:

此时再查看区块链信息,就可以看到刚刚的两个区块了。


参考:

hackernoon.com/learn-bl

发布于 2017-10-09

文章被以下专栏收录