使用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。
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!