專案來自視頻教學:https://www.bilibili.com/video/BV1kE411W7aD?
文章中有完整代碼鏈接
專案功能演示

1. printChain 輸出整條區塊鏈
simple:

2. getBalance ADDRESS 查詢賬戶余額
引數: ADDRESS-賬戶地址
simple:

3. send FROM TO AMOUNT MINER DATA 由FROM轉AMOUNT錢給TO,由MINER挖礦,同時寫入DATA
引數: FROM-轉出人 TO-轉入人 AMOUNT-轉賬金額 MINER-挖礦人 DATA-鑄幣交易可以自添加的資料
simple:

4. getTransaction TXHASH 查詢交易資訊
引數 TXHASH-交易hash
simple:

5. newWallet 創建一個新的錢包(公私鑰對)
simple:

6. listAddress 列舉所有的錢包地址
simple:

一、Go基礎
G252o環境安裝
Go語言
去官網下載并安裝配置好全域變數即可,記得配置GOROOT和GOPATH
編程IDE:GOLand的安裝與破解
詳情見:https://tech.souyunku.com/?p=16189
Go專案的目錄結構
專案目錄結構如何組織,一般語言都是沒有規定,但Go語言這方面做了規定,這樣可以保持一致性,做到統一、規則化比較明確,
1、一般的,一個Go專案在GOPATH下,會有如下三個目錄:
|--bin
|--pkg
|--src
其中,bin存放編譯后的可執行檔案;pkg存放編譯后的包檔案;src存放專案源檔案,
對于pkg目錄,曾經有人問:我把Go中的包放入pkg下面,怎么不行啊?他直接把Go包的源檔案放入了pkg中,
這顯然是不對的,pkg中的檔案是Go編譯生成的,而不是手動放進去的,(一般檔案后綴.a)
對于src目錄,存放源檔案,Go中源檔案以包(package)的形式組織,通常,新建一個包就在src目錄中新建一個檔案夾,
二、專案中的資料庫Blot
1.Blot簡介與實體
簡介:一個小型的key-value資料庫,沒有sql,輕便快捷高效,
操作demo詳情見:https://blog.csdn.net/yang731227/article/details/82974575
結構:

demo:
package main
import (
"fmt"
"itcast_Go/bolt"
"log"
)
func main() {
//1. 打開資料庫
//第一個引數是名字,第二個引數是權限6代表允許讀寫
db, err := bolt.Open("test.db", 0600, nil)
defer db.Close()
if err != nil{
log.Panic("打開資料庫失敗!" , err)
}
//操作資料庫
db.Update(func(tx *bolt.Tx) error {
//2. 打開抽屜(沒有就創建)
var bucketName []byte = []byte("b1")
bucket := tx.Bucket(bucketName)
if bucket == nil{
//沒有就創建
bucket, err = tx.CreateBucket(bucketName)
if err != nil{
log.Panic(err)
}
}
//操作抽屜中的資料,添加資料
//3. 寫資料
bucket.Put([]byte("1111"), []byte("hello"))
bucket.Put([]byte("2222"), []byte("world"))
return nil
})
//4. 讀資料
db.View(func(tx *bolt.Tx) error {
//找到抽屜
bucket := tx.Bucket([]byte("b1"))
if bucket != nil{
//如果存在就讀取
v1 := bucket.Get([]byte("1111"))
v2 := bucket.Get([]byte("2222"))
//輸出
fmt.Printf("'1111'-> %s\n", v1)
fmt.Printf("'2222'-> %s\n", v2)
}
return nil
})
}
2.專案存盤結構分析
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-OrNGpTtT-1601776118143)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200914200555511.png)]

三、專案中go語言的區塊序列化與反序列化
對于較為復雜的資料,采用序列化作為value存盤在k/v資料庫(blot)中,那么go語言的序列化與反序列化的基本操作是怎樣的呢?
使用gob來簡化操作,看完demo就懂:
demo:
package main
import (
"bytes"
"encoding/gob"
"fmt"
"log"
)
//創建“人”結構體
type Person struct {
Name string
Age uint
}
func main() {
//定義一個“人”結構
var xiaoming Person
xiaoming.Name = "小明"
xiaoming.Age = 18
//編碼的資料放進buffer
var buffle bytes.Buffer
//使用gob序列化得到位元組流
//定義一個編碼器encoder
encoder := gob.NewEncoder(&buffle)
//編碼結構體
err := encoder.Encode(&xiaoming)
if err != nil{
log.Panic(err)
}
fmt.Printf("小明編碼后的結果為:%v\n", buffle.Bytes())
//使用gob反序列化得到結構體
//創建byte讀input流,然后創建解碼器
decoder := gob.NewDecoder(bytes.NewReader(buffle.Bytes()))
var daming Person
//解碼
err = decoder.Decode(&daming)
if err != nil{
log.Panic(err)
}
fmt.Printf("解碼后的小明: %v\n", daming)
}
四、Go語言迭代器原理與區塊鏈的特殊迭代
range內部其實就是指標的迭代指向,然后賦值使用:

但是由于區塊鏈指標的特殊性,所以迭代需要從后向前迭代:

五、Go語言命令列的使用
很簡單
demo:
package main
//go命令列測驗
import (
"fmt"
"os"
)
//go命令列練習
func main() {
len1 := len(os.Args)
fmt.Printf("命令長度為:%d\n", len1)
for i, cmd := range os.Args{
fmt.Printf("arg[%d]: %s\n", i, cmd)
}
}

六、專案添加轉賬功能
轉賬重要的兩點:
- 每一筆交易能支配的錢都來自于上一個交易的輸出
- 每一個花費的輸出要一次性花完,有剩余的都要轉給自己
首先需要熟悉下位元幣交易腳本的三種模式介紹:
北大肖臻-第9講-位元幣腳本
專案交易的結構:

七、賬戶余額UTXO計算細節
1. 計算賬戶余額時統計UTXO
遍歷UTXO去統計某個賬戶的余額,如果只是簡單的遍歷則效率太低,而區塊鏈中的交易都是相關聯的,所以利用這一特點可以使用一些小技巧:

圖中黃色是已消費的輸出,藍色是還未消費的輸出
2. 轉賬時計算UTXO中的賬戶余額

專案中并沒有優化計算最合適的將零錢拼裝,而是簡單的遍歷逐步統計,滿足要求了就轉賬
八、blotDB資料庫的可視化
-
下載工具:運行
go get github.com/boltdb/boltd -
就會下載到GOPATH目錄中,查看GOPATH方法:
go env -
找到cmd檔案夾中的main檔案編譯其成為可執行檔案,編譯
go build main.go -
把可執行檔案放到和blot的db類檔案相同的目錄下,運行:
main.exe -- xxx.db
-
顯示結果:

九、公私鑰
位元幣公私鑰與地址的關系

公鑰生成地址的流程:

最后一步要用到base58演算法,一般都沒有這個包,可以通過以下命令引入位元幣原始碼官方的提供的包:
go get github.com/btcsuite/btcutil/base58
十、P2PKH的檢驗方式
1. 位元幣的幾種校驗方式
https://blog.csdn.net/weixin_43988498/article/details/107958185 三種位元幣的校驗方式
貼出P2PKH的校驗流程:
這一種是較為常見的一種形式,輸出腳本中輸出的是公鑰的Hash,而輸入腳本中要除了簽名還要包含公鑰
除了這些,其他的DUP、HASH160都是一些驗證操作,
腳本執行程序:
同樣的為了方便看,將輸入與輸出拼接到一起,從上往下執行,
前兩步操作相同,將輸入中的簽名和公鑰壓入堆疊
第三步操作DUP是將堆疊頂的公鑰復制一份
第四步操作HASH160是將復制的公鑰取HASH值,然后壓入堆疊中,
第五步,將輸出腳本里面的公鑰Hash壓入堆疊,這時堆疊里面出現了兩個公鑰的Hash值
搞清楚這個Hash值的來源:
第六步,EQUALVERIFY是彈出堆疊頂的兩個Hash值,比較兩者是否相等,
最后一步,和之前一樣,分別彈出,檢查公鑰與簽名是否配對(正確),
整個程序如果兩個Hash對不上,或者公鑰與私鑰簽名對不上,那么這個交易就是錯誤的,非法的
實體:
重點:
兩個保證:
1. 輸入中的公鑰和上一個輸出的公鑰的hash進行校驗,使input與output連接起來,保證使用者的身份的統一
2. 輸出入中的私鑰簽名與輸出的公鑰進行驗證,保證使用者使用此筆錢的權利,必須本人簽名了這個input才能被使用
2. 專案中的邏輯
采用P2PKH的校驗方式,輸入要包含公鑰和私鑰簽名,而輸出則需要包含公鑰的hash
公鑰的hash可以通過地址倒推:

這里的公鑰hash并不是簡單的最原始公鑰做一次SHA256,從圖中可以看出還經過了RIPEMD160的加密
代碼:
//地址轉其公鑰的hash函式
//(地址是由公鑰計算過來的, 可以逆推回去到公鑰的hash,但是無法逆推到原公鑰,原公鑰無法逆推到私鑰,因為hash函式不可逆)
func (Output *TxOutput)Lock(address string) {
// 1.base58函式的解碼
bytes25Data := base58.Decode(address)
// 2.去除尾部添加的4byte校驗碼和首部添加的1byte版本號
addressHash := bytes25Data[1: len(bytes25Data)-4]
// 3.賦值給Output
Output.PubKeyHash = addressHash
}
十一、地址String到公鑰hash的分場景安全策略
- 在NewTransaction函式中,我們使用地址,不采用反推公鑰Hash,因為涉及轉賬,此地址不一定是自己錢包中管理的地址,所以需要通過在本地錢包中讀取公私鑰,并且讀取的另一個目的就是接下來要使用私鑰
- 在getBalance函式中,獲取某個賬戶的余額,需要使用地址查詢UTXO,此時我們可以使用函式反推的方式去查詢,而不需要查詢本地錢包,因為對于區塊鏈系統來說,查詢余額功能是全網都可以使用的,不僅限于本地錢包賬戶,
十二、對于地址使用之前的校驗細節
不論是getBalacne還是Send轉賬,都需要對用戶輸入的地址進行校驗,
因為通過string逆推得到公鑰hash的方式會截去尾部的4位元組還有前面的四位元組,所以即使是后面幾位不同的地址不做檢測就查詢的話可能會查出一樣的結果


所以在查詢之前一定要做校驗
校驗的原理來自于那個四位元組的分支!
校驗流程思路: (先反向走在正向走岔路回來)
- 根據地址反推出25byte的資料,截斷后4位元組得到21位元組的資料
- 將這21byte資料進行兩次SHA256,再截取4位元組的校驗碼
- 通過校驗碼與原本的25位元組資料后四位比對
代碼:
//校驗地址
//校驗流程思路: (先反向走在正向走岔路回來)
func checkAddress(address string) bool {
//1. 根據地址反推出25byte的資料,截斷后4位元組得到21位元組的資料
bytes25Data := base58.Decode(address)
if len(bytes25Data) < 4 { //地址長度不夠直接回傳
return false
}
this4Bytes := bytes25Data[: 4]
//2. 將這21byte資料進行兩次SHA256,再截取4位元組的校驗碼
org4Bytes := CheckSum(adsToPubKeyHash(address))
//3. 通過校驗碼與原本的25位元組資料后四位比對
return bytes.Equal(this4Bytes, org4Bytes)
}
加上校驗后效果:

十三、 簽名驗證


1. 簽名需要的內容:
- 被簽名的資料
- 私鑰
2. 驗證需要的內容 :
- 已簽名的資料
- 公鑰
- 數字簽名
3. 注意事項
-
一個交易中同一個人的每一個input都需要簽名
-

具體簽名的資料需要能夠包含整個交易詳細內容
-
驗證時也是每個input都要驗證一次
-
簽名是由創建交易的節點完成,而校驗是驗證交易的節點完成
4. 對于每個input的簽名程序
每個input都有其對應的唯一的output,復制一份input,獲取其output的pubKeyHash賦值到input的pubKey中
對于每個交易中的input其自己生成的output就在同交易中并且其中自帶pubKeyHash和轉賬金額
對這個整體交易做hash,賦值到input的簽名中

1. 為什么不直接用創建新交易時打開的錢包中的公鑰去計算簽名hash,而是使用如此復雜的程序去尋找前一個hash?
因為直接用那個公鑰簽名的話那么驗證一定是成功的,因為公私鑰是一起那出來的,根據input中的前一個交易hash和outputindex找出來其關聯的output的pubKeyhash才是正確的做法,也是為了后面的驗證,
2. 注意對于每個input的簽名,在當前交易中與其無關的其他input的pubKey和ScriptSig都應該是空
代碼:
//簽名的實作
//引數:賬戶的私鑰
func (tx *Transaction) Signature(privateKey *ecdsa.PrivateKey, bc *BlockChain) {
// 1.復制一份input,獲取其output的pubKeyHash賦值到input的pubKey中(只是為了計算簽名)
trimmedCopyTx := TrimmedCopy(tx, bc)
//對于每個交易中的input其自己生成的output就在同交易中并且其中自帶pubKeyHash和轉賬金額
// 2.對這個整體交易做hash,賦值到input的簽名中
for i, input := range trimmedCopyTx.Vin{
//2.1 找到每個input關聯的上一個output的公鑰hash,并添加到當前的input的pubKey中
//獲取上一個交易
preTx, err := FindTxByTxHash(input.TxHash, bc)
if err != nil{
log.Panic("簽名時查找相關輸出交易出錯!", err)
}
//獲取該交易中的output中的公鑰hash
prePubKeyHash := preTx.Vout[input.OutputIndex].PubKeyHash
//注意!在這里直接對input賦值是無效的!!!
trimmedCopyTx.Vin[i].PubKey = prePubKeyHash
//2.2 簽名需要的資料都具備了,做hash處理
trimmedCopyTx.SetTxHash() //交易的hash就是需要的簽名資料
signDataHash := trimmedCopyTx.TxHash
//2.3 重要的一步!把當前交易中的這個input的pubKey還原為空,保證不影響其他input的簽名
trimmedCopyTx.Vin[i].PubKey = nil
//2.4 執行簽名動作得到r,s位元組流
r, s, err := ecdsa.Sign(rand.Reader, privateKey, signDataHash)
if err != nil{
log.Panic(err)
}
//2.5 把簽名放到原本交易的ScriptSig中
signnature := append(r.Bytes(), s.Bytes()...)
tx.Vin[i].ScriptSig = signnature
}
}
簽名的驗證
//驗證交易
func (tx *Transaction) Verify(bc *BlockChain) bool {
if tx.isCoinbaseTx(){
return true //鑄幣交易無需驗證
}
//1. 獲取驗證所需要的資料
// 1.1 Data
trimmedCopy := tx.TrimmedCopy(bc)
for i, input := range tx.Vin{ //注意,遍歷的是原本的交易
preTx, err := FindTxByTxHash(input.TxHash, bc)
if err != nil{
log.Panic(err)
}
trimmedCopy.Vin[i].PubKey = preTx.Vout[input.OutputIndex].PubKeyHash
//計算hash
trimmedCopy.SetTxHash()
//a. Data得到
dataHash := trimmedCopy.TxHash
// 還原
trimmedCopy.Vin[i].PubKey = nil
//b. 簽名得到
signature := input.ScriptSig
//c. 公鑰
//拆解PubKey, X, Y得到原生公鑰
PubKey := input.PubKey
//拆開簽名,得到r和s
r1 := big.Int{}
s1 := big.Int{}
//r是前半部分,s是后半部分
r1.SetBytes(signature[:len(signature)/2])
s1.SetBytes(signature[len(signature)/2:])
//拆開公鑰,得到x和y
x := big.Int{}
y := big.Int{}
//r是前半部分,s是后半部分
x.SetBytes(PubKey[:len(PubKey)/2])
y.SetBytes(PubKey[len(PubKey)/2:])
//得到公鑰原型
pubKeyOrigin := ecdsa.PublicKey{elliptic.P256(), &x, &y}
//1.2 verify
if !ecdsa.Verify(&pubKeyOrigin, dataHash, &r1, &s1){
return false //一旦有一個input驗證錯誤就失敗
}
}
return true
}
十四、專案總結
1. 專案完成的功能
- 區塊的創建
- 交易的打包
- 用戶余額的查詢
- pow挖礦演算法
- 錢包功能,增加賬戶、管理賬戶等
- 轉賬功能
- 公私鑰簽名以及驗證
2. 專案的待完善地方(缺點)
- 每個區塊都是只能打包一個交易就直接發布了,沒有區塊鏈網路體系去獲取交易
- 分布式網路共識協議沒有實作
- 梅克爾樹root的計算,目前專案只是簡單的拼接位元組
- 簽名機制待完善,專案使用的簽名驗證方式是P2PKH,還有P2SH、多重簽名等可以完善
- 遠程訪問rpc呼叫,類似于geth的遠程訪問
- 客戶端的構建
十五、專案代碼地址
https://github.com/xwjahahahaha/simpleBitCoin/tree/main/version9
Tips
1.Go語言語法
- 在同一個包下的go檔案不需要使用import匯入
- 使用命令
go run xxx.go命令運行main主函式的時候,如果同個包下的go檔案互相呼叫函式,單獨go build main.go(編譯),go run main.go(運行)是不對的:

解決辦法:
- linux下:
go build *.gogo run *.go - windows下:
go build./go run ./
2.Goland的使用技巧
ctrl + b 進入查看函式實作
3.Blot中要注意的細節
bucket.Put()方法如果bucket不存在那么就是直接的添加,但是如果已經存在了,那么就是更新,所以不存在Key重復的問題
4.go結構體內欄位命名不規范導致使用gob出錯!!!!Golang大坑!
注意:go語言中的結構體內的欄位都必須首字母大寫,不然會報如下錯誤:
gob: type main.Person has no exported fields
記住,Golang的結構體命名欄位必須要大寫,不然序列化都可能不行!!!!!
5. go回圈修改陣列值無效
注意Go語言和python一樣回圈修改資料的值必須使用索引!!!!
例: 錯誤示范:
for _, output := range outputArray{
output.TxHash = newTxHash
}
正確做法:
for i := range outputArray{
outputArray[i].TxHash = newTxHash
}
6. 關于golang.org/x包問題
由于谷歌被墻,跟谷歌相關的模塊無法通過go get來下載,解決方法:
git clone https://github.com/golang/net.git $GOPATH/src/github.com/golang/net
git clone https://github.com/golang/sys.git $GOPATH/src/github.com/golang/sys
git clone https://github.com/golang/tools.git $GOPATH/src/github.com/golang/tools
ln -s $GOPATH/src/github.com/golang $GOPATH/src/golang.org/x
上面三條命令會把所要用到的官方輔助包都下載到$GOPATH/src/github.com/golang中,在windows下軟連接不好弄,我的方法就是下載好了以后把這些復制一份到``$GOPATH/src/golang.org/x下,使用的時候就優先使用golang.org/x下的包,
7. 使用gob進行編碼
使用gob進行編碼的時候,如果位元組流中或者自定義的結構有interface()物件那么需要提前注冊
編碼解碼的時候都需要添加!
轉載請註明出處,本文鏈接:https://www.uj5u.com/shujuku/158488.html
標籤:其他
上一篇:【密碼學原理】RSA演算法
