在這一系列的文章中,我將向你展示如何使用 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瀏覽量
66951 -
區塊鏈
+關注
關注
111文章
15563瀏覽量
106691 -
Rust
+關注
關注
1文章
230瀏覽量
6664
原文標題:使用Rust從零開發區塊鏈 01
文章出處:【微信號:Rust語言中文社區,微信公眾號:Rust語言中文社區】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論