本文基于
https://blog.logrocket.com/how-to-build-a-blockchain-in-rust/
这篇博客,实现一个简单的区块链。
初始化项目
1 | cargo new rust-miniblockchain |
在cargo.toml中添加依赖:
1 |
|
- libp2p负责p2p,是libp2p(基于go)的rust版,后面会有文章详细讨论这个库。
- tokio 异步运行时
- serde json序列化
- chrono 时间戳
- sha2 sha计算
- onecell 静态初始化
- log 日志门面 pretty_env_logger 日志实现
Block
定义区块
基本结构如此:
1 | use serde::{Deserialize, Serialize}; |
Block中除data外都属于区块头,data为区块体。对于实现加密货币的场景,data中应包含交易信息及merkle树信息。但是本文着重于区块链本身,而不是局限于加密货币,此处的data只是一个简单的String。
Chain中定义了一个区块链,一个简单的Vec
计算区块链的哈希
使用sha2这个包中的Sha256计算哈希
1 | use sha2::{Digest, Sha256}; |
创世区块
区块链必须有一个创世区块,任何区块都可以向前回溯到创世区块,是整个链的根,创世区块通常会被硬编码到软件中。
1 | impl Block { |
挖矿逻辑
挖矿本身就是根据前一个块构建新块:修改nonce后这里判断前缀是否满足难度,如果不满足,则nonce+1重新计算,直到得到满足难度的hash,此时挖矿完成。
1 | const DIFFICULTY_PREFIX: &str = "00"; |
测试
构建创世块,然后挖出一个块。
1 |
|
链
1 | impl Chain { |
验证链
等于逐个验证块is_block_valid
。验证点包括:
- 本块记录的前块hash是否和前块的hash匹配
- 本块hash的难度是否匹配
- 本块的id是否和前块id匹配
- 前块计算hash是否等于本块的hash
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38impl Chain {
pub fn is_block_valid(&self, block: &Block, previous_block: &Block) -> bool {
if block.previous_hash != previous_block.hash {
warn!("块 id: {} 前一个块hash不同", block.id);
return false;
} else if !hex::decode(&block.hash)
.expect("can decode from hex")
.starts_with(DIFFICULTY_PREFIX.as_bytes())
{
warn!("块id: {} 难度不合要求: {}", block.id, DIFFICULTY_PREFIX);
return false;
} else if block.id != previous_block.id + 1 {
warn!("块id: {} 块id不匹配: {}", block.id, previous_block.id);
return false;
} else if hex::encode(previous_block.calculate_hash()) != block.previous_hash {
warn!(
"块id: {} 和前块id:{} hash不匹配 ",
block.id, previous_block.id
);
return false;
}
true
}
fn is_chain_valid(&self, chain: &[Block]) -> bool {
for i in 0..chain.len() {
if i == 0 {
continue;
}
let first = chain.get(i - 1).expect("has to exist");
let second = chain.get(i).expect("has to exist");
if !self.is_block_valid(second, first) {
return false;
}
}
true
}
}
测试
1 |
|
选择最长链
先判断合法性,如果都合法选择最长的
1 | impl Chain { |
至此区块链本地模型基本完成,目前简单版的区块链不支持API,只支持stdin作为命令输入。下面做Event部分,这部分能令区块链支持p2p,命令输入。
Event
目前为了简单libp2p只用了floodsub的广播方式,将消息发送给每个节点。
定义消息体
1 |
|
ChainResponse
用于查询目标节点所有区块ChainRequest
用于询问目标节点区块Event
tokio处理的事件类型
此结构体用于注册libp2p回调。因此,所有业务状态要么注册channel丢进来,要么这里直接将Chain放进来。
1 |
|
用App注册回调
1 |
|
后面会有文章具体讨论libp2p和其中的floodsub,mdns的具体细节,本文只需知道mdns是p2p发现,floodsub是协议就可以了。
需要注意如果是查询的请求,这里将其转给channel异步处理避免阻塞libp2p event处理。
cmd
命令处理
1 | use std::collections::HashSet; |
最后就是tokio的主流程 main.rs
1 | use libp2p::{ |
运行方式:
1 | RUST_LOG=info cargo run |
命令
- ls c 展示当前节点区块
- ls p 展示所有节点(除自身外)
- create b {data} 当前节点添加区块,data可以是字符串。