从黑帮电影理解区块链

208 阅读10分钟

本文尝试回答如下几个问题:

  • 什么是区块链?
  • 区块链解决了什么问题?
  • 区块链和比特币有什么关系?
  • 比特币的交易流程

本文从「基于信用的交易流程」开始,阐述「基于信用的交易」所存在的问题,基于这些问题,一点点的改进,得到比特币的交易流程,并使用kotlin实现一个简单的区块链,加深理解。

本文为个人理解,如果偏差,欢迎指正!

一手交钱一手交货

在很多黑帮电影里,都有这么个场景,两帮人在地下交易,我们用流程图描述的话就像这样:

举这个列子是因为这种流程下不可控因素比较多,方便描述,我可是个遵纪守法的好市民。如果觉得会教坏小朋友,可以想成转账流程。

![从黑帮电影理解区块链](https://p1-tt.byteimg.com/origin/pgc-image/b9f52eabed4f4533a2d4584e80a373a7?from=pc)

这其实有很多问题(电影里都有过):

  • 黑吃黑,一方把另一方干掉,直接拿了钱和货
  • 被警察一锅端了
  • 半道出个劫财的

怎么解决这个问题呢?可以找个有威望的中间人,由中间人来见证交易,如果谁反悔就没法在道上混了。流程就变成了这样:

![从黑帮电影理解区块链](https://p3-tt.byteimg.com/origin/pgc-image/6a92c2ac0586459ca490454dd5340820?from=pc)

中间人的作用是什么?是为了解决信任问题。A不信任B,B也不信任A,但是A,B都信任C,相信C不会坑自己。

但是这里实际是有「单点问题」的,即A和B的交易是建立在对C的信任之上的,如果C的信用出现了问题,双方的交易就会出现问题。假设C缺钱了,从中抽了一点钱。然后告诉B,A就给了这么多钱。那是不是又要干起来了?(这种情况电影里也有过)

同时这也解决不了半路打劫的问题,虽然C威望很高,但是可能有胆子大的就去打劫C了呢。(这个电影里也有)

那怎么解决这些问题呢?

保险箱

首先是「打劫」问题,既然没办法保证没人去「打劫」,那就对钱和货保个险。钱和货都放在保险箱里。流程就变成:

![从黑帮电影理解区块链](https://p3-tt.byteimg.com/origin/pgc-image/a70df2739c764aa796d03e0e3f358f92?from=pc)

这样就解决了「半路打劫」的问题,东西被抢了,打不开保险箱,也没用,白抢。但是有可能有走运的,把密码给蒙出来了。怎么办呢?我在货和钱上打烙印,包括当前拥有者是谁和下一个拥有者是谁!这样,你即使拿到了东西,东西也不是你的。比如这钱原来是陈浩南的,现在给山鸡,大B即使抢过来了,也用不了。

这就是比特币的做法,比特币的拥有者根据前一次交易和下一个拥有者的公钥签署一个随机散列的数字签名,并把这个签名加到这枚比特币的末尾,然后把比特币发给下一位拥有者。而收款人可以通过验证这个数字签名来确认这枚比特币的整个拥有者链条。

![从黑帮电影理解区块链](https://p1-tt.byteimg.com/origin/pgc-image/188f52a88b2d4ab1aa70e6f8014cade8?from=pc)

解决了「拦路抢劫」问题,那怎么解决「单点问题」呢?因为东西都在C那里,C做什么A和B都不知道!

从单点到分布式一文中已经给出了答案,就是集群和分布式。就是说对C的信任,转成对普罗大众的信任。原来A和B交易,只有C知道。现在的流程就相当于将交易公布于众(喂,这还算地下交易吗?)。

![从黑帮电影理解区块链](https://p1-tt.byteimg.com/origin/pgc-image/06ae1ff56df842a8b4090104a583ad8c?from=pc)

假设B翻脸了,说没有收到钱。那吃瓜群众就会站出来说:你不厚道,你明明已经收到钱了!这些「吃瓜群众」就是区块链的雏形

也就是说,只要让吃瓜群众知道A和B交易了,就可以了。从这个层面来看,其实就回到了分布式一致性问题上了。在讨论这个问题之前,先来看一下「拜占庭将军」问题。

拜占庭将军问题

拜占庭将军问题说的是:拜占庭将军们率领军队去攻打敌军,将军们各自占领山头,通过投票的方式来决定是否发动进攻。但是将军中有叛徒,可能会发出错误的消息,这就导致将军们无法达成一致。比如,有5个将军,其中有一个将军是叛徒,现在两个将军要进攻,两个将军要撤退,这一个叛徒就可以任意支配最终投票结果。

拜占庭将军问题(Byzantine failures),是由莱斯利·兰伯特提出的。要说明的是「在存在消息丢失的不可靠信道上试图通过消息传递的方式达到一致性是不可能的」。因此对一致性的研究一般假设信道是可靠的,或不存在问题的

从单点到分布式中提到的一致性算法,比如Paxos,Raft都是在「信道可靠」这个基础上的。

区块链

对于一般的分布式系统来说,一致性一般都是在内部通信上,可以假设通信可靠。但是区块链是在公网上,无法保证「信道可靠」;而且因为涉及到金钱,所以还可能有人为破坏的情况。比如上面的交易,A先把钱给了B,告诉了吃瓜群众;然后又把钱给了D,又告诉了吃瓜群众。吃瓜群众就会记录A分别给了B和D钱,A就这么轻轻松松的进行了一次假交易。(你可以用Paxos或者Raft来尝试看看,是否能避免这个问题?)

除了上面的问题,还可能出现:

  • 网络延迟,吃瓜群众延迟了半小时才听到
  • 网络断了,有吃瓜群众听不到了
  • 有黑客篡改交易记录了

比特币使用「时间戳服务器」和「工作量证明」来解决这些问题。

首先来说「时间戳服务器」,它和谷歌分布式数据库Spanner里提到的时间戳不同,Spanner里的「时间戳」是基于全球时钟的;而比特币里的「时间戳」是基于随机散列的,这个随机散列包含了一组交易数据、以及前一个时间戳。包含了这些信息的数据称为block,也就是区块。基于这个时间戳的关系,就构成了一个链条,通过这个链条就确定了先后顺序。这个链条就称为区块链。虽然这里叫「时间戳」,实际上和时间没有太大的关系。

![从黑帮电影理解区块链](https://p1-tt.byteimg.com/origin/pgc-image/4108253410be4f36a0ebb432ad690827?from=pc)

上面解决了顺序问题,但是没有解决「作恶」问题。如果A找了一帮假的吃瓜群众,记录已经将钱给B了,那么A可以生成最长的那个区块链,从而替换正确的那个区块链,相当于A控制了群众舆论。所以整个区块链网络又引入了「工作量证明」来解决这个问题。如何做的呢?

一般来说,我们取一个数据的hash,可以通过类似hash(data,key)的方式来获取,速度很快。但是反过来,给你hash值,去算这个key,就没有什么好办法了,只能去猜。也就是说谁要创建区块,那么就需要先去猜这个key是什么。比方说SHA-256下,设置随机散列值需要以n个0开始,随着0的数目的上升, 找到这个解所需要的工作量将呈指数增长,而对结果进行检验则仅需要一次随机散列运算。比特网络就可以通过控制以几个0开头,控制计算出一个结果的时间。如果整体网络算力大,就增加难度;反之降低难度。一般比特币网络会控制十分钟左右算出一个值,继而创建出一个区块。也就是说,一笔交易在区块链中最快也要十分钟才能完成。

某个节点算出值(称为Nonce)以后,将其包含到区块中,广播到整个网络,其它节点收到以后,验证ok后,将其加到自己的区块链最后,然后开始计算下一个值。

![从黑帮电影理解区块链](https://p3-tt.byteimg.com/origin/pgc-image/7e7af26430ef442f8893977b65c20a7e?from=pc)

这个工作量如何解决「作恶」问题呢?假设现在有11,12,13号区块,11号区块记录了AB之间的交易,A对11号区块进行修改,又改成AD之间的交易,这就分叉出了了12A,然后A还要继续计算13A,这个时候主链可能已经有了14,15区块,也就是说只要某个人的算力达不到整体网络算力的一半以上,他就永远无法赶上主链,赶不上就没办法替代主链。

这虽然解决了共识问题,但是浪费了很多计算资源(另外一种方式叫PoS,所有节点通过质押代币的方式参与共识,这里不展开)。那别人凭什么要浪费资源帮你去证明交易呢?因为有利可图!

第一个计算出结果的节点,可以得到奖励。上面A,B交易,需要给第一个记录的吃瓜群众小费。所以吃瓜群众又称为「矿工」,计算值的过程称为「挖矿」。

完整流程

  • A,B交易,并告诉所有节点
  • 每个节点收到以后,验证交易正确性,如果正确,放到「未确认交易池」中
  • 然后去算猜数字
  • 当其中一个节点找到了结果,就从「未确认交易池」中选择一部分交易,打包成区块,通知所有节点
  • 其它节点接收到区块后,校验哈希及区块中的所有交易,校验成功则接受此区块
  • 将此区块添加到自己的区块链的最后,并使用此区块的hash来计算下一个值

这个流程怎么解决双花问题呢?

  • A,B交易,最快要十分钟以后才能完成交易
  • 完成后A伪造和C交易,但是因为
  • 节点是要校验交易的,
  • 这两个交易是有先后顺序的,
  • 交易是可以追溯的,在追溯交易时就发现,A已经没钱了,交易校验失败,这次交易就不会被记录了。

最后,实际上**「A」和「B」也是一个节点,也可以记录账本!因为在别人眼里,「A」和「B」也是吃瓜群众!**

演示代码

下面通过kotlin来演示一下这个区块链流程!

首先是区块的结构:

data class Block(
 val index: Int = 0,
 val previousHash: String = "",
 val timestamp: Long = 0,
 val data: String = "",
 val hash: String = "")

每个节点需要维护一个账本,也就是区块链:

private var blockChain: MutableList<Block> = ArrayList()

整个网络开始时,需要一个「创世区块」:

private val fristBlock: Block
 get() =
 Block(1, "0", System.currentTimeMillis(), "EdenBlock", "816534932c2b7154836da6afc367695e6337db8a921823784c14378abed4f7d7")

现在A,B开始交易,通知吃瓜群众:

@RequestMapping(value = "/block", method = [(RequestMethod.POST)])
fun mineBlock(data: String): ResponseEntity<Block> {
 // data就是交易数据,放入「未确认交易池」
 // todo 先计算值
 // 从「未确认交易池」取出交易,创建区块
 val newBlock = blockService.generateNextBlock(data)
 // 加入到自身区块链中
 blockService.addBlock(newBlock)
 // 通知其它节点记录该区块
 p2pService.broatcast(p2pService.responseLatestMsg())
 return ResponseEntity.ok(newBlock)
}

其它吃瓜群众接收到区块了,要验证发过来的区块是否正确:

private fun isValidNewBlock(newBlock: Block, previousBlock: Block): Boolean {
 if (previousBlock.index + 1 != newBlock.index) {
 println("invalid index")
 return false
 } else if (previousBlock.hash != newBlock.previousHash) {
 println("invalid previoushash")
 return false
 } else {
 val hash = calculateHash(newBlock.index, newBlock.previousHash, newBlock.timestamp,
 newBlock.data)
 if (hash != newBlock.hash) {
 println("invalid hash: " + hash + " " + newBlock.hash)
 return false
 }
 }
 return true
}

如果正确的话,要将其添加到自己的账本里:

fun addBlock(newBlock: Block) {
 if (isValidNewBlock(newBlock, latestBlock)) {
 blockChain!!.add(newBlock)
 }
}

参考资料

  • Bitcoin: A Peer-to-Peer Electronic Cash System
  • A blockchain in 200 lines of code