Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

使用Elixir实现一条简易POW区块链

share.livemd

使用Elixir实现一条简易POW区块链

Why Elixir

> Elixir is a dynamic, functional language for building scalable and maintainable applications. > > 一种用于构建可伸缩、可维护应用的动态、函数式编程语言。 > > https://elixir-lang.org/

惊艳的语法

Elixir的语法在向Ruby致敬,同时透着Erlang灵气。 任何语言语法的设计都和其创始人的偏好和目标分不开,Elixir作者之前是做Rails社区核心开发者,所以语法上自然就很Ruby。 当然,植根于Erlang的Elixir,又有很多自己的特点。

最让人爱不释手的是pipe |>, 它把一层层的逆着思维的函数调用变成了更直观的表现,比如说我们常常这么写代码:

IO.puts(format(to_map(Store.get_host(host))))

or

list_data = Store.get_host(host)
map = to_map(list)
formatted_output = format(map)
IO.puts(formatted_output)

这样的代码在Elixir中可以被写成:

host
|> Store.get_host
|> to_map
|> foramt
|> IO.puts

非常清晰!最重要的是,它更符合你的思维模式,让代码更容易在指尖流淌。 我们写代码的时候,基本就是一个不断「分治」的过程:把大问题分解成小问题,小问题分解成更小的问题,最终解决问题。 而Elixir让你的代码和你的思路高度一致。

pipe非常灵活,你可以一边组织思路一边组合函数,有点搭积木的感觉。

模式匹配

凡是学过一点点编程的人应该都知道,= 是大多数编程语言的赋值操作符。 即便不是这样,也会有类似的一个符号,在它的左边是一个变量,右边是赋值给它的常量, 其它变量或一些运算,比如:a = 1 或者 a = max(number_list)。

但是,在 Elixir 里面,= 是匹配操作符。

iex>a = 1
1
iex>1 = a
1
iex>2 = a
** (MatchError) no match of right hand side value: 1

Elixir 的模式匹配和递归配合起来使用,实在是太爽了。 完全不必用到 if else 之流来判断边界值,代码表达得相当优雅。

我觉得,递归可以说非常好地体现了 First Principle 原则。只有看透数据处理的本质,才能理解递归。 我们来看看书中的一句霸气的话吧:

> L. Peter Deutsch once penned, “To iterate is human, to recurse divine.”

什么意思?“使用遍历的是普通人,能用递归的是神”。

举个例子感受一下:

defmodule Recursion do
  def sum(0), do: 0
  def sum(n), do: n + sum(n - 1)
end

IO.puts Recursion.sum(5)  # => 15

这个函数计算了某个给定数字之下的所有正整数之和。以上的例子就是计算 5+4+3+2+1+0

使用模式匹配取代大部分条件分支是件相当伟大的事情:代码的简洁自不必说, 其效率还有可能进一步优化。if/else是一种顺序执行的逻辑,因为其语法结构的灵活(if的条件里是个函数这事大家都干吧),顶多是对一些特殊的情况使用跳转表优化,大多数情况是O(N),而且很难并行处理。 而pattern matching由于其语法上的限制,很多情况可以被优化成决策树,时间复杂度是O(logN),而且未来还有并行处理的优化空间。

Macro

很多人看到 macro 眼前一黑,总觉得它代表了某种神秘的力量。

实际上,我们可以认为 macro 是 一个特殊的函数,这个函数接受的参数是语法树(一个或者多个),然后返回一个语法树。

defmodule 块是语法吗?不,它是一个宏。def 块是语法吗?不,它还是一个宏。

x in [1, 2, 3] # => true

这应该是一个语法了吧?不,连 in 都是宏。

天生的并发支持

Elixir 建立于 Erlang 之上,最终编译为完全等同的字节码模块,可互相调用。

Erlang的基于actor的并发模型,let it crash的处理思想,监督树,错误处理,都是在二十多年来与并发作斗争过程中不断总结出来的best practice,无论在思想上,还是实操上, 在可预见的未来,没有语言能够超越它。Elixir站在巨人的肩膀上,坐享其成。

服务周到的工具链

进入21世纪以来,新兴的语言在工具链上也都是卯足了劲,工具链(几乎)成为语言的一部分,而非附属品。

Elixir自身携带了mix —— 从项目的创建和scaffolding(mix new),编译(mix compile),到测试(mix test),到文档(mix doc),到依赖管理(mix deps),全部包圆。

> 以上内容参考:elixir:灵丹妙药?or 徒有其名?

Livebook

Livebook 是一个用于为Elixir编写交互式和协作性的代码笔记本。 可以类比Python的jupyter notebook。

https://livebook.dev/

Mix.install/2

我们可以通过Mix.install/2来安装依赖。

:force - if true, removes install cache. This is useful when you want to update your dependencies or your install got into an inconsistent state (Default: false)

:verbose - if true, prints additional debugging information (Default: false)

Mix.install(
  [:poison, :req]
  # force: true,
  # verbose: true
)
:ok
%{hello: "world", arr: [1, 2, 3]}
|> Poison.encode!()
|> IO.inspect()

"{\"hello\": \"world\",\"arr\": [1, 2, 3]}"
|> Poison.decode!()
|> IO.inspect()
"{\"hello\":\"world\",\"arr\":[1,2,3]}"
%{"arr" => [1, 2, 3], "hello" => "world"}
%{"arr" => [1, 2, 3], "hello" => "world"}
Req.get!("https://api.github.com/repos/elixir-lang/elixir").body["description"]
"Elixir is a dynamic, functional language designed for building scalable and maintainable applications"

可玩性很高,更多例子可以参考:https://github.com/wojtekmach/mix_install_examples

Blockchain介绍

区块

想要了解区块到底是什么,最简单快捷的办法就是分析它的数据结构,以 Bitcoin 中的区块 #514095 为例:

{
  "hash":"00000000000000000018b0a6ae560fa33c469b6528bc9e0fb0c669319a186c33",
  "confirmations":1009,
  "strippedsize":956228,
  "size":1112639,
  "weight":3981323,
  "height":514095,
  "version":536870912,
  "versionHex":"20000000",
  "merkleroot":"5f8f8e053fd4c0c3175c10ac5189c15e6ba218909319850936fe54934dcbfeac",
  "tx":[
    // ...
  ],
  "time":1521380124,
  "mediantime":1521377506,
  "nonce":3001236454,
  "bits":"17514a49",
  "difficulty":3462542391191.563,
  "chainwork":"0000000000000000000000000000000000000000014d2b41a340e60b72292430",
  "previousblockhash":"000000000000000000481ab128418847dc25db4dafec464baa5a33e66490990b",
  "nextblockhash":"0000000000000000000c74966205813839ad1c6d55d75f95c9c5f821db9c3510"
}

在这个 Block 的结构体中,previousblockhash 和 merkleroot 是两个最重要的字段; 前者是一个哈希指针,它其实是前一个 Block 的哈希,通过 previousblockhash 我们能递归地找到全部的 Block,也就是整条主链; 后者是一个 Merkle 树的根,Merkle 树中包含整个 Block 中的全部交易,通过保存 merkleroot,我们可以保证当前 Block 中任意交易都不会被修改。

Ethereum 的区块链模型虽然与 Bitcoin 有非常大的不同,但是它的 Block 结构中也有着类似的信息:

{
   "jsonrpc":"2.0",
   "result":{
      "author":"0x00d8ae40d9a06d0e7a2877b62e32eb959afbe16d",
      "difficulty":"0x785042b0",
      "extraData":"0x414952412f7630",
      "gasLimit":"0x47b784",
      "gasUsed":"0x44218a",
      "hash":"0x4de91e4af8d135e061d50ddd6d0d6f4119cd0f7062ebe8ff2d79c5af0e8344b9",
      "logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
      "miner":"0x00d8ae40d9a06d0e7a2877b62e32eb959afbe16d",
      "mixHash":"0xb8155224974967443d8b83e484402fb6e1e18ff69a8fc5acdda32f2bcc6dd443",
      "nonce":"0xad14fb6803147c7c",
      "number":"0x2000f1",
      "parentHash":"0x31919e2bf29306778f50bbc376bd490a7d056ddfd5b1f615752e79f32c7f1a38",
      "receiptsRoot":"0xa2a7af5e3b9e1bbb6252ba82a09302321b8f0eea7ec8e3bb977401e4f473e672",
      "sealFields":[
         "0xa0b8155224974967443d8b83e484402fb6e1e18ff69a8fc5acdda32f2bcc6dd443",
         "0x88ad14fb6803147c7c"
      ],
      "sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
      "size":"0x276",
      "stateRoot":"0x87e7e54cf229003014f453d64f0344e2ba4fc7ee3b95c7dd2642cca389fa1efe",
      "timestamp":"0x5a10968a",
      "totalDifficulty":"0x1804de0c47ffe1",
      "transactions":[...],
      "transactionsRoot":"0xc2091b032961ca23cf8323ea827e8956fe6dda9e68d75bcfaa8b910035397e35",
      "uncles":[]
   },
   "id":1
}

parentHash 和 transactionsRoot 分别对应着 Bitcoin 中 previousblockhash 和 merkleroot, 这两者在整个区块链网络中是非常重要的。

哈希指针

Block 结构体中的哈希指针在区块链中有两个作用,它不仅能够连接不同的区块,还能够对 Block 进行验证,保证 Block 中的数据不会被其他恶意节点篡改。

除了第一个 Block,每一个 Block 中的 prev_hash 都是前一个 Block 的哈希, 如果某一个节点想要修改主链上 Block 的交易,就会改变当前 Block 的哈希, 后面的 Block 就没有办法通过 prev_hash 找到前面的链, 所以当前节点篡改交易的行为就会被其他节点发现。

Merkle Tree

另一个字段 merkleroot 其实就是一个 Merkle 树 的根节点,它其实是一种使用哈希指针连接的数据结构;虽然 Merkle 树有叶节点和非叶节点, 但是它只有叶节点会存储数据,所有的非叶结点都是用于验证数据完整性的哈希。

每一个 Block 中的全部交易都是存储在这个 Merkle 树中并将 merkleroot 保存在 Block 的结构体中, 保证当前 Block 中任意交易的篡改都能被立刻发现。

小结

prev_hash 和 merkleroot 分别通过『指针』的方式保证所有的 Block 和交易都是连接起来的,最终保证 Block 和交易不会被恶意节点或攻击者篡改,几乎全部的区块链项目都会使用类似方式连接不同的 Block 和交易, 这可以说是区块链项目的基础设施和标配了。

> 以上内容参考:Draveness博客

创建区块

区块

一个基本的block格式如下:

%{
  :index => 0,
  :timestamp => "",
  :name => "",
  :previous_hash => "",
  :current_transactions => []
}
  • index:当前区块的索引
  • timestamp:生成区块的时间
  • name:区块的名字
  • previous_hash:上一个区块的hash
  • current_transactions:当前区块的交易

在这一点上,一个 区块链 的概念应该是明显的:每个新块都包含在其内的前一个块的散列。

这是至关重要的,因为这是 区块链 不可改变的原因: 如果攻击者损坏 区块链 中较早的块,则所有后续块将包含不正确的哈希值。 这有道理吗?如果你还没有想通,花点时间仔细思考一下。

这是区块链背后的核心理念。

程序基本结构如下:

defmodule Blockchain do
  defstruct chain: [], current_transactions: []

  def new_block(c, name) do
  end

  def zero() do
  end

  def last_block(c) do
  end

  def hash(block) do
  end
end
warning: variable "c" is unused (if the variable is not meant to be used, prefix it with an underscore)
  share.livemd#cell:4: Blockchain.new_block/2

warning: variable "name" is unused (if the variable is not meant to be used, prefix it with an underscore)
  share.livemd#cell:4: Blockchain.new_block/2

warning: variable "c" is unused (if the variable is not meant to be used, prefix it with an underscore)
  share.livemd#cell:10: Blockchain.last_block/1

warning: variable "block" is unused (if the variable is not meant to be used, prefix it with an underscore)
  share.livemd#cell:13: Blockchain.hash/1
{:module, Blockchain, <<70, 79, 82, 49, 0, 0, 9, ...>>, {:hash, 1}}

补全程序

补全new_block函数:

def new_block(c, name) do
    b = %{
      :index => length(c.chain),
      :timestamp => NaiveDateTime.utc_now(),
      :name => name,
      :previous_hash => c |> last_block() |> hash(),
      :current_transactions => c.current_transactions
    }

    %{c | chain: c.chain ++ [b], current_transactions: []}
end
  • 计算上一个区块的hash,写入previous_hash中
  • %{c | chain: c.chain ++ [b], current_transactions: []} 是一个语法糖,把当前区块 append 到链上。

Elixir的hash要用:crypto.hash函数来做,比如

value = "fenix"

:crypto.hash(:sha256, value)
|> Base.encode16()
|> String.downcase()
"b170f30d201da73b7637e94391f22a4642f8c396f9c1c9f8b2799919ced46d78"

完整的程序如下:

defmodule Blockchain do
  defstruct chain: [], current_transactions: []

  def new_block(c, name) do
    b = %{
      :index => length(c.chain),
      :timestamp => NaiveDateTime.utc_now(),
      :name => name,
      :previous_hash => c |> last_block() |> hash(),
      :current_transactions => c.current_transactions
    }

    %{c | chain: c.chain ++ [b], current_transactions: []}
  end

  def zero() do
    b = %{
      :index => 0,
      :timestamp => NaiveDateTime.utc_now(),
      :name => "ZERO",
      :previous_hash => "",
      :current_transactions => []
    }

    %Blockchain{
      chain: [b],
      current_transactions: []
    }
  end

  def last_block(c) do
    c.chain |> Enum.reverse() |> hd()
  end

  def hash(block) do
    value = block |> Poison.encode!()

    :crypto.hash(:sha256, value)
    |> Base.encode16()
    |> String.downcase()
  end
end
{:module, Blockchain, <<70, 79, 82, 49, 0, 0, 14, ...>>, {:hash, 1}}

试一试,生成新的区块

创世区块

zero = Blockchain.zero()
%Blockchain{
  chain: [
    %{
      current_transactions: [],
      index: 0,
      name: "ZERO",
      previous_hash: "",
      timestamp: ~N[2022-04-01 12:17:21.126190]
    }
  ],
  current_transactions: []
}

创建更多区块

zero
|> Blockchain.new_block("first")
|> Blockchain.new_block("second")
|> Blockchain.new_block("third")
%Blockchain{
  chain: [
    %{
      current_transactions: [],
      index: 0,
      name: "ZERO",
      previous_hash: "",
      timestamp: ~N[2022-04-01 12:17:21.126190]
    },
    %{
      current_transactions: [],
      index: 1,
      name: "first",
      previous_hash: "dd9a0b7a81ba060c2d1cd7e581ad0bda35c7019831049b39dbcc1c35a8f3f131",
      timestamp: ~N[2022-04-01 12:17:35.067392]
    },
    %{
      current_transactions: [],
      index: 2,
      name: "second",
      previous_hash: "f93e075bef13d86514a77d407e36ac9d3b2636769bfe483a3ecc4a2afd5b88ac",
      timestamp: ~N[2022-04-01 12:17:35.067467]
    },
    %{
      current_transactions: [],
      index: 3,
      name: "third",
      previous_hash: "0ac2d3ec623b955ef6781ff130f6aa8f65b1dd1022f9c7f6a60d40ee03ed3c8a",
      timestamp: ~N[2022-04-01 12:17:35.067551]
    }
  ],
  current_transactions: []
}

实现交易

区块链中的「交易」一词很具有迷惑性,似乎代表着一定涉及金钱的转换。

实际上,我们应该将交易抽象为如下的抽象模型:

{
    from: from_addr,
    to: to_addr,
    amount: amount, # 仅公链有效
    gas: gas, # 手续费
    op: operation # 附带的操作
}

公链中,每笔交易均会包含amount(amount可能是0)与gas。 有的交易是单纯的用户间转账,有的交易的关键点在于operation。 这个时候,to通常是一个合约地址,operation会告诉「区块链计算机」要做什么——例如在区块链数据库里存储一个值,例如在区块链上进行某些计算,这时我们就将区块链看成是一台「分布式计算机」。

在联盟链中,amount这个概念被废弃,同时,也没有了原生代币转账的交易(注意,ERC20这种基于智能合约的代币是支持的,不过通常被称为积分)。 但是,gas依然存在,用来衡量对计算资源的消耗量。

在Elixir中一个交易的格式可以被这样抽象:

%{
  :sender => sender,
  :recipient => recipient,
  :amount => amount
}

补全new_transaction函数:

def new_transaction(c, sender, recipient, amount) do
    tx = %{
      :sender => sender,
      :recipient => recipient,
      :amount => amount
    }

    %Blockchain{c | current_transactions: c.current_transactions ++ [tx]}
end

>

我们包含交易的完整代码如下:

defmodule Blockchain do
  defstruct chain: [], current_transactions: []

  def new_block(c, name) do
    b = %{
      :index => length(c.chain),
      :timestamp => NaiveDateTime.utc_now(),
      :name => name,
      :previous_hash => c |> last_block() |> hash(),
      :current_transactions => c.current_transactions
    }

    %{c | chain: c.chain ++ [b], current_transactions: []}
  end

  def zero() do
    b = %{
      :index => 0,
      :timestamp => NaiveDateTime.utc_now(),
      :name => "ZERO",
      :previous_hash => "",
      :current_transactions => []
    }

    %Blockchain{
      chain: [b],
      current_transactions: []
    }
  end

  def new_transaction(c, sender, recipient, amount) do
    tx = %{
      :sender => sender,
      :recipient => recipient,
      :amount => amount
    }

    %Blockchain{c | current_transactions: c.current_transactions ++ [tx]}
  end

  def last_block(c) do
    c.chain |> Enum.reverse() |> hd()
  end

  def hash(block) do
    value = block |> Poison.encode!()

    :crypto.hash(:sha256, value)
    |> Base.encode16()
    |> String.downcase()
  end
end
{:module, Blockchain, <<70, 79, 82, 49, 0, 0, 16, ...>>, {:hash, 1}}

试一试,创建交易!

创世区块

zero = Blockchain.zero()
%Blockchain{
  chain: [
    %{
      current_transactions: [],
      index: 0,
      name: "ZERO",
      previous_hash: "",
      timestamp: ~N[2022-04-01 12:20:26.788775]
    }
  ],
  current_transactions: []
}

执行几个交易,并打包到新区块

zero
|> Blockchain.new_transaction("alice", "bob", 100)
|> Blockchain.new_transaction("alice", "bob", 20)
|> Blockchain.new_block("first")
%Blockchain{
  chain: [
    %{
      current_transactions: [],
      index: 0,
      name: "ZERO",
      previous_hash: "",
      timestamp: ~N[2022-04-01 12:20:26.788775]
    },
    %{
      current_transactions: [
        %{amount: 100, recipient: "bob", sender: "alice"},
        %{amount: 20, recipient: "bob", sender: "alice"}
      ],
      index: 1,
      name: "first",
      previous_hash: "56881bced4dd4b52f76cc75e0b7f217fe4696e3e15974d4c174d84fb4205b92a",
      timestamp: ~N[2022-04-01 12:20:39.174474]
    }
  ],
  current_transactions: []
}

可以看到交易已经在 first 区块中了。

工作量证明PoW

因为比特币采用了 PoW 共识机制,所以这个概念才得以被广泛传播。 PoW 全称 Proof of Work,中文名是工作量证明,PoW 共识机制其实是一种设计思路, 而不是一种具体的实现。

PoW 机制其实早在 1997 年就被提出了,它早期多被应用在抵抗滥用软件服务的场景中, 例如抵抗垃圾邮件(所以 PoW在邮件服务系统会有所涉及)。

我们借用维基百科的一张图来解释一下 PoW 机制是如何用在这个场景中的。

为了防止垃圾消息泛滥,接收者并不直接接受来自任意发送者的消息,所以在一次有效的会话中,发送者需要计算一个按照规则约定难题的答案,发送给接受者的同时, 需要附带验证这个答案,如果这个答案被验证有效,那么接受者才会接受这个消息。

可以看出 PoW 的核心设计思路是提出一个计算难题,但是这个难题答案的验证过程是非常容易的,这种特性我们称之为计算不对称特性

如何理解区块链PoW

举个例子,假设我们给定一个字符串“Fenix”,我们提出的难题是,计算一个数字, 与给定的字符串连接起来,使这个字符串的 SHA256 计算结果的前 4 位是 0, 这个数字我们称作 nonce,比如字符串 “Fenix1234”,nonce 就是 1234, 我们要找到符合条件的 nonce。

以Python代码为例子。

#!/usr/bin/env python
import hashlib

def main():
    base_string = "Fenix"
    nonce = 10000
    count = 0
    while True:
        target_string = base_string + str(nonce)
        pow_hash = hashlib.sha256(target_string).hexdigest()
        count = count + 1
        if pow_hash.startswith("0000"):
            print pow_hash
            print "nonce: %s  scan times: %s" % (nonce, count)
            break
        nonce = nonce + 1

if __name__ == '__main__':
    main()

我规定了基础字符串是 “Fenix”,nonce 从 10000 开始自增往上搜索, 直到找到符合条件的 nonce 值。

# 前4位是0
0000250248f805c558bc28864a6bb6bf0c244d836a6b1a0c5078987aa219a404
nonce: 68828  scan times: 58829
# 前5位是0
0000067fc247325064f685c32f8a079584b19106c5228b533f10c775638d454c
nonce: 1241205  scan times: 1231206
# 前7位是0
00000003f41b126ec689b1a2da9e7d46d13d0fd1bece47983d53c5d32eb4ac90
nonce: 165744821  scan times: 165734822

可以看出,每次要求哈希结果的前 N 位多一个 0,计算次数就多了很多倍, 当要求前 7 位都是 0 时,计算次数达到了 1.6 亿次。

> 以上内容参考:《深入浅出区块链》

其他语言版本

Go

package main

import (
    "crypto/sha256"
    "encoding/hex"
    "fmt"
    "strconv"
    "strings"
)

func main() {
    pow("0000")
}

func pow(diff string) {
    baseStr := "fenix"
    nonce := 0
    count := 0
    for {
        targetStr := baseStr + strconv.Itoa(nonce)
        h := sha256.New()
        h.Write([]byte(targetStr))
        powHash := hex.EncodeToString(h.Sum(nil))
        fmt.Printf("\r%s", powHash)
        count += 1
        if strings.HasPrefix(string(powHash), diff) {
            fmt.Println()
            fmt.Println("nonce: ", nonce, "scan times: ", count)
            break
        }
        nonce += 1
    }
}

Rust

use sha256::digest;

fn main() {
    pow(String::from("0000"));
}

fn pow(diff: String) {
    println!("{}", diff);
    let mut count = 0;
    let mut nonce = 0;
    let base_str = String::from("fenix");
    loop {
        count += 1;
        let target_str = base_str.clone() + &nonce.to_string();
        let pow_hash = digest(target_str);
        print!("\r{}", pow_hash);
        if pow_hash.starts_with(&diff) {
            println!("");
            println!("nonce: {}, scan times: {}", nonce, count);
            break;
        }
        nonce += 1;
    }
}

可以看出来,主流编程语言去实现的时候,基本都会选择循环的方式。

Elixir 的变量是不可变的,常规的循环方式是不可以的,我们就得用递归的思路来做,如下:

defmodule Pow do
  def work(value, nonce, count, difficulty) do
    if String.starts_with?(value, difficulty) do
      IO.puts(value)
      IO.puts("nonce: #{nonce}, scan times: #{count}")
    else
      digest("#{value}#{nonce}") |> work(nonce + 1, count + 1, difficulty)
    end
  end

  def digest(value) do
    IO.write("\rhash: #{value}")

    :crypto.hash(:sha256, value)
    |> Base.encode16()
    |> String.downcase()
  end
end
{:module, Pow, <<70, 79, 82, 49, 0, 0, 10, ...>>, {:digest, 1}}
Pow.work("Fenix", 0, 0, "0000")
Pow.work("Fenix", 0, 0, "00000")
hash: a9d1ee2ad4586af4acc3b3dbd97d85355fbd4f8634a2062b38f4f878d350eb7e1770240000303ab87561a134d5ba70ff0dc96f59df2cb2c76a12418363a7065d7021e5
nonce: 177025, scan times: 177025
hash: 78ba29b80a1a9a0dbd363f00119a1822aad18cf999b93b732ff6fe7c42d4ee6b1468816000002fc5a2268fc4e118f4a221e1e76991c3f9a20d683f489e8466e26fcde54
nonce: 1468817, scan times: 1468817
:ok

难度越大,计算的次数越多。

终态

接下来为我们的区块链添加工作量证明,整个完整的程序如下:

defmodule Blockchain do
  defstruct chain: [], current_transactions: [], last_nonce: 0

  def new_block(c, name, nonce) do
    if valid_proof?(c.last_nonce, nonce) do
      b = %{
        :index => length(c.chain),
        :timestamp => NaiveDateTime.utc_now(),
        :name => name,
        :previous_hash => c |> last_block() |> hash(),
        :current_transactions => c.current_transactions
      }

      %{c | chain: c.chain ++ [b], current_transactions: [], last_nonce: nonce}
    else
      IO.puts("\nnonce: #{nonce} is invalid")
    end
  end

  def zero() do
    b = %{
      :index => 0,
      :timestamp => NaiveDateTime.utc_now(),
      :name => "ZERO",
      :previous_hash => "",
      :current_transactions => []
    }

    %Blockchain{
      chain: [b],
      current_transactions: [],
      last_nonce: 0
    }
  end

  def new_transaction(c, sender, recipient, amount) do
    tx = %{
      :sender => sender,
      :recipient => recipient,
      :amount => amount
    }

    %Blockchain{c | current_transactions: c.current_transactions ++ [tx]}
  end

  def last_block(c) do
    c.chain |> Enum.reverse() |> hd()
  end

  def hash(block) do
    value = block |> Poison.encode!()

    :crypto.hash(:sha256, value)
    |> Base.encode16()
    |> String.downcase()
  end

  def proof_of_work(last_nonce, nonce \\ 0) do
    case valid_proof?(last_nonce, nonce) do
      true ->
        nonce

      _ ->
        proof_of_work(last_nonce, nonce + 1)
    end
  end

  def valid_proof?(last_nonce, nonce, difficulty \\ "0000") do
    guess = "#{last_nonce}#{nonce}"

    guess_hash =
      :crypto.hash(:sha256, guess)
      |> Base.encode16()
      |> String.downcase()

    IO.write("\rdifficulty: #{difficulty}, attempt: #{nonce}, hash: #{guess_hash}")
    guess_hash |> String.starts_with?(difficulty)
  end
end
{:module, Blockchain, <<70, 79, 82, 49, 0, 0, 24, ...>>, {:valid_proof?, 3}}

创世区块

zero = Blockchain.zero()
%Blockchain{
  chain: [
    %{
      current_transactions: [],
      index: 0,
      name: "ZERO",
      previous_hash: "",
      timestamp: ~N[2022-04-01 12:27:28.717693]
    }
  ],
  current_transactions: [],
  last_nonce: 0
}

如果你想要生成一个新的区块,就必须去计算nonce,用算出的来的nonce去创建block。

如果没有计算好nonce,则会报错。比如我这里随便给了个nonce是333,我们执行一下:

first = zero |> Blockchain.new_block("first", 333)
difficulty: 0000, attempt: 333, hash: e73a36f8264731f64049b86564604e671d4bb1cfc0f6ab26803a5ad18040fee2
nonce: 333 is invalid
:ok

调用proof_of_work去计算nonce:

nonce = Blockchain.proof_of_work(zero.last_nonce)
difficulty: 0000, attempt: 69732, hash: 0000e326186933fa83f0efd581d09409022ec07b73a10f549bbaa6472e8a1175
69732

利用计算好的nonce去生成区块:

first = zero |> Blockchain.new_block("first", nonce)
difficulty: 0000, attempt: 69732, hash: 0000e326186933fa83f0efd581d09409022ec07b73a10f549bbaa6472e8a1175
%Blockchain{
  chain: [
    %{
      current_transactions: [],
      index: 0,
      name: "ZERO",
      previous_hash: "",
      timestamp: ~N[2022-04-01 12:27:28.717693]
    },
    %{
      current_transactions: [],
      index: 1,
      name: "first",
      previous_hash: "d530c9090258905de8009fe933f8e4686916adc3e803b89600615a2fb817392d",
      timestamp: ~N[2022-04-01 12:28:33.450247]
    }
  ],
  current_transactions: [],
  last_nonce: 69732
}

大工告成!

完整执行如下步骤:

  • 构建创世区块
  • 计算出来nonce,创建第一个区块
  • 计算新的nonce
  • 执行几个交易,再生成新的区块
zero = Blockchain.zero()

nonce = Blockchain.proof_of_work(zero.last_nonce)
IO.inspect(nonce)
first = zero |> Blockchain.new_block("first", nonce)
IO.inspect(first)

nonce = Blockchain.proof_of_work(first.last_nonce)

second =
  first
  |> Blockchain.new_transaction("alice", "bob", 100)
  |> Blockchain.new_block("second", nonce)

IO.inspect(second)
difficulty: 0000, attempt: 69732, hash: 0000e326186933fa83f0efd581d09409022ec07b73a10f549bbaa6472e8a117569732
difficulty: 0000, attempt: 69732, hash: 0000e326186933fa83f0efd581d09409022ec07b73a10f549bbaa6472e8a1175%Blockchain{
  chain: [
    %{
      current_transactions: [],
      index: 0,
      name: "ZERO",
      previous_hash: "",
      timestamp: ~N[2022-04-01 12:29:06.201828]
    },
    %{
      current_transactions: [],
      index: 1,
      name: "first",
      previous_hash: "b8099550bfe6931ad0eccb7a18d1d638d33177fe6ecc7621dcb593bdc406cf00",
      timestamp: ~N[2022-04-01 12:29:06.916490]
    }
  ],
  current_transactions: [],
  last_nonce: 69732
}
difficulty: 0000, attempt: 23263, hash: 0000ffc4b3bdbd6d46a4649d48944700b204fe59883f915fe1030f05c16a5492%Blockchain{
  chain: [
    %{
      current_transactions: [],
      index: 0,
      name: "ZERO",
      previous_hash: "",
      timestamp: ~N[2022-04-01 12:29:06.201828]
    },
    %{
      current_transactions: [],
      index: 1,
      name: "first",
      previous_hash: "b8099550bfe6931ad0eccb7a18d1d638d33177fe6ecc7621dcb593bdc406cf00",
      timestamp: ~N[2022-04-01 12:29:06.916490]
    },
    %{
      current_transactions: [%{amount: 100, recipient: "bob", sender: "alice"}],
      index: 2,
      name: "second",
      previous_hash: "5e1e75f4822ac949e20944175a39d9301ad278611eb74043cb761ba83742df8c",
      timestamp: ~N[2022-04-01 12:29:07.222425]
    }
  ],
  current_transactions: [],
  last_nonce: 23263
}
%Blockchain{
  chain: [
    %{
      current_transactions: [],
      index: 0,
      name: "ZERO",
      previous_hash: "",
      timestamp: ~N[2022-04-01 12:29:06.201828]
    },
    %{
      current_transactions: [],
      index: 1,
      name: "first",
      previous_hash: "b8099550bfe6931ad0eccb7a18d1d638d33177fe6ecc7621dcb593bdc406cf00",
      timestamp: ~N[2022-04-01 12:29:06.916490]
    },
    %{
      current_transactions: [%{amount: 100, recipient: "bob", sender: "alice"}],
      index: 2,
      name: "second",
      previous_hash: "5e1e75f4822ac949e20944175a39d9301ad278611eb74043cb761ba83742df8c",
      timestamp: ~N[2022-04-01 12:29:07.222425]
    }
  ],
  current_transactions: [],
  last_nonce: 23263
}

贤者时间

编程就是数据转换

> Programming Should Be About Transforming Data.

如果你深刻地理解了这句话,数据分析的 ETL(Extract - Transform - Load)是那么地自然,Map/Reduce 分而治之也是很好理解。

写代码没有唯一正确的方法,我们应该稍稍改变一下思考的方式:

  • 面向对象不是设计代码的唯一方法
  • 函数式编程不一定是复杂的和纯数学的
  • 编程的基础不是赋值、if语句和循环
  • 并发不一定需要锁、信号量、监视器等类似的东西
  • 进程不必消耗大量的资源
  • 元编程不只是语言的附属品
  • 即使编程是你的工作,也应该是充满乐趣的

掌握 Elixir 也许目前还不能给你的简历添彩,让你在职场获得更高的溢价, 但学习 Elixir 可以让你以不同的视角去看待函数、可变性、并发、高可用。

与主流的编程语言相比,Elixir非常与众不同,它会丰富你的视角,并开阔你的眼界来接受新的编程思维。

最后,Why Elixir & Blockchain? 确保Web3发生在Elixir!

参考