[易学易懂系列|rustlang语言|零基础|快速入门|(27)|实战4:从零实现BTC区块链]

释放双眼,带上耳机,听听看~!

[易学易懂系列|rustlang语言|零基础|快速入门|(27)|实战4:从零实现BTC区块链]

项目实战

实战4:从零实现BTC区块链

我们今天来开发我们的BTC区块链系统。

简单来说,从数据结构的角度上来说,区块链,就是区块组成的链。

以下就是BTC区块链典型的结构:

那最小单元就是区块:block。

这个block包含两部分:区块头,区块体。

我们先忽略Merkle树,先简化所有数据结构,只保留最基本的数据结构。

那区块头,就包含:区块高度,时间截;前一个区块地址;工作证明;

区块体,就包含交易数据,我们用一个vector来存储。

代码如下 :


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
1///交易结构体
2#[derive(Clone, Hash, Serialize, Deserialize, Debug)]
3pub struct Transaction {
4    sender: String,    //发送者
5    recipient: String, //接收者
6    amount: i64,       //交易数量
7}
8/// 区块结构体
9#[derive(Clone, Hash, Serialize, Deserialize, Debug)]
10pub struct Block {
11    pub index: u64,                     //区块高度
12    timestamp: DateTime<Utc>,           //时间截
13    pub transactions: Vec<Transaction>, //交易集合
14    pub proof: u64,                     //证明
15    pub previous_hash: String,          //上一个区块哈希地址
16}
17//区块链结构体
18#[derive(Default)]
19pub struct Blockchain {
20    pub chain: Vec<Block>,                  //区块链帐本
21    current_transactions: Vec<Transaction>, //当前交易集合
22    pub nodes: HashSet<String>,             //节点集合
23}
24
25
26

现在我们创建了一个基本的区块数据结构,现在我们来让矿工来创建区块吧。

怎么让不同的矿工,积极地创建区块呢?

我们引入一个机制叫:POW共识机制。

什么叫POW?简单来说,就是大家根据给定的一个数值proof,进行hash计算,谁最先算出来的结果值符合某个条件,就拥有创建新的区块,并把这个区块连接到原来的区块链上的权力。

比如,困难程度为5,那有个矿工用proof数据进行SHA哈希计算,出如下结果:

0x0000010000000000000000000000000000000000000000000000000000000000

这个结果,前面的0(除了0x外)是5个,则这就是结果值。

如果,没有计算出上面的结果值,矿工将proof自增1,再进行SHA哈希计算,直到计算出这个符合条件的结果值为止。

而那个给定的数据值proof,也要放在区块头,这个值在每次创建新区块的时候由矿工产生并写入区块头。

当然,如果 两个节点都算出结果并加入了新区块,这时,会产生链的分叉,这时如何决定冲突呢?

我们用最长链原则,即给定周期内,哪个节点拥有的链最长,就用哪个。

所以我们的共识机制是:POW+最长链原则

这个共识机制核心 代码如下:


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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
1impl Blockchain {
2    //创建创世区块
3    pub fn new() -> Blockchain {
4        let mut blockchain = Blockchain {
5            chain: vec![],
6            current_transactions: vec![],
7            nodes: HashSet::new(),
8        };
9        blockchain.new_block(100, Some("1"));
10        blockchain
11    }
12    /// Create a new Block in the Blockchain
13    ///
14    /// :param proof: The proof given by the Proof of Work algorithm
15    /// :param previous_hash: (Optional) hash of previous Block
16    /// :return: New Bloc
17    /// 创建新区块
18    pub fn new_block(&mut self, proof: u64, previous_hash: Option<&str>) -> Block {
19        let block = Block {
20            index: (self.chain.len() + 1) as u64,
21            timestamp: Utc::now(),
22            transactions: self.current_transactions.drain(0..).collect(),
23            proof,
24            previous_hash: previous_hash.unwrap_or("0").to_string(),
25        };
26
27        self.chain.push(block.clone());
28        block
29    }
30    /// Creates a new transaction to go into the next mined Block
31    ///
32    /// :param sender: Address of the Å›ender
33    /// :param recipient: Address of the recipient
34    /// :param amount: Amount
35    /// :return: The index of the Block that will hold this transaction
36    /// 发起一个新交易,将写入下一个区块
37    pub fn new_transaction(&mut self, sender: &str, recipient: &str, amount: i64) -> u64 {
38        self.current_transactions.push(Transaction {
39            sender: sender.to_string(),
40            recipient: recipient.to_string(),
41            amount,
42        });
43        self.last_block().unwrap().index + 1
44    }
45    /// Simple Proof of Work Algorithm:
46    /// - Find a number p' such that hash(pp') contains 4 leading zeroes,
47    ///   where p is the previous proof, and p' is the new proof
48    /// POW工作量证明共识机制算法
49    pub fn proof_of_work(last_block: &Block) -> u64 {
50        let mut proof = 0;
51        let last_proof = last_block.proof;
52        let last_hash = &last_block.previous_hash;
53        while !Self::valid_proof(last_proof, proof, last_hash) {
54            proof += 1;
55        }
56        proof
57    }
58    /// Validates the Proof: Does hash(last_proof, proof, last_hash) containt 4 leading zeroes
59    //验证工作证明数字
60    fn valid_proof(last_proof: u64, proof: u64, last_hash: &String) -> bool {
61        let guess = format!("{}{}{}", last_proof, proof, last_hash);
62        let guess_hash = hex_digest(Algorithm::SHA256, guess.as_bytes());
63        guess_hash.ends_with("00000") //困难度为5
64    }
65
66    /// Creates a SHA-256 hash of a Block
67    ///
68    /// :param block: Block
69    /// :return hash for the block
70    /// 创建一个区块 的哈希值,基SHA-256算法
71    pub fn hash(block: &Block) -> String {
72        let serialized = serde_json::to_string(&block).unwrap();
73        hex_digest(Algorithm::SHA256, serialized.as_bytes())
74    }
75    /// Returns the last Block in the chain
76    /// 返回最后一个区块
77    pub fn last_block(&self) -> Option<&Block> {
78        self.chain.last()
79    }
80
81    /// Add a new node to the list of nodes
82    ///
83    /// :param address: Address of the node. Eg. 'http://192.168.0.5:5000'
84    ///
85    /// 节点注册,即新节点加入区块链网络,注册地址参数为节点服务器地址,如:'http://192.168.0.5:5000‘
86    pub fn register_node(&mut self, address: &str) {
87        let parsed_url = urlparse(address);
88        self.nodes.insert(parsed_url.netloc);
89    }
90
91    /// Determine if a given blockchain is valid
92    /// 链的验证
93    fn valid_chain(&self, chain: &[Block]) -> bool {
94        let mut last_block = &chain[0];
95        let mut current_index: usize = 1;
96        while current_index < chain.len() {
97            let block = &chain[current_index];
98            println!("{:?}", last_block);
99            println!("{:?}", block);
100            println!("-----------");
101            if block.previous_hash != Blockchain::hash(last_block) {
102                return false;
103            }
104            if !Blockchain::valid_proof(last_block.proof, block.proof, &last_block.previous_hash) {
105                return false;
106            }
107
108            last_block = block;
109            current_index += 1;
110        }
111        true
112    }
113
114    /// This is our Consensus Algorithm, it resolves conflicts
115    /// by replacing our chain with the longest one in the network.
116    ///
117    /// :return True if our chain was replaced and false otherwise
118    /// 解决冲突的机制,即共识机制,最长链原则处理逻辑,即共识机制为(POw+最长链原则)
119    pub fn resolve_conflicts(&mut self) -> bool {
120        let mut max_length = self.chain.len();
121        let mut new_chain: Option<Vec<Block>> = None;
122
123        // Grab and verify the chains from all the nodes in our network
124        for node in &self.nodes {
125            let mut response = reqwest::get(&format!("http://{}/chain", node)).unwrap();
126            if response.status().is_success() {
127                let node_chain: Chain = response.json().unwrap();
128                if node_chain.length > max_length && self.valid_chain(&node_chain.chain) {
129                    max_length = node_chain.length;
130                    new_chain = Some(node_chain.chain);
131                }
132            }
133        }
134        // Replace our chain if we discovered a new, valid chain longer than ours
135        match new_chain {
136            Some(x) => {
137                self.chain = x;
138                true
139            }
140            None => false,
141        }
142    }
143}
144
145

以上代码,我们放在当前工程目录下的src/blockchain.rs,完整代码如下 :


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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
1use crate::api::Chain;
2use chrono::{DateTime, Utc};
3use crypto_hash::{hex_digest, Algorithm};
4use reqwest;
5use serde::{Deserialize, Serialize};
6use std::collections::HashSet;
7use urlparse::urlparse;
8///交易结构体
9#[derive(Clone, Hash, Serialize, Deserialize, Debug)]
10pub struct Transaction {
11    sender: String,    //发送者
12    recipient: String, //接收者
13    amount: i64,       //交易数量
14}
15/// 区块结构体
16#[derive(Clone, Hash, Serialize, Deserialize, Debug)]
17pub struct Block {
18    pub index: u64,                     //区块高度
19    timestamp: DateTime<Utc>,           //时间截
20    pub transactions: Vec<Transaction>, //交易
21    pub proof: u64,                     //证明
22    pub previous_hash: String,          //上一个区块哈希地址
23}
24//区块链结构体
25#[derive(Default)]
26pub struct Blockchain {
27    pub chain: Vec<Block>,                  //区块链帐本
28    current_transactions: Vec<Transaction>, //交易集合
29    pub nodes: HashSet<String>,             //节点集合
30}
31
32impl Blockchain {
33    //创建创世区块
34    pub fn new() -> Blockchain {
35        let mut blockchain = Blockchain {
36            chain: vec![],
37            current_transactions: vec![],
38            nodes: HashSet::new(),
39        };
40        blockchain.new_block(100, Some("1"));
41        blockchain
42    }
43    /// Create a new Block in the Blockchain
44    ///
45    /// :param proof: The proof given by the Proof of Work algorithm
46    /// :param previous_hash: (Optional) hash of previous Block
47    /// :return: New Bloc
48    /// 创建新区块
49    pub fn new_block(&mut self, proof: u64, previous_hash: Option<&str>) -> Block {
50        let block = Block {
51            index: (self.chain.len() + 1) as u64,
52            timestamp: Utc::now(),
53            transactions: self.current_transactions.drain(0..).collect(),
54            proof,
55            previous_hash: previous_hash.unwrap_or("0").to_string(),
56        };
57
58        self.chain.push(block.clone());
59        block
60    }
61    /// Creates a new transaction to go into the next mined Block
62    ///
63    /// :param sender: Address of the Å›ender
64    /// :param recipient: Address of the recipient
65    /// :param amount: Amount
66    /// :return: The index of the Block that will hold this transaction
67    /// 发起一个新交易,将写入下一个区块
68    pub fn new_transaction(&mut self, sender: &str, recipient: &str, amount: i64) -> u64 {
69        self.current_transactions.push(Transaction {
70            sender: sender.to_string(),
71            recipient: recipient.to_string(),
72            amount,
73        });
74        self.last_block().unwrap().index + 1
75    }
76    /// Simple Proof of Work Algorithm:
77    /// - Find a number p' such that hash(pp') contains 4 leading zeroes,
78    ///   where p is the previous proof, and p' is the new proof
79    /// POW工作量证明共识机制算法
80    pub fn proof_of_work(last_block: &Block) -> u64 {
81        let mut proof = 0;
82        let last_proof = last_block.proof;
83        let last_hash = &last_block.previous_hash;
84        while !Self::valid_proof(last_proof, proof, last_hash) {
85            proof += 1;
86        }
87        proof
88    }
89    /// Validates the Proof: Does hash(last_proof, proof, last_hash) containt 4 leading zeroes
90    //验证工作证明数字
91    fn valid_proof(last_proof: u64, proof: u64, last_hash: &String) -> bool {
92        let guess = format!("{}{}{}", last_proof, proof, last_hash);
93        let guess_hash = hex_digest(Algorithm::SHA256, guess.as_bytes());
94        guess_hash.ends_with("00000") //困难度为5
95    }
96
97    /// Creates a SHA-256 hash of a Block
98    ///
99    /// :param block: Block
100    /// :return hash for the block
101    /// 创建一个区块 的哈希值,基SHA-256算法
102    pub fn hash(block: &Block) -> String {
103        let serialized = serde_json::to_string(&block).unwrap();
104        hex_digest(Algorithm::SHA256, serialized.as_bytes())
105    }
106    /// Returns the last Block in the chain
107    /// 返回最后一个区块
108    pub fn last_block(&self) -> Option<&Block> {
109        self.chain.last()
110    }
111
112    /// Add a new node to the list of nodes
113    ///
114    /// :param address: Address of the node. Eg. 'http://192.168.0.5:5000'
115    ///
116    /// 节点注册,即新节点加入区块链网络,注册地址参数为节点服务器地址,如:'http://192.168.0.5:5000‘
117    pub fn register_node(&mut self, address: &str) {
118        let parsed_url = urlparse(address);
119        self.nodes.insert(parsed_url.netloc);
120    }
121
122    /// Determine if a given blockchain is valid
123    /// 链的验证
124    fn valid_chain(&self, chain: &[Block]) -> bool {
125        let mut last_block = &chain[0];
126        let mut current_index: usize = 1;
127        while current_index < chain.len() {
128            let block = &chain[current_index];
129            println!("{:?}", last_block);
130            println!("{:?}", block);
131            println!("-----------");
132            if block.previous_hash != Blockchain::hash(last_block) {
133                return false;
134            }
135            if !Blockchain::valid_proof(last_block.proof, block.proof, &last_block.previous_hash) {
136                return false;
137            }
138
139            last_block = block;
140            current_index += 1;
141        }
142        true
143    }
144
145    /// This is our Consensus Algorithm, it resolves conflicts
146    /// by replacing our chain with the longest one in the network.
147    ///
148    /// :return True if our chain was replaced and false otherwise
149    /// 最长链原则处理逻辑,即共识机制为(POw+最长链原则)
150    pub fn resolve_conflicts(&mut self) -> bool {
151        let mut max_length = self.chain.len();
152        let mut new_chain: Option<Vec<Block>> = None;
153
154        // Grab and verify the chains from all the nodes in our network
155        for node in &self.nodes {
156            let mut response = reqwest::get(&format!("http://{}/chain", node)).unwrap();
157            if response.status().is_success() {
158                let node_chain: Chain = response.json().unwrap();
159                if node_chain.length > max_length && self.valid_chain(&node_chain.chain) {
160                    max_length = node_chain.length;
161                    new_chain = Some(node_chain.chain);
162                }
163            }
164        }
165        // Replace our chain if we discovered a new, valid chain longer than ours
166        match new_chain {
167            Some(x) => {
168                self.chain = x;
169                true
170            }
171            None => false,
172        }
173    }
174}
175
176

现在 我们向外界提供一些可用的API。

我们新建一个文件:src/api.rs,代码如下 :


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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
1use crate::blockchain::{Block, Blockchain, Transaction};
2
3use actix_web::{web, HttpRequest, HttpResponse};
4use serde::{Deserialize, Serialize};
5use std::sync::Mutex;
6///返回消息体
7#[derive(Serialize, Deserialize)]
8pub struct MessageResponse {
9    message: String,
10}
11//交易请求信息
12#[derive(Serialize, Deserialize)]
13pub struct TransactionRequest {
14    sender: String,
15    recipient: String,
16    amount: i64,
17}
18///挖矿响应消息
19#[derive(Serialize)]
20pub struct MiningRespose {
21    message: String,
22    index: u64,
23    transactions: Vec<Transaction>,
24    proof: u64,
25    previous_hash: String,
26}
27///链结构体,代表现在网络上的最长链
28#[derive(Serialize, Deserialize)]
29pub struct Chain {
30    pub chain: Vec<Block>,
31    pub length: usize,
32}
33///节点注册请求信息
34#[derive(Deserialize)]
35pub struct RegisterRequest {
36    nodes: Vec<String>,
37}
38///节点注册响应信息
39#[derive(Serialize)]
40pub struct RegisterResponse {
41    message: String,
42    total_nodes: Vec<String>,
43}
44//解决冲突响应信息
45#[derive(Serialize)]
46pub struct ResolveResponse {
47    message: String,
48    chain: Vec<Block>,
49}
50///发起新交易
51pub fn new_transaction(
52    state: web::Data<Mutex<Blockchain>>,
53    req: web::Json<TransactionRequest>,
54) -> HttpResponse {
55    let sender = req.sender.to_owned();
56    let recipient = req.recipient.to_owned();
57    let index = state
58        .lock()
59        .unwrap()
60        .new_transaction(&sender, &recipient, req.amount);
61    HttpResponse::Created().json(MessageResponse {
62        message: format! {"Transaction will be added to Block {}", index},
63    })
64}
65///矿工挖矿
66pub fn mine(
67    node_identifier: web::Data<String>,
68    state: web::Data<Mutex<Blockchain>>,
69    _req: HttpRequest,
70) -> HttpResponse {
71    let (proof, previous_hash) = {
72        let blockchain = state.lock().unwrap();
73        let last_block = blockchain.last_block().unwrap();
74        let proof = Blockchain::proof_of_work(&last_block);
75        let previous_hash = Blockchain::hash(last_block);
76        (proof, previous_hash)
77    };
78    let mut blockchain = state.lock().unwrap();
79    blockchain.new_transaction("0", &*node_identifier, 1);
80    let block = blockchain.new_block(proof, Some(&previous_hash));
81    HttpResponse::Ok().json(MiningRespose {
82        message: "New Block Forged".to_string(),
83        index: block.index,
84        transactions: block.transactions,
85        proof,
86        previous_hash,
87    })
88}
89///当前最新链的信息
90pub fn chain(state: web::Data<Mutex<Blockchain>>, _req: HttpRequest) -> HttpResponse {
91    let length = state.lock().unwrap().chain.len();
92    HttpResponse::Ok().json(Chain {
93        chain: state.lock().unwrap().chain.clone(),
94        length,
95    })
96}
97///节点注册
98pub fn register_node(
99    state: web::Data<Mutex<Blockchain>>,
100    req: web::Json<RegisterRequest>,
101) -> HttpResponse {
102    if req.nodes.is_empty() {
103        return HttpResponse::BadRequest().json(MessageResponse {
104            message: "Error: Please supply a valid list of nodes".to_string(),
105        });
106    }
107    let mut blockchain = state.lock().unwrap();
108    for node in req.nodes.iter() {
109        blockchain.register_node(node)
110    }
111    HttpResponse::Created().json(RegisterResponse {
112        message: "New nodes have been added".to_string(),
113        total_nodes: blockchain.nodes.iter().cloned().collect(),
114    })
115}
116///跟网络上其他节点达成共识,即解决冲突
117pub fn resolve_nodes(state: web::Data<Mutex<Blockchain>>, _req: HttpRequest) -> HttpResponse {
118    let mut blockchain = state.lock().unwrap();
119    let replaced = blockchain.resolve_conflicts();
120    let message = if replaced {
121        "Our chain was replaced"
122    } else {
123        "Our chain is authorative"
124    };
125    HttpResponse::Ok().json(ResolveResponse {
126        message: message.to_string(),
127        chain: blockchain.chain.clone(),
128    })
129}
130
131

当然,我们要用到一些好用的库,在我们的Cargo.toml文件,我们加入依赖,完整代码如下:


1
2
3
4
5
6
7
8
9
10
1[dependencies]
2chrono = { version = "0.4.6", features = ["serde"] }
3crypto-hash = "0.3.3"
4serde = { version = "1.0.90", features = ["derive"] }
5serde_json = "1.0"
6actix-web = "1.0"
7uuid = { version = "0.7", features = ["v4"] }
8urlparse = "0.7.3"
9reqwest = "=0.9.17"
10

最后我们的主程序 src/main.rs如下:


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
38
39
40
41
1pub mod api;
2pub mod blockchain;
3
4use actix_web::{web, App, HttpServer};
5use std::env;
6use std::sync::Mutex;
7use uuid::Uuid;
8
9fn main() {
10    let args: Vec<String> = env::args().collect();
11    let port = match args.as_slice() {
12        [_, key, value] => {
13            if key == "--p" {
14                value
15            } else {
16                panic!("Illegal arguments passed to the program.");
17            }
18        }
19        _ => "5000",
20    };
21    // TODO: make chain shared across threads
22    let sharedchain = web::Data::new(Mutex::new(blockchain::Blockchain::new()));
23    let node_identifier = web::Data::new(Uuid::new_v4().to_simple().to_string());
24
25    HttpServer::new(move || {
26        App::new()
27            .register_data(sharedchain.clone())
28            .register_data(node_identifier.clone())
29            .data(web::JsonConfig::default().limit(4096))
30            .service(web::resource("/mine").route(web::get().to(api::mine)))
31            .service(web::resource("/transactions/new").route(web::post().to(api::new_transaction)))
32            .service(web::resource("/chain").route(web::get().to(api::chain)))
33            .service(web::resource("/nodes/register").route(web::post().to(api::register_node)))
34            .service(web::resource("/nodes/resolve").route(web::get().to(api::resolve_nodes)))
35    })
36    .bind(format!("127.0.0.1:{}", port))
37    .unwrap()
38    .run();
39}
40
41

然后我们可以用以下命令来调用 :

挖矿:


1
2
1curl http://localhost:5000/mine
2

创建新交易:


1
2
1curl -H "Content-Type: application/json" --request POST --data '{"sender":"e79fcabd1d70433191701d17c4d13112", "recipient":"some-other-address", "amount":5}' http://localhost:5000/transactions/new
2

查看整条链信息:


1
2
3
1curl http://localhost:5000/chain
2
3

注册节点:


1
2
3
1curl -H "Content-Type: application/json" --request POST --data '{"nodes":["http://localhost:5001"]}' http://localhost:5000/nodes/register
2
3

与其他节点达成共识(共识机制):


1
2
3
1curl http://localhost:5000/nodes/resolve
2
3

以上,希望对你有用。


1
2
3
1如果遇到什么问题,欢迎加入:rust新手群,在这里我可以提供一些简单的帮助,加微信:360369487,注明:博客园+rust
2
3

https://asymmetric.github.io/2018/02/11/blockchain-rust/

https://jeiwan.net/posts/building-blockchain-in-go-part-1/

https://freestartupkits.com/articles/technology/cryptocurrency-news-and-tips/ultimate-rust-blockchain-tutorial/

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

https://medium.com/@vanflymen/learn-blockchains-by-building-one-117428612f46?

https://github.com/Koura/blockchain-example

给TA打赏
共{{data.count}}人
人已打赏
安全技术

C++ lambda表达式

2022-1-11 12:36:11

气候事件

“海葵”明天前后或登陆台州至宁波一带

2012-8-6 9:02:33

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索