Skip to content

rottenpen/blockchain-in-js-zh

 
 

Repository files navigation

用 Javascript 创建我们的区块链

随着区块链和加密货币的大热,我决定深入了解它。还有什么比自己构建一个更好的学习方法呢?接下来你将通过一步又一步的实验来理解区块链是怎么运行的,深入了解其中原理。下面的每一个步骤,你都可以通过 blockchain-step${N}.nambrot.com 找到对应版本,以及通过 blockchain.nambrot.com 可以看到最终版本。 免责声明:因为出于教学目的,在某些方面我的实现会不太标准,该区块链会偏离现实。

Step 1: 区块里的链?

了解区块链的原理之前,我们先从区块里的一条链说起。

区块链常常被误以为是一条单独的链,实际上,区块链更像一棵区块树。因此在任何时间节点中,区块都有很多条链指向它们的父级。而这个指向是通过区块中的数据(例如: 父节点的哈希值,交易数据和其他重要的东西)所计算出来的。

因为区块哈希值的存在,区块链将强制按照指定的规则运行。例如,给定一个区块链,你不能只修改中间某一个节点的数据,因为这将改变这个节点的哈希值,从而导致它关联的所有子区块的哈希值都发生变化。

class Block {
  constructor(blockchain, parentHash, nonce = sha256(new Date().getTime().toString()).toString()) {
    this.blockchain = blockchain;
    // 当前节点数据
    this.nonce = nonce;
    this.parentHash = parentHash;
    // 哈希值是根据 当前节点的数据以及父节点的哈希值进行计算的
    this.hash = sha256(this.nonce + this.parentHash).toString()
  }
}

从上面的代码,你可以看到区块链的 P2P 是怎样运作的。当一个节点决定“挖掘”一个区块的时候,它就可以将该区块广播给所有其他节点,其他节点可以对其进行验证,然后添加到各自的树中。

blockbroadcast

Step 2: So what is THE blockchain? 所以什么是区块链呢

第二步 Demo 的链接

在第一步我们看到了一条链上的所有区块,最后一个区块会验证整条链上的所有信息,因为上层链路任何修改都会改变最后一个区块的哈希值。OK,那大家都用区块链干什么呢?

根据定义,区块链就是树结构上的最长的链。因此,这条最长链是可以被其他更长的链取代的。让我们来看看这条最长的链是怎么实现的。

class Blockchain {
  longestChain() {
    const blocks = values(this.blocks)
    const maxByHeight = maxBy(prop('height'))
    const maxHeightBlock = reduce(maxByHeight, blocks[0], blocks)
    const getParent = (x) => {
      if (x === undefined) {
        return false
      }

      return [x, this.blocks[x.parentHash]]
    }
    return reverse(unfold(getParent, maxHeightBlock))
  }
}

最长链

在一个树中,这个最长链可以代表当前时间节点这个树的历史,因此我们可以它来确定哪一块的数据是有效的。

Step 3: Not a free-for-all 不是谁都能改的

Link to Step 3 Demo

如果区块链真的像第二步那么运作,就乱套了。任何节点分出来的分支都可以随便通过添加上无限个区块成为最长链,从而成为区块链。这意味着任何人都可以改变历史并有效地改变过去的数据。那我们如何避免这种情况呢?

这个解决方案就是通过添加复杂的数学运算来提高插入节点的难度。区块链共识规定了哪些块有效的,哪些块是无效的。在这种情况下,我们希望通过密集计算提高它的成本。最常见的做法就是像比特币白皮书那样(也是它最令人佩服的部分),将节点和工作量证明(POW)配对。通过 POW 可以确保了每一个区块节点都要证明它们付出了巨大的努力才能添加到树中。由于 区块哈希是真随机(希望是)的,我们强制要求其结尾有一定数量的 '0'(在比特币中,要求它以一定数量的“0”开头)

class Block {
  isValid() {
    return this.parentHash === 'root' ||
      (this.hash.substr(-DIFFICULTY) === "0".repeat(DIFFICULTY) &&
      this.hash === sha256(this.nonce + this.parentHash).toString())
  }

  setNonce(nonce) {
    this.nonce = nonce
    this._setHash()
  }

  _setHash() {
    this.hash = sha256(this.nonce + this.parentHash).toString()
  }
}

在真实的区块链中,0 的数量是根据最近一次添加的节点计算出来的。挖矿节点不得不尝试许多不同的随机数,才能得到最终以 {DIFFICULTY} 个'0'结尾的哈希值。

proofofwork

工作量证明(Proof-of-work)是区块链可以实现去中心化的奥妙所在,同时也是遭到臭名昭著的 51% 双花难题的由来。一旦一个区块进入区块链,攻击者不得不为其后面的所有块重做工作量证明( proof-of-work)。这里有个双花难题的例子:添加一笔交易到一个区块,但随后通过从父区块中挖出另一条链使其无效。然而,如果没有 51% 的算量,它将总是落后于其他合法的节点。因此,区块链的安全保障全依赖于算力不被集中在单一节点。

Step 4: 我要怎么挖矿呢?

Link to Step 4 Demo

那么问题来了,为什么矿工要花费这么大的精力来添加一个块呢?要么这对于他们来说是一场有趣的游戏,要么我们需要给予他们经济上的奖励。为了让矿工保护区块链,该协议为矿工提供了挖矿奖励,目前是 12.5 比特币。只要它通过我们上面讨论的其他规则,其他节点就会接受这个挖矿节点并允许它奖励自己。让我们来谈谈矿工如何给自己奖励的具体机制,这需要一个所有权的概念和将这种所有权纳入区块的方法。(当矿工挖到了矿,就可以自己生成一个公钥,广播给其他节点听,我这个公钥有多少个币)

从比特币发明最初的 50 个比特币/区块到 2016 年后的 12.5 个比特币/区块,并会在 2040 年达到总数接近 2100 万个比特币,在那之后新的区块不再包含比特币奖励,矿工的收益全部来自交易费。

为了理解所有权,你需要对公钥有更高级别的理解,这超出了本教程的范围。https://www.youtube.com/watch?v=3QnD2c4Xovk 这是一个看起来很好的非技术性教程。接下来,你只需要知道的是以下情况可能发生:

  1. 有一种方法可以生成两样东西,一个公钥和一个私钥。矿工来保存私钥。

  2. 公钥是你可以公开广播给其他各方的东西。

  3. 为了证明你是产生公钥的人,你可以用你的私钥签署一个特定的信息(或任意的数据)。其他人可以用你的签名(专门针对该信息)、该信息以及你的公钥,并验证该签名确实来自于控制私钥的人(因为没有私钥,就没有办法令人满意地签署该信息)。

  4. 使用公钥,你可以对信息(数据)进行加密,以便只有私钥的拥有者可以解密。

简而言之,所有权是控制某物的概念,在这种情况下,你 "拥有 "公钥,你可以通过用你的私钥签署数据来证明这种所有权。因此,为了获得采矿奖励,即要求对其拥有所有权,矿工所要做的就是在区块中包括他们的公钥。该公钥也被称为比特币的钱包地址。

因此,我们只需在区块中添加一个名为 "coinbaseBeneficiary "的字段,包含矿工的公钥,并将其添加到哈希计算的有效载荷中。

class Block {
  isValid() {
    return this.parentHash === 'root' ||
      (this.hash.substr(-DIFFICULTY) === "0".repeat(DIFFICULTY) &&
      this.hash === this._calculateHash())
  }

  createChild(coinbaseBeneficiary) {
    return new Block({
      blockchain: this.blockchain,
      parentHash: this.hash,
      height: this.height + 1,
      coinbaseBeneficiary
    })
  }

  _calculateHash() {
    return sha256(this.nonce + this.parentHash + this.coinbaseBeneficiary).toString()
  }
}

因此,一个硬币只对一个公钥与一个私钥拥有所有权。沿着区块链往下走,你可以把哪些公钥拥有多少个币加起来。在现实中,这些被称为未花费的输出(UTXO)。为了避免每次我们想知道一个地址控制了多少个币时都要遍历区块链,我们把这些知识与每个区块一起 "缓存" 到 UTXO 池中。每当我们把一个区块添加到父区块时,我们只需在父区块的UTXO池中添加coinbase受益人的硬币。

在当前的区块链项目中,主要有两种记录保存方式,一种是账户/余额模型,一种是UTXO模型。比特币采用就是UTXO模型,以太坊、EOS等则采用的是账户/余额模型。 UTXO是 Unspent Transaction Output 的缩写,意思是未花费的输出,可以简单理解为还没有用掉的收款。比如韩梅梅收到一笔比特币,她没有用掉,这笔比特币对她来说就是一个UTXO。 UTXO 核心设计思路是:它记录交易事件,而不记录最终状态

class UTXOPool {
  constructor(utxos = {}) {
    this.utxos = utxos
  }

  addUTXO(publicKey, amount) {
    if (this.utxos[publicKey]) {
      this.utxos[publicKey].amount += amount
    } else {
      const utxo = new UTXO(publicKey, amount)
      this.utxos[publicKey] = utxo
    }
  }

  clone() {
    return new UTXOPool(clone(this.utxos))
  }
}

class Blockchain {
  _addBlock(block) {
    if (!block.isValid())
      return
    if (this.containsBlock(block))
      return

    // check that the parent is actually existent and the advertised height is correct
    const parent = this.blocks[block.parentHash];
    if (parent === undefined && parent.height + 1 !== block.height )
      return

    // Add coinbase coin to the pool of the parent
    const newUtxoPool = parent.utxoPool.clone();
    newUtxoPool.addUTXO(block.coinbaseBeneficiary, 12.5)
    block.utxoPool = newUtxoPool;

    this.blocks[block.hash] = block;
    rerender()
  }
}

正如你看到,如果我们继续挖更多的区块,我们就能计算出更多的币。

utxopool

因此你还应该开始了解区块链是如何饰演账本的角色,同时需要了解它是如何变现的。正如你在下面的 GIF 中看到的那样,区块链中的分叉产生了不同的 UTXOPools,从而产生不同的币(这就是确保共识如此重要的原因)。 这就是为什么通常建议等待块的数量达到一定数目,再考虑进行结算交易,否则分叉可能使您的账本状态无效。

51attack

Step 5: 你得到一个币了!

Link to Step 5 Demo

我们已经非常接近让它成为一个可用的区块链,唯一真正缺乏的是向某人发送硬币的能力。加上这个功能,我们终于开始交易了。它实际上非常简单:

class Transaction {
  constructor(inputPublicKey, outputPublicKey, amount) {
    this.inputPublicKey = inputPublicKey
    this.outputPublicKey = outputPublicKey
    this.amount = amount
    this._setHash()
  }

  _setHash() {
    this.hash = this._calculateHash()
  }

  _calculateHash() {
    return sha256(this.inputPublicKey + this.outputPublicKey + this.amount).toString()
  }
}

交易只是一个把公钥的所有权转移给另一个节点的声明,因此我们在交易中只需要记录当前公钥和目标公钥以及想要转多少个币的信息。(在真正的比特币中,UTXO 必须被完全消耗,并且可以有多个输入和输出)我们显然需要确保人们只能花费存在的币。我们通过跟踪“余额”的 UTXOPool 来做到这一点。(所以下图有转给自己的概念) image

class UTXOPool {
  handleTransaction(transaction) {
    if (!this.isValidTransaction(transaction.inputPublicKey, transaction.amount))
      return
    const inputUTXO = this.utxos[transaction.inputPublicKey];
    inputUTXO.amount -= transaction.amount
    if (inputUTXO.amount === 0)
      delete this.utxos[transaction.inputPublicKey]
    this.addUTXO(transaction.outputPublicKey, transaction.amount)
  }

  isValidTransaction(inputPublicKey, amount) {
    const utxo = this.utxos[inputPublicKey]
    return utxo !== undefined && utxo.amount >= amount && amount > 0
  }
}

由于我们将交易的哈希信息放到区块中运算了,其他节点能轻易验证出 1. 这个区块的父节点是否有效 2. 这个交易必须来自已经进行了 POW.

class Blockchain {
  _addBlock(block) {
    // ...
    const newUtxoPool = parent.utxoPool.clone();
    block.utxoPool = newUtxoPool;

    // Add coinbase coin to the pool
    block.utxoPool.addUTXO(block.coinbaseBeneficiary, 12.5)

    // Reapply transactions to validate them
    const transactions = block.transactions
    block.transactions = {}
    let containsInvalidTransactions = false;

    Object.values(transactions).forEach(transaction => {
      if (block.isValidTransaction(transaction.inputPublicKey, transaction.amount)) {
        block.addTransaction(transaction.inputPublicKey, transaction.outputPublicKey, transaction.amount)
      } else {
        containsInvalidTransactions = true
      }
    })

    // If we found any invalid transactions, dont add the block
    if (containsInvalidTransactions)
      return
    // ...
  }
}

你应该意识到这是一个保持矿工“真诚”的方法。如果矿工包含了无效的教育,其他节点将拒绝这个区块,认为它不是最长链的一部分。我们要确保有效交易的共识。看看下图:

addingtx

Step 6: 不一定要计算

Link to Step 6 Demo

如果你数学不好(准确来说是你电脑算力不足),是否就以为这你无法将交易添加到区块链中?那也太可怕了吧!相反,作为非挖矿节点,我们也可以订阅广播,来实现类似交易的能力:

class Blockchain {
  constructor() {
    // ...
    subscribeTo("TRANSACTION_BROADCAST", ({ transaction, blockchainName }) => {
      if (blockchainName === this.name) {
        this.pendingTransactions[transaction.hash] = new Transaction(
          transaction.inputPublicKey,
          transaction.outputPublicKey,
          transaction.amount
        );
      }
    });
  }
}

txbroadcast

Step 7: 天底下没有免费的午餐

Link to Step 7 Demo

除非你赞同 “爱你的邻居” 这种做慈善的说法,人们通常不喜欢免费为其他人做事。那么为什么挖矿节点要为非挖矿节点添加交易呢?是的,他们不会这么做。因此,让我们通过交易费用为他们添加一些激励措施,我们可以指定一些矿点作为交易作者,他们的区块可以在我们的交易中获取小费。

class Block {
  addTransaction(inputPublicKey, outputPublicKey, amount, fee) {
    if (!this.isValidTransaction(inputPublicKey, amount, fee))
      return
    const transaction = new Transaction(inputPublicKey, outputPublicKey, amount, fee)
    this.transactions[transaction.hash] = transaction
    this.utxoPool.handleTransaction(transaction, this.coinbaseBeneficiary)
    this._setHash();
  }
}

class UTXOPool {
  handleTransaction(transaction, feeReceiver) {
    if (!this.isValidTransaction(transaction.inputPublicKey, transaction.amount, transaction.fee))
      return
    const inputUTXO = this.utxos[transaction.inputPublicKey];
    inputUTXO.amount -= transaction.amount
    inputUTXO.amount -= transaction.fee
    if (inputUTXO.amount === 0)
      delete this.utxos[transaction.inputPublicKey]
    this.addUTXO(transaction.outputPublicKey, transaction.amount)
    this.addUTXO(feeReceiver, transaction.fee)
  }
}

Step 8: 不要碰我的钱

Link to Final Demo

你已经注意到任何节点都有可能消费可用的 UTXO。如果是这样也太疯狂了吧!让我们通过所有权来修复这个问题。正如我们上面所说的,所有权实际上就是你可以生成私钥的能力。为了确认所有者的意图,我们需要私钥和一个哈希签名。然后当节点接收到交易的区块时,可以验证签名对交易来说确实有效。(也就是说,每次交易都会用当前交易者的私钥生成签名,广播给其他节点配合对应的公钥进行验证,这次交易是否有效)

class Transaction {
  constructor(inputPublicKey, outputPublicKey, amount, fee, signature) {
    this.inputPublicKey = inputPublicKey;
    this.outputPublicKey = outputPublicKey;
    this.amount = amount;
    this.fee = fee;
    this.signature = signature;
    this._setHash();
  }

  hasValidSignature() {
    return (
      this.signature !== undefined &&
      verifySignature(this.hash, this.signature, this.inputPublicKey)
    );
  }
}

class Block {
  isValidTransaction(transaction) {
    return (
      this.utxoPool.isValidTransaction(transaction) &&
      transaction.hasValidSignature()
    );
  }
}

如下图所示,这将通过来自私钥的签名将 UTXO 的控制权与公钥的相应所有者联系起来,从而完成我们的区块链

transactionsinging

就是这样!!!你应该也觉得区块链非常简单。以至于比特币白皮书也就简单的 8 页。正如演示所见,您真正需要学习的只是一些公钥加密知识和一些比较绕的哈希函数。

更多

这就结束了?将来我会添加 merkle trees 和 segwit 相关的功能进来,但就目前来说,我希望可以让你更好的理解区块链,比如比特币是怎么工作的。

怎么运行

这个项目是基于 create-react-app 构建的,所以一条简单的 yarn start 即可完美运行一切。你只需要通过 node src/server.js 打开一个 socket.io 服务。根据你的喜好,你也可以执行 docker-compose。或者你可以在 blockchain.nambrot.com 上直接运行。

About

用 js 创建你的区块链!

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • JavaScript 97.3%
  • HTML 2.0%
  • CSS 0.7%