banner
leaf

leaf

It is better to manage the army than to manage the people. And the enemy.

区块链开发实战

区块链技术主要包括以下几个部分。

1. 加密哈希函数

我们都知道,一个函数可以接收一个或若干个输入值,然后经函数运算产生一个或若干个输出值。哈希函数满足所有下列条件:

・接收任意长度的字符串作为输入。

・产生一个固定长度的输出值。

・计算时间在合理范围内。

只要满足上述条件,一个函数就可以称为哈希函数。举个简单的例子:取模运算,任意数字对 10 取模后得到的结果都是 0~9 之间的一个数字,那么取模运算就可以认为是一个哈希函数。

目前用于比特币等数字货币的哈希函数则是加密哈希函数。加密哈希函数除了拥有上述哈希函数的三个特点外,还有着更为独特的特性:无碰撞性、隐藏性、结果随机性,下面分别解释。

(1)无碰撞性

无碰撞性分为强无碰撞性和弱无碰撞性。强无碰撞性的意思是,对于一个哈希函数 H,我们无法找到两个不同的 x 和 y 值,使得 H(x)=H(y)。弱无碰撞性则是,对于一个哈希函数 H 以及输入 x 值,无法找到另外一个 y 值,使得 H(x)=H(y)。

无碰撞性并不是真正的 “无” 碰撞,碰撞是肯定存在的,这里强调的是寻找碰撞的难度。我们以比特币中使用到的哈希函数 SHA256 为例,它的输出值为 256 位,因此结果只有 2256 种可能,但是输入值却可以有无限种可能。现在就有一种必然能找到碰撞的办法:我们首先找到 2256+1 个不同的值,分别计算出它们的哈希值,那结果集里必然有重复的值,这样就会发现一次碰撞了。但是这种方法的可行性怎样呢?假设把全世界所有的计算设备集合起来,从宇宙诞生的时刻到现在一直不停地运算,能够找到一次碰撞的概率和下一秒钟地球被陨石撞击而毁灭的概率一致。既然你读到了这里,那就说明地球毁灭没有发生,也就是没有碰撞发生。

现在还没有一种加密哈希函数在数学上被证明是严格无碰撞性的,现在市面上提到的无碰撞性,一般认为是目前除了暴力破解之外没有其他的途径能够更快地找到碰撞而已。以前也有曾经被认为是无碰撞性的哈希函数后来找到了破解方案的案例。比如 MD5 哈希算法。比特币使用的 SHA256 哈希算法目前也被认为是无碰撞性的,但是不排除以后被破解的可能。

无碰撞性有什么应用呢?一个比较常见的就是消息摘要(Message Digest)。消息摘要是指针对任意长度的输入,通过加密哈希函数运算后得到的哈希值。以现在常用的哈希算法 MD5 为例,其运算示例如下:

image

可见输入任何长度的字符串,得到的结果都是固定长度的随机字符串。因为无碰撞性的存在,我们可以认为这个字符串能唯一地代表输入值。

我们平时在互联网上下载软件时,如何确定我们下载的这个软件和网站上的软件就是同一个呢?这时消息摘要就可以发挥作用了。例如,有的网站在下载软件时提供该软件的 md5sum 值,那么我们在下载完该软件时,就可以手工计算一遍该软件的 md5sum 值,然后和网站上的值进行对比,只要两个数值一致,就可以说明我们下载的软件是完整无误的。

(2)隐藏性

给定 H(x),无法推测出 x 的值。不仅无法推测出 x 的值,也不能推测出关于 x 的任何特点,比如奇偶性等。

(3)结果随机性

无论 x 值是否相近,经过哈希运算后得出的 H(x)都是完全随机的。

这个特点是说,哪怕输入值 x 的长度很长,同时另一个输入值 x' 和 x 值只有一位不同,那它们经过哈希函数 H 运算后得到的结果没有任何的相关性,就像输入了两个完全不同的 x 值一样。

继续以 md5sum 值为例:

liangpeili@LiangXiaoxin:~$ echo 'aschplatform' | md5sum
150fa3630db1d8f576d1266176f6e0f7  -
liangpeili@LiangXiaoxin:~$ echo 'aschplatform1' | md5sum
e915a617b2301631ec14d1ca2c093c63  -
liangpeili@LiangXiaoxin:~$ echo 'aschplatform2' | md5sum
bbb9d830f4a5d47051f9fd19cb0fc75e  -

从上面的程序中可以看出,即使只改变一个很小的值,经过哈希运算后的结果也会有很大的不同。

这个特性有什么作用呢?如果我们针对特定的结果值 H(x),想找到一个符 H(x),想找到一个符合条件的输入值 x,那么除了暴力尝试之外没有其他办法。继续以 SHA256 为例,它的输出结果长度为 256 位,如果我们想找到这样的一个 x 值,使得它经过 SHA256 运算后,结果的第一位是 0,求解这样的 x 值的期望次数为 2,那如果想要得到连续 10 位为 0 的哈希值呢?期望计算次数就是 210 了。通过调整结果范围,我们就可以对计算次数(也可以认为是结果难度)进行调整,这也是比特币调整难度值的原理。

使用 Python 实现的挖矿算法如下:

#!/usr/bin/env python
#coding:utf-8
example of proof-of-work algorithm

import hashlib
import time

max_nonce = 2 ** 32 # 4 billion

def proof_of_work(header, difficulty_bits):

calculate the difficulty target

target = 2 ** (256-difficulty_bits)

for nonce in xrange(max_nonce):
hash_result = hashlib.sha256(str(header)+str(nonce)).hexdigest()
# check if this is a valid result, below the target
  if long(hash_result, 16) < target:
     print("Success with nonce %d" % nonce)
     print("Hash is %s" % hash_result)
     return (hash_result, nonce)
print("Failed after %d (max_nonce) tries" % nonce)
return nonce
if name == "main":
nonce = 0
hash_result = ''

difficult from 0 to 15 bits

for difficulty_bits in xrange(16):

difficulty = 2 ** difficulty_bits
print("Difficulty: %ld (%d bits)" %(difficulty, difficulty_bits))

print("Starting search...")

# checkpoint the current time
start_time = time.time()

# make a new block which includes the hash from the previous block
# we fack a block of transactions - just a string
new_block = 'test block with transactions' + hash_result
# find a valid nonce for the new block
(hash_result, nonce) = proof_of_work(new_block, difficulty_bits)

# checkpoint how long it took to find a result 
end_time = time.time()

elapsed_time = end_time - start_time
print("Elapsed Time: %.4f seconds" % elapsed_time) 

if elapsed_time > 0:

  # estimate the hashes per second
  hash_power = float(long(nonce)/elapsed_time)
  print("Hashing Power: %ld hashes per second" % hash_power)  
2.数字签名

在现实工作和生活中,我们使用签名的方式表达了对一份文件的认可,其他人可以识别出你的签名并且无法伪造你的签名。数字签名就是对现实签名的一种电子实现,它不仅可以完全达到现实签名的特点,甚至能做得更好。常用的数字签名算法有RSA(Rivest-Shamir-Adleman Scheme)、DSS(Digital Signature Standard)等。比特币使用ECDSA(椭圆曲线数字签名算法)来生成账户的公私钥以及对交易和区块进行验证数字签名的工作原理如下所示:

1)Alice生成一对密钥,一个是sk(signing key),是非公开的;另一个是vk(verification key),是公开的。这一对密钥同时生成,并且在数学上是相互关联的,同时,根据vk无法推测出关于sk的任何信息。

2)数字签名算法接收两个输入:信息M和sk,生成一个数字签名Sm。

3)验证函数接收信息M、Sm以及vk作为输入,返回的结果是yes或者no。这一步的目的是为了验证你看到的针对信息M的数字签名确实是由Alice的sk来签发的,用于确认信息与签名是否相符。

与手写签名不同,手写签名基本都是相似的,但是数字签名却受输入影响很大。对输入的轻微改变都会产生一个完全不同的数字签名。一般不会直接对信息进行数字签名,而是对信息的哈希值进行签名。由加密哈希函数的无碰撞性可知,这样和对原信息进行签名一样安全。

3.共识机制

区块链可以看做是一本记录所有交易的分布式公开账簿,而区块链中每个节点都是对等的。这就带来一个问题:谁有权往这个账本录入数据?如果有好几个节点同时对区块链进行数据写入,最终以谁的为准?这就是在分布式网络中如何保持数据一致性的问题。共识机制是指在一个分布式的网络中,让各个参与网络的节点达成数据上的一致性。在区块链中,共识机制的作用还包括区块生产、区块验证以及系统的经济激励等功能。

不同的共识机制适用于不同的应用场景,以下是常用的共识机制及其适用的应用场景介绍:

工作量证明(Proof of Work,POW)——比特币使用的就是工作量证明的共识机制。在这种机制里,任何拥有计算能力的设备都可以参与竞争区块的生产,系统会根据当前全网的算力动态调整难度值,来保证平均每10分钟网络将根据后续区块的态度来决定认可哪个区块。一般来说,一笔交易在经过6次确认(约1个小时)后被认为是比较安全而且不可逆的。中本聪在设计比特币时,使用工作量证明机制背后的核心思想是“one cpu one vote”,期望能够把比特币设计成一个完全去中心化的系统,任何人都可以使用电脑等终端参与进来。虽然后来由于矿池的出现,使得比特币系统的算力比较集中,但目前工作量证明机制仍然被认为是最适合公链的共识机制。

·股权证明(Proof of Stake,POS)——股权证明机制于2013年被提出,最早应用于Peercoin中。在工作量证明机制中,生产区块的概率和你拥有的算力成正比。相应的,在股权证明机制中,生产区块的难度和你在该系统中占有的股权成正比。在股权证明机制中,一个区块的生产过程为:节点通过保证金(代币、资产、名声等具备价值属性的物品即可)来对赌一个合法的区块会成为新的区块,其收益为抵押资本的利息和交易服务费。提供的保证金越多,获得记账权的概率就越大。一旦生产了一个新的区块,节点就可以获得相应的收益。股权证明机制的目标是为了解决工作量证明机制里大量能源被浪费的问题。恶意参与者存在保证金被罚没的风险。

·授权股权证明(Delegated Proof of Stake,DPOS)——工作量证明和股权证明机制虽然都可以解决区块链数据的一致性问题,但正如上面提到的工作量证明机制存在算力集中(矿池)的问题,而股权证明机制根据保证金的数量来调节生产区块难度的方式则会导致“马太效应”的出现,也就是拥有大量代币的账户权利会越来越大,有可能支配记账权。为了解决前两者的问题,后来又有人提出了基于股权证明机制的改进算法——授权股权证明机制。在这种共识机制里,系统中的每个持币用户都可以投票给某些代表,最终得票率在前101名的代表可以获得系统的记账权。这些代表按照既定时间来锻造区

块,并且获取锻造区块的收益。授权股权证明机制既可以提高共识的效率(相比较比特币每10分钟生产一个区块,这种机制可以实现10秒以内生产一个区块),又避免了能源的浪费和马太效应,因此成为了很多新兴公链(比如EOS)的选择。

4.交易的区块链

在比特币网络中,每笔交易完成后,这笔交易会广播到比特币的P2P网络。矿工不仅能够接收到这笔交易,而且还能接收到相同时间段内其他的所有未被记录的交易。矿工的工作就是把这些所有交易打包成一个交易区块。具体的过程是:

1)矿工会把这些交易记录两两配对,通过默克尔树计算出根节点的值。

2)根节点和上一个区块的哈希值结合,作为一个Challenge String,供矿工作为工作量证明的输入值。

3)矿工完成工作量证明,并把proof公开出去供其他节点验证。同时在第一条记录(这条记录也称为coinbase transaction)里给自己分配挖矿奖励。

4)其他节点验证通过,该区块作为新区块加入到区块链中。

5)矿工也可以收集其他交易记录里的交易费分配给自己。

比特币的诞生和区块链技术的不断发展给我们巨大的想象力。目前,互联网完成了信息的传递,而区块链技术或许可以为互联网带来价值的传递。区块链技术的基础设施、应用场景或许还需要一定的时间才可以发展到目前互联网技术的水平,但是区块链技术的潜力却不容小觑。用300行代码开发一个区块链系统
本节使用Node.js来实现一个简单的区块链系统,只需300行代码。
区块和区块链的创建
区块链是把区块用哈希指针连接起来的链条,区块是其中的基本单位。这里我们从设计一个区块的数据结构开始。

1.创建区块

区块是构建区块链的基本单位,一个区块至少要包含以下信息。

·index:区块在区块链中的位置。

·timestamp:区块产生的时间。

·transactions:区块包含的交易。

·previousHash:前一个区块的Hash值。

·hash:当前区块的Hash值。

其中,最后两个属性previousHash和hash是区块链的精华所在,区块链的不可篡改特性正是由这两个属性来保证的。

根据上面的信息,我们来创建一个Block类
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();
  }
  // 计算区块的哈希值
  calculateHash() {
    return SHA256(this.index + this.previousHash + this.timestamp + JSON.stringify(this.transactions) + this.nonce).toString();    
  }
  // 添加新的交易到当前区块
  addNewTransaction(sender, recipient, amount) {
    this.transactions.push({
      sender,
      recipient,
      amount
    })
  }
  // 查看当前区块里的交易信息
  getTransactions() {
    return this.transactions;
  }
}

  在上面的Block类的实现中,我们实用了crypto-js里的SHA256来作为区块的哈希算法,这也是比特币中使用的哈希算法。transactions是一系列交易对象的列表,其中包含的每笔交易的格式为:

{
  sender: sender,
  recipient: recipient,
  amount: amount
}

另外我们给Block类添加了三个方法:calculateHash、addNewTransaction、get-Transactions,分别用来计算当前区块哈希、增加新交易到当前区块、获取当前区块所有交易。

区块构建完成后,下一步就是考虑如何把区块组装成一个区块链了。

2.创建区块链

一个区块链就是一个链表,链表中的每个元素都是一个区块。区块链需要一个创世区块(Genesis Block)来进行初始化,这也是区块链的第一个区块,需要手工生成。在我们创建Blockchain类时,需要考虑到创世区块的生成。以下是代码示例:

class Blockchain {
  constructor() {
    this.chain = [this.createGenesisBlock()];
  }
  // 创建创始区块
  createGenesisBlock() {
    const genesisBlock = new Block(0, "01/10/2017");
    genesisBlock.previousHash = '0';
    genesisBlock.addNewTransaction('Leo', 'Janice', 520);
    return genesisBlock;
  }
  // 获取最新区块
  getLatestBlock() {
    return this.chain[this.chain.length - 1];
  }
  // 添加区块到区块链
  addBlock(newBlock) {
    newBlock.previousHash = this.getLatestBlock().hash;
    newBlock.hash = newBlock.calculateHash();
    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];

      // 验证当前区块的 hash 是否正确

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

      // 验证当前区块的 previousHash 是否等于上一个区块的 hash
      if(currentBlock.previousHash !== previousBlock.hash){
        return false;
      }
    }
    return true;
  }
}

在Blockchain这个类中,我们实现了一个创建创世区块的方法。由于创世区块中并没有前一个区块,因此previousHash设置为0。另外假定这一天是Leo和Janice的结婚纪念日,Leo给Janice转账520个代币,由此产生了一笔交易并记录到创世区块中。最后我们把这个创世区块添加到构造函数中,这样区块链就包含一个创世区块了。方法getLatestBlock和addBlock含义比较明显,含义分别是获取最新区块和往区块链中添加新的区块。最后一个isChainValid方法是通过验证区块的哈希值来验证整个区块链是否有效,如果已经添加到区块链的区块数据被篡改,那么该方法则返回为false。我们会在下一部分对此场景进行验证。

3.对区块链进行测试

到现在为止,我们已经实现了一个最简单的区块链了。在这一部分,我们会对创建的区块链进行测试。方法是向区块链中添加两个完整的区块,并且通过尝试修改区块内容来展示区块链的不可篡改的特性。

我们先创建一个名字叫作testCoin的区块链。使用Blockchain类新建一个对象,此时它应该只包含创世区块:

const testCoin = new Blockchain();
console.log(JSON.stringify(testCoin.chain, undefined, 2));

运行该程序,结果为:

[
  {
    "index": 0,
    "timestamp": "01/10/2017",
    "transactions": [
      {
        "sender": "Leo",
        "recipient": "Janice",
        "amount": "520"
      }
    ],
    "previousHash": "0",
    "hash": "23975e8996cd37311c7fd0907f9b2511c3bf23cf9c9147cca329dec76d7b544e"
  }
]

然后,我们新建两个区块,每个区块里都包含一笔交易。然后把这两个区块依次添加到testCoin这个区块链上:

lock1 = new Block('1', '02/10/2017');
block1.addNewTransaction('Alice', 'Bob', 500);
testCoin.addBlock(block1);

block2 = new Block('2', '03/10/2017');
block2.addNewTransaction('Jack', 'David', 1000);
testCoin.addBlock(block2);
console.log(JSON.stringify(testCoin.chain, undefined, 2));

可以得到以下结果:


  {
    "index": 0,
    "timestamp": "01/10/2017",
    "transactions": [
      {
        "sender": "Leo",
        "recipient": "Janice",
        "amount": 520
      }
    ],
    "previousHash": "0",
    "hash": "23975e8996cd37311c7fd0907f9b2511c3bf23cf9c9147cca329dec76d7b544e"
  },
  {
    "index": "1",
    "timestamp": "02/10/2017",
    "transactions": [
      {
        "sender": "Alice",
        "recipient": "Bob",
        "amount": 500
      }
    ],
    "previousHash": "23975e8996cd37311c7fd0907f9b2511c3bf23cf9c9147cca329dec76d7b544e",
    "hash": "32b96fa0bba9a7353e67498d822fb0c1f89c307098295c288459cb44dbc5d0f1"
  },
  {
    "index": "2",
    "timestamp": "03/10/2017",
    "transactions": [
      {
        "sender": "Jack",
        "recipient": "David",
        "amount": 1000
      }
    ],
    "previousHash": "32b96fa0bba9a7353e67498d822fb0c1f89c307098295c288459cb44dbc5d0f1",
    "hash": "3a0b9a0471bb474f7560968f2f05ff93306cfc26be7f854a36dc4fea92018db2"
  }
]

testCoin现在包含三个区块,除了一个创世区块以外,剩下的两个区块是我们刚刚添加的。注意每一个区块的previousHash属性是否正确地指向了前一个区块的哈希值。

此时我们使用isChainValid方法可以验证该区块链的有效性。console.log(testCoin.isChainValid())的返回结果为true。

区块链的防篡改性体现在哪里呢?我们先来修改第一个区块的交易。在第一个区块中,Alice向Bob转账500元,假设Alice后悔了,她只想付100元给Bob,于是修改交易信息如下:

block1.transactions[0].amount = 100;
console.log(block1.getTransactions())

Alice查看区块链的交易信息,发现已经改成了100元,放心地走了。Bob看到后,发现交易遭到了篡改。于是Bob开始收集证据,他怎么证明block1的那笔交易是被人为篡改后的交易呢?Bob可以调用isChainValid方法来证明目前的testCoin是无效的。因为testCoin.isChainValid()返回值为false。但是testCoin.isChainValid()为什么会返回false呢?我们来看一下背后的逻辑:首先Alice修改了交易的内容,这个时候block1的哈希值肯定和通过之前交易计算出的哈希值是不同的。这两个值的不同会触发isChainValid返回为false,也就是如下代码实现的功能:

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

既然如此,Alice在修改交易内容的同时修改block1的hash不就可以了吗?Alice可以继续篡改其他的区块内容:

block1.transactions[0].amount = 100;
block1.hash = block1.calculateHash();
console.log(testCoin.isChainValid())
这样的话,最后的结果依然是false。为什么呢?是因为下面这段代码:

if(currentBlock.previousHash !== previousBlock.hash){
  return false;
}
每一个区块都存储了上一个区块的哈希值,只修改一个区块是不够的,还需要修改下一个区块存储的previousHash。如果我们已经安全地存储了block2的哈希值,那无论如何Alice都是不可能在不被发现的情况下篡改已有数据的。在真实的区块链项目中,修改一个区块必须修改接下来该区块之后的所有区块,这也是无法办到的事情。区块链的这个“哈希指针”的特性,保证了区块链数据的不可篡改性。




工作量证明#

实现的区块链系统还比较简单,并且没有解决电子货币系统中需要解决的 “双重支付” 问题。要想维持整个系统健康运转,需要在系统中设计一定的经济激励机制。在比特币体系中,中本聪就设计了一个 “工作量证明” 的机制,解决了系统里的经济激励问题以及双重支付问题。下面我们介绍工作量证明算法的原理和实现。

1. 工作量证明算法

一个健康运行的区块链系统随时会产生交易,我们需要有服务器进行以下工作:定时把一个时间段(比特币是 10 分钟,Asch 是 10 秒)的交易打包到一个区块,并且添加到现有的区块链中。但是一个区块链系统中可能有很多台服务器,究竟是以哪台服务器打包的区块为准呢?为了解决这个问题,比特币中采用了一种叫做工作量证明的算法来决定采用哪一台服务器打包的区块并且给予相应的奖励。

工作量证明算法可以简单地描述为:在一个时间段同时有多台服务器对这一段时间的交易进行打包,打包完成后连带区块 Header 信息一起经过 SHA256 算法进行运算。在区块头以及奖励交易 coinbase 里各有一个变量 nonce,如果运算的结果不符合难度值(稍后会解释这个概念)要求,那么就调整 nonce 的值继续运算。如果有某台服务器率先计算出了符合难度值的区块,那么它可以广播这个区块。其他服务器验证没问题后就可以添加到现有区块链上,然后大家再一起竞争下一个区块。这个过程也称为 “挖矿”。

工作量证明算法采用了哈希算法 SHA256,这种算法的特点是难以通过运算得到特定的结果,但是一旦计算出来合适的结果后则很容易验证。在比特币系统里,找到一个符合难度要求的区块需要耗费 10 分钟左右,但是验证它是否有效却是瞬间的事。在下一节的代码实现里我们会看到这一点。

举一个简单的例子:假设有一群人玩一个扔硬币游戏,每个人有十枚硬币,依次扔完十枚硬币,最后看十枚中的正面和反面的排序结果。由于最后的结果是有顺序的,结果总有 210 种可能。现在有个规定,在一轮游戏中,谁先扔出了前 4 枚硬币都是正面的结果,谁就可以得到奖励。于是大家都开始扔十枚硬币并统计结果。前四枚都是正面的可能性有 26 种,因此一个人能获取该结果的期望尝试次数为 24。如果规定正面的个数越多,那么每个人的尝试次数就会越多,而这里的个数就是难度。如果玩游戏的人逐渐增多,那我们就可以要求结果的前 6 个、前 8 个是正面,这样每轮游戏的时间依然差不多。这也是为什么比特币的算力大增,而依然能保持平均每 10 分钟产生一个区块的原因。

上面阐述了什么是工作量证明,那如何把它添加到我们的区块链应用中呢?

任何一个数据经过 SHA256 运算后都会得到长度为 256 位的二进制数值,我们可以通过调整最开始的部分连续 0 的个数作为 “难度值”。比如我们要求最后的区块经过 SHA256 运算后第一位为 0,那么平均每两次运算就会得到一个这样的结果。但是如果我们要求连续 10 位都是 0,那就需要平均计算 210 次才能得到一次这样的结果了。系统可以通过调整计算结果里连续 0 的个数来达成调整难度的目标。

我们在区块的头信息中添加一个变量 nonce。通过不停地调节 nonce 的值来重新计算整个区块的哈希值,直到计算的结果满足难度要求。

2. 工作量证明的代码实现

基于上节的概念,我们开始改造现在的区块链应用。首先在 Block 类添加一个 nonce 变量:

class Block {
  constructor(index, timestamp) {
    this.index = index;
    this.timestamp = timestamp;
    this.transactions = [];
    this.previousHash = '';
    this.hash = this.calculateHash();
    this.nonce = 0;
  }
  ...
}

然后在 Block 类中添加一个 mineBlock 方法:

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);
  }

方法 mineBlock 就是根据难度值来寻找 nonce,只有找到合适的 nonce 之后才可以提交区块。这里的 difficulty 指的是结果里从开头连续为 0 的个数。如果计算出来的哈希值不符合要求,那么 nonce 加 1,然后重新计算区块的哈希值。

于是,我们在 Blockchain 类里定义一个难度值:

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

把挖矿的过程应用到添加区块到区块链的过程中:

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

到此为止,我们对应用的改造就完成了。下面对这部分添加后的代码进行测试。

我们先只添加一个区块


block1 = new Block('1', '02/10/2017');
block1.addNewTransaction('Alice', 'Bob', 500);
testCoin.addBlock(block1);

console.log(block1)

运算结果为:

Mining block 1
BLOCK MINED: 005fed00324fcbe1f0ab1703afe94e45a99e197a7df142e669444687f9513e57
Block {
  index: '1',
  timestamp: '02/10/2017',transactions: [ { sender: 'Alice', recipient: 'Bob', amount: 500 } ],
  previousHash: '31b15cc32d6772f237dcf298d5b7a2417f298f40ce6d8d5fbe07958141df7a4c',
  hash: '005fed00324fcbe1f0ab1703afe94e45a99e197a7df142e669444687f9513e57',
  nonce: 419 
      }

注意那个 nonce 值以及 hash 值。nonce 值表明了计算次数,hash 值是最后得到的结果。这次我们设置的难度值为 2,期望计算次数是 28 次(hash 里一个字符代表 4 位)。如果把难度值改成 3 呢?运算结果为:

Mining block 1
BLOCK MINED: 000b7f17beaf58bc8fea996a9fed11103ed27ad6d63818b87d89a440cd9757b5
Block {
  index: '1',
  timestamp: '02/10/2017',
  transactions: [ { sender: 'Alice', recipient: 'Bob', amount: 500 } ],
  previousHash: '31b15cc32d6772f237dcf298d5b7a2417f298f40ce6d8d5fbe07958141df7a4c',
  hash: '000b7f17beaf58bc8fea996a9fed11103ed27ad6d63818b87d89a440cd9757b5',
  nonce: 4848
     }

可以看到,计算的次数增加了。随着难度值增大,CPU 计算的次数也会呈指数级增加,相应耗费的时间也就越长。

提供和区块链进行交互的 API#

1. 挖矿奖励

在实现相关 API 之前,我们首先来看一下什么是挖矿奖励。

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

这里面有几个问题。首先,奖励金额的问题。比特币刚开始发行时,每个区块的奖励是 50BTC,其后每隔四年时间减半,2018 年 7 月已经是 12.5 个 BTC 了。其次,矿工能否创建多笔奖励交易或者加大奖励金额?矿工当然可以这么干,但是这么做以后广播出去的区块是无法通过其他节点验证的。其他节点收到区块后会进行合法性验证,如果不符合系统的规则就会丢弃该区块,而该区块最终也不会被添加到区块链中。

2. 代码重构

为了把我们当前的代码改造成适合通过 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 的代码。

3. 使用 Express 提供 API 服务

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

・POST/transactions/new:添加新的交易,格式为 JSON。

・GET/mine:将目前的交易打包到新的区块。

・GET/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.error(err);
  });
  next();  
})
  res.send(`The transaction 
${JSON.stringify(newTransaction)} is successfully added to the blockchain.`);

测试 API

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

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

可以看到,当前区块链只有一个区块。那怎么添加新的交易呢?

参考资料:

·Implementing proof-of-work

https://www.savjee.be/2017/09/Implementing-proof-of-work-javascript-blockchain/

·Learn Blockchains by Building One

https://hackernoon.com/learn-blockchains-by-building-one-117428612f46

·Building Blockchain in Go

https://jeiwan.cc/posts/building-blockchain-in-go-part-2/

·Bitcoin whitepaper

https://bitcoin.org/bitcoin.pdf

智能合约#

通过前面两章介绍我们初步认识了智能合约及以太坊的核心概念,从这一 章开始将逐步介绍用 Solidity 编写智能合约。本章将介绍一个合约通常包含哪 些内容。我们将分两个角度来讨论,一是从 Solidity 合约文件结构的角度:二 是从合约内容的角度。 Solidity 文件结构 Solidity 合约源文件使用的扩展名为 “sol”。从文件结构上看,一个合约文 件通常包含以下几个部分:合约版本声明、引入其他源文件、定义一个合约及 注释等。 合约版本声明 Solidity 的源文件需要进行版本声明,告知编译器此源文件所支持的编译器 版本,当出现不兼容的新的编译器版本时,它会拒绝编译旧的源文件。经常阅 读版本更新日志是一个好习惯,尤其是当大版本发布时。

版本声明方式如下: pragma solidity ^0.4.0: 这样一个源文件不兼容 Solidity0.4.0 之前的版本和 Solidity0.5.0 之后的版本 (“” 符号用来控制版本号的第 2 部分)。通常版本号第 3 部分的升级仅仅是

一些小变化(不会有任何兼容性问题),所以通常使用这种方式,而不是指定特 定的版本。这样当编译器有 bug 要修复时,不需要更改代码。 如果要使用更复杂的版本声明,那么其声明表达式和 npm 要保持一致,可 以参考:https:/docs.npmjs.com/misc/semver. 引入其他源文件 Solidity 支持 import 语句,类似于 JavaScript (ES6), 但 Solidity 没有 “缺省导出” 的概念。 全局引入,引入形式如下:

import "filename";

从 “filename” 引入所有的全局符号 (包括 filename 从其他文件引入的) 到
当前的全局作用域。
自定义命名空间引入,引入形式如下:

import as symbolName from "filename";

创建一个全局的命名空间 symbolName,. 成员来自 filename 的全局符号。
有一种非 ES6 兼容的简写语法与其等价:

import {symbol1 as alias,symbol2}from "filename";

image

Solidity 数据类型#

Solidity 是一种静态类型语言,这一章我们将深入介绍 Solidity 的数据类型。
主要内容包括:
・类型概述及分类
・布尔类型
・整型
・定长浮点型
定长字节数组
・有理数和整型常量
・字符串常量
十六进制常量
枚举

函数类型

地址类型
・地址常量
数据位置
数组
结构体
映射
・类型转换

类型推导 运算符 类型概述及分类 Solidity 是一种静态类型语言,常见的静态类型语言有 C、C++、Java 等, 静态类型意味着在编译时需要为每个变量(本地或状态变量)都指定类型(或 至少可以推导出类型)。 Solidity 数据类型看起来使用很简单,但却是最容易产生漏洞的地方(如出现溢出等问题)。

有一点大家也需要关注,就是 Solidity 的类型非常在意所占空 间的大小。另外,Solidity 的一些基本类型可以组合成复杂类型。 Solidity 类型分为两类:值类型 (Value Type) 和引用类型 (Reference Type).

另外,不同类型还可以与不同的运算符组合,支持表达式运算,并按照表 达式的执行顺序 (Order of Evaluation of Expression) 来执行。 值类型 值类型所占空间在 32 个字节以内,值类型变量在赋值或传参时总是进行值 拷贝。

值类型包括:・布尔类型 (Boolean)・整型 (Integer)・定长浮点型 (Fixed Point Number) 定长字节数组 (Fixed-size Byte Array) 有理数和整型常量 (Rational and Integer Literal) 字符串常量 (String Literal) 十六进制常量 (Hexadecimal Literal) 枚举 (Enum)・函数类型 (Function Type) 地址类型 (Addres) 地址常量 (Address Literal) 引用类型 引用类型主要包括:数组 (Array)、结构体 (Struct) 和映射 (Mapping)。

布尔类型 (Boolean) 布尔类型使用 bool 关键字声明,声明方式如下: bool isActive; boo1is0k=false;/ 带默认值 布尔类型可能的取值为常量值 tnue 和 false.

布尔类型支持的运算符如下。・!, 逻辑非。・&&, 逻辑与。・, 逻辑或。・=, 等于。・=, 不等于。 注意:运算符 “&&” 和 “川” 是短路运算符,如 fx) 川 gy), 当 fx) 为真时, 则不会继续执行 gy))&&gy), 当 fx) 为假时,则不会继续执行 gy) 整型 (Integer) 和 Java 等语言用 short、.int、long 来表示整型有些不一样,Solidity 的整型 用 it 加一个类型所占位数的数字一起来表示,这种方式和 Go 语言一致。

整型的关键字有 int8、imt16 到 int256, 数字以 8 步进。

对应的无符号整型 有 uint8 到 uint256,uint 和 int 默认对应的是 uint256 和 int256. 声明方式如下: 1nt8×=-1; 1nt16y=2; int32 z 整型支持的运算符如下。

・比较运算符:<=、<、=、I=、>=、>(返回布尔值 tue 或 false).

位运算符:&、、A(异或)、~(位取反)。

算术运算符:+、一、一元运算符 “-”、一元运算符 “+”、*、人、%(取 余数)、(幂)、<<(左移位)、>(右移位)。

说明:

・整数除法总是截断的,但如果运算的是常量(常量稍后讲),则不会截断。・整数除 0 会抛异常,即 x0 为非法的。・右移位和除是等价的,如 x>y 和 x/2**y 是相等的。左移位和乘 等价,x<y 和 x2y 是相等的。移位运算的结果的正负取决于运算符左边的数。右移位一个负数,向下取整时会为 0.・不能进行负移位,即运算符右边的数不可以为负数,否则会抛出运行 时异常,如 3>-1 为非法的。 我门多手看下而一个别子

image

运算符简写

和 C、C++、Java 类似,对于一些运算符运算赋值支持以下简写。a+=e 等价于 a=a+e, 类似的操作符有:-=、*=、仁、%=、=、&=、=。

a + 和 a 一等价于 a+=1 和 a=1, 但表达式仍为 a 的值。而一 a 和 ++a 则返回更改之后的值。 整型溢出问题 在使用整型时,要特别注意整型的大小及所能容纳的最大值和最小值,很 多合约就是因为溢出问题导致了漏洞,比如美链的漏洞,大家可以参考博客文 章 (https:/learnblockchain.cn/2018/04/25bec-overflow/) 了解。

下面列举几个关于溢出问题的例子。

image

避免溢出的一个方法是在运算之后对结果值进行一次检查,比如对上面的
k 做一个检查,如使用 assert (k>=i)。也推荐大家在进行加、减、乘、除运算时
使用 OpenZeppelin 的 SafeMath 库,代码的 GitHub 地址为 https:/github.coml
OpenZeppelin/openzeppelin-solidity/blob/master/contracts/math/SafeMath.sol.

定长浮点型 (Fixed Point Number)#

定长浮点型的功能和其他语言的浮点型 float 和 double 差不多,都是用来表 示浮点数的。但是定长浮点型又有不一样的地方,它需要在声明时指定类型所 占的大小,以及小数点的位数,而传统的浮点型 float 和 double 通常值会不一样, 即所占用的空间不一样。 Solidity 还不完全支持定长浮点型,它可以用来声明变量,但不可以用来赋值,因此当前还没法使用定长浮点型。 定长浮点型的声明方式如下:

ufixed32x1 f;
ufixed32x1 fi=0.1;//UnimplementedFeatureError:Not yet implemented

fixed/ufixed 表示有符号和无符号的固定位浮点数。关键字为 ufixedMxN 和 ufixedMxN, 其中 M 表示这种类型要占用的位数,以 8 步进,可以为 8~256 位:N 表示小数点的个数,可以为 0~80. fixed/ufixed 分别代表 fixed128x18 和 ufixed1:28xl8。

其支持的运算符如下。

・比较运算符:<=、<、=、!=、>=、>(返回布尔值 tnue 或 false).

算术运算符:+、一、一元运算符 “_”、一元运算符 “+”、*、人、%(取 余数)。 注意:它和大多数语言的 float 和 double 不一样,这里的 M 表示整个数占 用的固定位数,包含整数部分和小数部分。因此,当用一个小位数 (M 较小) 来表示一个浮点数时,小数部分几乎会占用整个空间。

定长字节数组 (Fixed-size Byte Array)#

定长字节数组是指一个所占空间固定的数组,每个元素都是一个字节。由
于变长数组不是值类型,因此我们会用单独一节来介绍。
定长字节数组的声明方式如下:

byte bte;
bytes1 bt1 0x01;
bytes2 bt2 "ab";
bytes3 bt3 "abc";

关键字有 bytes1,bytes?2,bytes3.,…,bytes32, 以步长 1 递增,byte 代表 bytes1. 在实际使用中,定长字节数组经常被用来代替字符串(如声明中的 b2 及 bt3). 定长字节数组支持的运算符如下。

  • ・比较运算符:<=、<、一、!=、>=、>(返回布尔值 tue 或 falsa).

  • ・位运算符:&、、(按位异或)、~(按位取反)、<<(左移位)、>> (右移位)。

  • 索引(下标)访问:如果 x 是 bytesI, 当 O≤k<I 时,则 xk] 返回第 k 个字节(只读)。 移位运算和整型类似,移位运算结果的正负取决于运算符左边的数,且不 能进行负移位。例如可以 - 5<<1,不可以 5<<-1. 定长字节数组有成员变量:。length, 表示这个字节数组的长度(不能修改)。

  • 例如获取 bt2 和 bt3 的长度:
    bt2.1 ength;/ 返回 2
    bt3.length;/ 返回 3
    有理数和整型常量 (Rational and Integer Literal)
    有理数和整型常量是表达式中直接出现的数字常量,不过也有人把它称为
    字面量。

  • 整型常量是由一系列 09 的数字组成的,以十进制数表示。比如:八进制 数是不存在的,前置 0 在 Solidity 中是无效的。十进制小数常量 (Decimal Fraction Literal) 带了一个 “。”,在 “。” 的两边 至少有一个数字,有效的表示如 1、1 和 13 等。 它也支持科学符号,基数可以是小数,指数必须是整数,有效的表示如 210、 -2e10、2e-10、2.5e1 等。 数字常量表达式本身支持任意精度,也就是不会发生运算溢出或除法截断。 但是当它被转换成对应的非常量类型,或者将它与非常量进行运算时,就不能 保证精度了。比如 (2800+1)-2**800 的结果为 1 (uint8 整型,尽管中间结果 己经超过计算机字长):58 的结果是 4(尽管有非整型数参与了运算):5/2+

  • 52 的结果是 5 (会转为有理数,不会被截断,不过在早期的版本中会被截断)。只要操作数是整型,整型支持的运算符就适用于整型常量表达式。如果两 个操作数都是小数,则不允许进行位运算,指数也不能是小数。 示例代码如下

: pragma solidity ^0.4.18; 
         contract testType2{
        function interLiteral()
        public returns (uint,uint){ return(2800+1)-2880,0.5*8);

       }
   }
  • 注意:Solidity 的每一个数字常量都有对应的数字常量类型,整型常量和有
    理数常量属于数字常量类型。所有的数字常量表达式的结果都是数字常量。
    在数字常量表达式中一旦含有非常量表达式,它就会被转换为非常量类型。
Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.