在这一系列的文章中,我将向你展示如何使用 Rust 语言从零开始编写一个区块链。这些文章并不是为了想学习智能合约编程的人,而是为了理解区块链的底层知识 - 区块、链、键值数据库、网络、共识机制等的人而准备的。如果你想学习区块链的底层知识,这就是你要找的地方。
我已经在区块链领域工作了几年,从一开始就计划写这一系列的文章来学习区块链。但直到现在,我才有足够的时间来做这件事。我主要熟悉 Substrate 框架,从其文档站点学到了很多知识,从核心概念到如何使用它来开发应用。所以我建议你如果有时间的话,去阅读那些文档,他们官方还有关于它的课程,你可以在这里找到信息:Polkadot Academy。国内的话,OneBlock+社区在做免费的Substrate培训课程,你可以在这里找到最新一期(2023.12)报名链接。
Substrate 是一个功能齐全的区块链框架,它非常强大。但它也很复杂,难以深入理解,所以我开设这个系列,帮助那些想要深入了解区块链内部的开发者。
Rust 无疑是编写区块链的首选,它也是我最喜欢的编程语言。如果你还没有接触过它,你应该去试试。
好的,让我们开始吧。
块
什么是区块?它是区块链的基本单位。在我们的例子中,它只是一个 Rust 结构体。我们可以这样定义它:
structBlock{ header:BlockHeader, body:BlockBody, }
区块由两部分组成:BlockHeader 和 BlockBody。每种类型的定义如下:
BlockHeader
structBlockHeader{ hash:String, height:u64, prev_hash:String, timestamp:u64, }
BlockBody
typeBlockBody=Vec
区块的Body部分是一个普通的字符串向量,而头部看起来更有趣。在所有的字段中,prev_hash 是最有趣的,它存储了前一个区块的哈希字段值,我们将在这篇文章后面的链部分讨论它。
height 字段表示这个区块的序列号,新的区块被添加到区块链中时,高度会递增。
timestamp 字段表示创建这个区块时的 Unix 时间戳。所以它与你正在使用的本地机器有关。
而 hash 字段存储了这个区块的哈希值。我们会问:如何计算这个区块的哈希值?因为哈希字段是这个结构体的一部分,所以简单地序列化这个结构体是行不通的。我们需要在做计算时从其他字段中排除这个字段。所以算法看起来像这样:
fncalc_block_hash(height:u64,prev_hash:&str,timestamp:u64,body:&Vec
letconcated_str=vec![ height.to_string(), prev_hash.to_string(), timestamp.to_string(), body.concat(), ] .concat(); letmuthasher=Sha256::new(); hasher.update(concated_str.as_bytes()); hex::encode(hasher.finalize().as_slice()) }
我们不会教授如何编写 Rust 代码的细节,相反,我们主要会描述如何设计它的思路流程。
在这里,我们按照 height, prev_hash, timestamp, body 的顺序连接这个区块的元素。由于 body 是一个向量,我们应该首先连接它。一旦字符串连接完成,我们使用 Sha256 对其进行哈希计算。这一步创建了一个 32 字节的 u8 数组:[u8; 32]。然后我们使用 hex 将其编码为长度为 64 的字符串,这代表了这个区块的哈希。
你会注意到,我们将 prev_hash 值作为这个区块的哈希的来源之一。这非常重要,你可能会想知道我们为什么要这么做。
我们可以按照以下方式测试这个算法:
#[test] fntest_block_hash(){ letblock1=Block::new(10,"aaabbbcccdddeeefff".to_string(),vec![]); letblock2=Block::new(10,"aaabbbcccdddeeefff".to_string(),vec![]); assert_eq!(block1.header.height,block2.header.height); assert_eq!(block1.header.prev_hash,block2.header.prev_hash); //XXX:havelittleprobabilitytofail assert_eq!(block1.header.timestamp,block2.header.timestamp); //XXX:havelittleprobabilitytofail assert_eq!(block1.header.hash,block2.header.hash); assert_eq!(block1.body,block2.body); }
链
什么是链?你可以想象一条项链或者一条铁链。在我们的例子中,链是一个抽象的概念,每个区块都存储了前一个区块的哈希字段值。就这样,你看,没有复杂的地方。
在这个链中,每个区块只关心前一个区块,而不关心其他区块。所以它是一个相对简单的结构。
但我们即将遇到一个问题:第一个区块怎么办?它之前没有区块。
是的,对于这个边缘情况,我们需要为 hash 字段设置一个预定义的值。由于这个特殊情况,区块链的第一个区块通常被称为 创世(Genesis) 区块。
这就是整个区块链的样子:
随着时间的推移,这个结构将无限扩展(或增长)。
区块链管理器
我们需要一个管理器来管理区块链。现在它非常简单,只包含一个区块的向量。
#[derive(Debug)] structBlockChain{ blocks:Vec, }
并在其上实现一些方法:
implBlockChain{ fnnew()->Self{ BlockChain{blocks:vec![]} } fngenesis()->Block{ lettxs=vec!["Thebigbrotheriswatchingyou.".to_string()]; Block::new(0,"1984,GeorgeOrwell".to_string(),txs) } fnadd_block(&mutself,block:Block){ self.blocks.push(block); } }
现在我们可以使用这个管理器来构建一个区块链:
fnmain(){
letmutblockchain=BlockChain::new(); letgenesis_block=BlockChain::genesis(); letprev_hash=genesis_block.header.hash.clone(); blockchain.add_block(genesis_block); letb1=Block::new(1,prev_hash,vec![]); letprev_hash=b1.header.hash.clone(); blockchain.add_block(b1); letb2=Block::new(2,prev_hash,vec![]); letprev_hash=b2.header.hash.clone(); blockchain.add_block(b2); letb3=Block::new(3,prev_hash,vec![]); letprev_hash=b3.header.hash.clone(); blockchain.add_block(b3); letb4=Block::new(4,prev_hash,vec![]); letprev_hash=b4.header.hash.clone(); blockchain.add_block(b4); letb5=Block::new(5,prev_hash,vec![]); //letprev_hash=b5.header.hash.clone(); blockchain.add_block(b5); println!("{:#?}",blockchain); }
它将打印出来类似下面的东西:
mike@alberta:~/works/blockchainworks/vintage$cargorun Compilingvintagev0.1.0(/home/mike/works/blockchainworks/vintage) Finisheddev[unoptimized+debuginfo]target(s)in0.22s Running`target/debug/vintage` BlockChain{ blocks:[ Block{ header:BlockHeader{ hash:"96cf34aa91e070ddf95eb9e0e8616b24e2f326c80d5fa9746e8dd8f0bec730d6", height:0, prev_hash:"1984,GeorgeOrwell", timestamp:1705649594, }, body:[ "Thebigbrotheriswatchingyou.", ], }, Block{ header:BlockHeader{ hash:"0dd52ac54a9d621c47688f7920cd9eaee18ffe0cca3c83e124b8f78cef8999e5", height:1, prev_hash:"96cf34aa91e070ddf95eb9e0e8616b24e2f326c80d5fa9746e8dd8f0bec730d6", timestamp:1705649594, }, body:[], }, Block{ header:BlockHeader{ hash:"61e95ab151cfa41c2a74cb076c33511ddf71f45dab0571f5f2db89df7ebc64cf", height:2, prev_hash:"0dd52ac54a9d621c47688f7920cd9eaee18ffe0cca3c83e124b8f78cef8999e5", timestamp:1705649594, }, body:[], }, Block{ header:BlockHeader{ hash:"dde009c56c1b02d41fec8271e5f990e9b33c84a2cf044de6fc33e96605f90458", height:3, prev_hash:"61e95ab151cfa41c2a74cb076c33511ddf71f45dab0571f5f2db89df7ebc64cf", timestamp:1705649594, }, body:[], }, Block{ header:BlockHeader{ hash:"f8cd3ab5f6ccc864515635878498e2e26b63b4fbf4dbc60ea3649e859b4a7d27", height:4, prev_hash:"dde009c56c1b02d41fec8271e5f990e9b33c84a2cf044de6fc33e96605f90458", timestamp:1705649594, }, body:[], }, Block{ header:BlockHeader{ hash:"4415f4993729459f5ff07c7c963890f1d9210d5241f5203b9179c8d3db6e9dac", height:5, prev_hash:"f8cd3ab5f6ccc864515635878498e2e26b63b4fbf4dbc60ea3649e859b4a7d27", timestamp:1705649594, }, body:[], }, ], }
我们做到了,它已经是一个区块链了。
如何持久化
到目前为止,我们只是将区块链保存在计算机的内存中,所以如果我们现在关闭计算机,区块链将一无所有。我们最好将整个链存储在我们的计算机上。
一般来说,人们会使用键值数据库(kv db)来存储区块链。为什么使用 kv db 而不是文件或 SQL db 呢?因为它简单且高效。
在我们的例子中,我们将使用 redb 作为我们的存储后端。根据其官方网站,Redb 是一个简单、便携、高性能、ACID、嵌入式键值存储,完全用 Rust 编写,并受到 lmdb 的启发。
接下来,我们需要设计一个存储模式。有以下几点:
使用两个表:blocks 表用于开发/生产模式,blocks_fortest 表用于测试模式;
每个区块都作为 redb 中的一个键值元素存储,其中键是区块的哈希字段值,值是区块的完全序列化字符串。
我们需要一个指针指向最后一个区块。'指向'实际上意味着持有区块的哈希值。我们可以使用这个指针从数据库中重构整个链(内存表示)。
我们还保持了 height 和区块的 hash 之间的映射关系。
然后我们需要将一个 db 实例注入到 BlockChain 管理器结构中。
#[derive(Debug)] structBlockChain{ blocks:Vec, db:Db, }
基于这个管理器实例,我们可以按照以下方式实现一个持久化方法:
fnpersist_block_to_table(
&mutself, table:TableDefinition<&str, &str>, block:&Block, )->Result<()>{ letheight=&block.header.height; lethash=&block.header.hash; letcontent=serde_json::to_string(&block)?; //storehash->blockpair self.db.write_block_table(table,&hash,&content)?; //storeheight->hashpair self.db .write_block_table(table,&height.to_string(),&hash)?; //storethelbp->hashpair(lastblockpointertohash) self.db .write_block_table(table,LAST_BLOCK_POINTER,&hash)?; Ok(()) }
其中,我们使用 serde 框架并使用 serde_json 将整个 block 结构体序列化为字符串(json 格式)。如你所见,我们存储了 3 对键值对:
hash -> 序列化的区块字符串
height -> 区块哈希
lbp (最后一个区块的指针) -> 区块哈希
我们可以像这样从数据库中检索一个 Block:
fnretrieve_block_by_hash_from_table(
&self, table:TableDefinition<&str, &str>, hash:&str, )->Result
我们使用 serde_json::from_str() 来反序列化原始字符串。
接下来,我们需要弄清楚如何从数据库中重新构建一个正确的区块链。我们可以使用一个迭代来做这个,首先获取最后一个区块,然后获取最后一个区块的前一个区块,依此类推。我们可以看看代码:
fnpopulate_from_db_table(&mutself,table:TableDefinition<&str, &str>)->Result<()>{
//findlastblockhashfromdb letlast_block_hash=self.db.read_block_table(table,LAST_BLOCK_POINTER)?; iflast_block_hash.is_none(){ returnOk(()); } letlast_block_hash=last_block_hash.unwrap(); //retrievelastblock letblock=self.retrieve_block_by_hash_from_table(table,&last_block_hash)?; ifblock.is_none(){ returnOk(()); } letblock=block.unwrap(); letmutprev_hash=block.header.prev_hash.clone(); letmutblocks:Vec=vec![block]; //iteratetooldblockesbyprev_hash whileprev_hash!=GENESIS_PREV_HASH{ letblock=self.retrieve_block_by_hash_from_table(table,&prev_hash)?; ifblock.is_none(){ returnOk(()); } letblock=block.unwrap(); prev_hash=block.header.prev_hash.clone(); blocks.insert(0,block); } //contructaninstanceofblockchain self.blocks=blocks; Ok(()) }
我们可以像这样使用这个 API:
letmutblockchain=BlockChain::new();
blockchain .populate_from_db() .expect("errorwhenpopulatefromdb");
然后可以测试它:
#[test] fntest_store_block_and_restore_block(){ letmutblockchain=BlockChain::new_to_table(TABLE_BLOCKS_FORTEST); //initialization letgenesis_block=BlockChain::genesis(); letprev_hash=genesis_block.header.hash.clone(); blockchain.add_block_to_table(TABLE_BLOCKS_FORTEST,genesis_block); letb1=Block::new(1,prev_hash,vec![]); letprev_hash=b1.header.hash.clone(); blockchain.add_block_to_table(TABLE_BLOCKS_FORTEST,b1); letb2=Block::new(2,prev_hash,vec![]); blockchain.add_block_to_table(TABLE_BLOCKS_FORTEST,b2); letblock_vec=blockchain.blocks.clone(); blockchain .populate_from_db_table(TABLE_BLOCKS_FORTEST) .expect("errorwhenpopulatefromdb"); _=blockchain.db.drop_table(TABLE_BLOCKS_FORTEST); for(i,block)inblock_vec.into_iter().enumerate(){ letblock_tmp=blockchain.blocks[i].clone(); assert_eq!(block,block_tmp); } }
在我们的代码中,使用 anyhow 来帮助管理各种错误,感谢这个漂亮的 crate,它使我们的生活更加轻松。
到目前为止,我们的区块链已经具有了持久化的能力,不再担心会丢失数据。我们已经到达了伟大征程的第一个里程碑。
审核编辑:黄飞
-
源代码
+关注
关注
96文章
2946浏览量
66884 -
区块链
+关注
关注
111文章
15563浏览量
106558 -
Rust
+关注
关注
1文章
230浏览量
6644
原文标题:使用Rust从零开发区块链 01
文章出处:【微信号:Rust语言中文社区,微信公众号:Rust语言中文社区】欢迎添加关注!文章转载请注明出处。
发布评论请先 登录
相关推荐
评论