源代碼地址:[https://github.com/daleboy/blockchain5](https://github.com/daleboy/blockchain5)
# 前言
在上一篇中,我們把一個由用戶定義的任意字符串當成是地址,現在我們將要實現一個跟比特幣一樣的真實地址。
# 比特幣地址
比特幣地址是基于加密算法的組合創建的密鑰,實際上是將公鑰表示成人類可讀的形式,并且保證這個世界上沒有人其他人可以取走你的幣,除非拿到你的私鑰。
# 公鑰加密
公鑰加密(public-key cryptography)算法使用的是成對的密鑰:公鑰和私鑰。
本質上,比特幣錢包也只不過是這樣的密鑰對而已。當你安裝一個錢包應用,或是使用一個比特幣客戶端來生成一個新地址時,它就會為你生成一對密鑰。在比特幣中,誰擁有了私鑰,誰就可以控制所以發送到這個公鑰的幣。
私鑰和公鑰只不過是隨機的字節序列,因此它們無法在屏幕上打印,人類也無法通過肉眼去讀取。這就是為什么比特幣使用了一個轉換算法(公鑰加密),將公鑰轉化為一個人類可讀的字符串(也就是我們看到的地址)。
公鑰和私鑰必須確保生成真正的隨機字節。比特幣采用橢圓曲線產生私鑰。
比特幣使用 Base58 算法將公鑰轉換成人類可讀的形式(公鑰加密算法)。這個算法跟著名的 Base64 很類似,區別在于它使用了更短的字母表:為了避免一些利用字母相似性的攻擊,從字母表中移除了一些字母。也就是,沒有這些符號:0(零),O(大寫的 o),I(大寫的i),l(小寫的 L),因為這幾個字母看著很像。另外,也沒有 + 和 / 符號。
# 每一筆交易輸入都會由創建交易的人簽名
比特幣使用的是 ECDSA(Elliptic Curve Digital Signature Algorithm)算法來對交易進行簽名,我們也會使用該算法。
在數學和密碼學中,有一個數字簽名(digital signature)的概念,算法可以保證:
1. 當數據從發送方傳送到接收方時,數據不會被修改;
2. 數據由某一確定的發送方創建;
3. 發送方無法否認發送過數據這一事實。
通過在數據上應用簽名算法(也就是對數據進行簽名),你就可以得到一個簽名,這個簽名晚些時候會被驗證。生成數字簽名需要一個私鑰,而驗證簽名需要一個公鑰。簽名有點類似于印章,比方說我做了一幅畫,完了用印章一蓋,就說明了這幅畫是我的作品。給數據生成簽名,就是給數據蓋了章。
為了對數據進行簽名,我們需要下面兩樣東西:
1. 要簽名的數據
2. 私鑰
應用簽名算法可以生成一個簽名,并且這個簽名會被存儲在交易輸入中。為了對一個簽名進行驗證,我們需要以下三樣東西:
1. 被簽名的數據
2. 簽名
3. 公鑰
簡單來說,驗證過程可以被描述為:檢查簽名是由被簽名數據加上私鑰得來,并且公鑰恰好是由該私鑰生成。
> 數據簽名并不是加密,你無法從一個簽名重新構造出數據。這有點像哈希:你在數據上運行一個哈希算法,然后得到一個該數據的唯一表示。簽名與哈希的區別在于密鑰對:有了密鑰對,才有簽名驗證。但是密鑰對也可以被用于加密數據:私鑰用于加密,公鑰用于解密數據。不過比特幣并不使用加密算法。
**在比特幣中,每一筆交易輸入都會由創建交易的人簽名。**,創始區塊例外,因為創始區塊是沒有輸入的,所以也不需要簽名。在被放入到一個塊之前,必須要對每一筆交易進行驗證。除了一些其他步驟,驗證意味著:
1. 檢查交易輸入有權使用來自之前交易的輸出(同一人)
2. 檢查交易簽名是正確的

# 實現錢包地址
## 錢包
錢包結構如下:
~~~
//Wallet 錢包結構
type Wallet struct {
PrivateKey ecdsa.PrivateKey//錢包私鑰,誰擁有私鑰,誰就擁有錢包
PublicKey []byte
}
//Wallets 多個錢包
type Wallets struct {
Wallets map[string]*Wallet//value為struct類型,一般用map存儲
}
//NewWallet 創建一個錢包
func NewWallet() *Wallet {
private, public := newKeyPair()
wallet := Wallet{private, public}
return &wallet
}
//newKeyPair 創建公私鑰對
func newKeyPair() (ecdsa.PrivateKey, []byte) {
curve := elliptic.P256()
private, err := ecdsa.GenerateKey(curve, rand.Reader)//生成私鑰
//公鑰從私鑰生成:將兩個slice拼接到一起(這種方式使用append只能用兩個參數,第二個參數的名稱后需要加三個點)
pubKey := append(private.PublicKey.X.Bytes(), private.PublicKey.Y.Bytes()...)
return *private, pubKey
}
~~~
## 生成一個地址:將公鑰轉換為Base58地址
~~~
//GetAddress address=version(1個字節)+public key hash(32個字節)+checksum(4字節)
func (w Wallet) GetAddress() []byte {
pubKeyHash := HashPubKey(w.PublicKey)
//version:生成算法的版本的前綴
versionedPayload := append([]byte{version}, pubKeyHash...)
checksum := checksum(versionedPayload)
fullPayload := append(versionedPayload, checksum...)
address := Base58Encode(fullPayload)
return address
}
//HashPubKey 公鑰哈希轉為Base58編碼
func HashPubKey(pubKey []byte) []byte {
publicSHA256 := sha256.Sum256(pubKey)
RIPEMD160Hasher := ripemd160.New()
_, err := RIPEMD160Hasher.Write(publicSHA256[:])
publicRIPEMD160 := RIPEMD160Hasher.Sum(nil)
return publicRIPEMD160
}
//校驗值:4字節
func checksum(payload []byte) []byte {
firstSHA := sha256.Sum256(payload)
secondSHA := sha256.Sum256(firstSHA[:])//兩次哈希計算
//addressChecksumLen=4,這里只截取secondSHA的4個字節長度切片
return secondSHA[:addressChecksumLen]
}
~~~
## 修改輸入和輸出來使用地址
交易中輸入者的身份,必須和輸入中引用的輸出者的身份相同(只有自己才有權處置自己的資產)
~~~
ype TXInput struct {
Txid []byte
Vout int//引用的輸出在原輸出列表中的編號
Signature []byte//輸入的簽名
PubKey []byte//公鑰
}
//UsesKey 是否可以解鎖輸入中的輸出
func (in *TXInput) UsesKey(pubKeyHash []byte) bool {
lockingHash := HashPubKey(in.PubKey)
return bytes.Compare(lockingHash, pubKeyHash) == 0
}
type TXOutput struct {
Value int
PubKeyHash []byte//公鑰哈希,并不存儲公鑰本身
}
//Lock 設置輸出中的公鑰哈希,鎖定輸出
func (out *TXOutput) Lock(address []byte) {
pubKeyHash := Base58Decode(address)//解碼出公鑰
pubKeyHash = pubKeyHash[1 : len(pubKeyHash)-4]//除掉version和checksum得到公鑰哈希
out.PubKeyHash = pubKeyHash
}
//IsLockedWithKey 檢查輸出的擁有者,不直接檢查公鑰是否相同,而是檢查公鑰的哈希是否相同
func (out *TXOutput) IsLockedWithKey(pubKeyHash []byte) bool {
return bytes.Compare(out.PubKeyHash, pubKeyHash) == 0
}
~~~
## 交易簽名
(1)交易必須被交易發起者使用自己的私鑰進行簽名。因為這是比特幣里面保證發送方不會花費屬于其他人的幣的唯一方式。
(2)簽名必須是有效的。如果一個簽名是無效的,那么這筆交易就會被認為是無效的,因此,這筆交易也就無法被加到區塊鏈中。
(3)我們現在離實現交易簽名還差一件事情:用于簽名的數據。一筆交易的哪些部分需要簽名?又或者說,要對完整的交易進行簽名?選擇簽名的數據相當重要。因為用于簽名的這個數據,必須要包含能夠唯一識別數據的信息。比如,如果僅僅對輸出值進行簽名并沒有什么意義,因為簽名不會考慮發送方和接收方。
考慮到交易解鎖的是之前的輸出,然后重新分配里面的價值,并鎖定新的輸出,那么必須要簽名以下數據:
1. 存儲在已解鎖輸出的公鑰哈希。它識別了一筆交易的“發送方”。(from)
2. 存儲在新的鎖定輸出里面的公鑰哈希。它識別了一筆交易的“接收方”。(to)
3. 新的輸出值。
> 在比特幣中,鎖定/解鎖邏輯被存儲在腳本中,它們被分別存儲在輸入和輸出的`ScriptSig`和`ScriptPubKey`字段。由于比特幣允許這樣不同類型的腳本,它對`ScriptPubKey`的整個內容進行了簽名。
可以看到,我們不需要對存儲在輸入里面的公鑰簽名。因此,在比特幣里, 所簽名的并不是一個交易,而是一個去除部分內容的輸入副本,輸入里面存儲了被引用輸出的`ScriptPubKey`。
可以看到,我們不需要對存儲在輸入里面的公鑰簽名。因此,在比特幣里, 所簽名的并不是一個交易,而是一個去除部分內容的輸入副本,輸入里面存儲了被引用輸出的`ScriptPubKey`。
~~~
//Sign 對當前交易進行簽名,需要把輸入所引用的輸出交易prevTXs作為參數進行處理
func (tx *Transaction) Sign(privKey ecdsa.PrivateKey, prevTXs map[string]Transaction) {
if tx.IsCoinbase() {//coinbase交易沒有實際輸入,所以沒有無需簽名
return
}
txCopy := tx.TrimmedCopy()//將會被簽署的是修剪后的當前交易的交易副本,而不是一個完整交易:
for inID, vin := range txCopy.Vin {//迭代副本中的每一個輸入
prevTx := prevTXs[hex.EncodeToString(vin.Txid)]
//在每個輸入中,`Signature`被設置為`nil`(Signature僅僅是一個雙重檢驗,所以沒有必要放進來)
txCopy.Vin[inID].Signature = nil
//`pubKey`被設置為所引用輸出的`PubKeyHash
txCopy.Vin[inID].PubKey = prevTx.Vout[vin.Vout].PubKeyHash
txCopy.ID = txCopy.Hash()
txCopy.Vin[inID].PubKey = nil
r, s, err := ecdsa.Sign(rand.Reader, &privKey, txCopy.ID)//簽名的是交易副本的ID(即交易副本的哈希)
signature := append(r.Bytes(), s.Bytes()...)//一個 ECDSA 簽名就是一對數字。連接切片,構建簽名
//**副本中每一個輸入是被分開簽名的**
//盡管這對于我們的應用并不十分緊要,但是比特幣允許交易包含引用了不同地址的輸入
tx.Vin[inID].Signature = signature
}
}
//TrimmedCopy 獲得修剪后的交易副本
func (tx *Transaction) TrimmedCopy() Transaction {
var inputs []TXInput
var outputs []TXOutput
for _, vin := range tx.Vin {
//包含了所有的輸入和輸出,但是`TXInput.Signature`和`TXIput.PubKey`被設置為`nil`
//在調用這個方法后,會用引用的前一個交易的輸出的PubKeyHash,取代這里的PubKey
inputs = append(inputs, TXInput{vin.Txid, vin.Vout, nil, nil})
}
for _, vout := range tx.Vout {
outputs = append(outputs, TXOutput{vout.Value, vout.PubKeyHash})
}
txCopy := Transaction{tx.ID, inputs, outputs}
return txCopy
}
~~~
## 驗證簽名
~~~
func (tx *Transaction) Verify(prevTXs map[string]Transaction) bool {
if?tx.IsCoinbase()?{
returntrue
????}
txCopy := tx.TrimmedCopy()//同一筆交易的副本
curve := elliptic.P256()//生成密鑰對的橢圓曲線
for inID, vin := range tx.Vin {//迭代每個輸入
//以下代碼跟簽名一樣,因為在驗證階段,我們需要的是與簽名相同的數據
prevTx := prevTXs[hex.EncodeToString(vin.Txid)]
txCopy.Vin[inID].Signature = nil
txCopy.Vin[inID].PubKey = prevTx.Vout[vin.Vout].PubKeyHash
txCopy.ID = txCopy.Hash()
txCopy.Vin[inID].PubKey = nil
//解包存儲在`TXInput.Signature`和`TXInput.PubKey`中的值
//一個簽名就是一對長度相同的數字。
r := big.Int{}
s := big.Int{}
sigLen := len(vin.Signature)
r.SetBytes(vin.Signature[:(sigLen / 2)])
s.SetBytes(vin.Signature[(sigLen / 2):])
//一個公鑰(輸入提取的公鑰)就是一對長度相同的坐標。
x := big.Int{}
y := big.Int{}
keyLen := len(vin.PubKey)
x.SetBytes(vin.PubKey[:(keyLen / 2)])
y.SetBytes(vin.PubKey[(keyLen / 2):])
//從輸入提取的公鑰創建一個rawPubKey
rawPubKey := ecdsa.PublicKey{curve, &x, &y}
//使用公鑰驗證副本的簽名,是否私鑰簽名檔結果一致(&r和&s是私鑰簽名txCopy.ID的結果)
if ecdsa.Verify(&rawPubKey, txCopy.ID, &r, &s) == false {
return false
}
}
return true
}
~~~
## 獲得之前的交易
~~~
//FindTransaction 根據交易ID,獲得交易
func (bc *Blockchain) FindTransaction(ID []byte) (Transaction, error) {
bci := bc.Iterator()
for {
block := bci.Next()
for _, tx := range block.Transactions {
if bytes.Compare(tx.ID, ID) == 0 {
return *tx, nil
}
}
if len(block.PrevBlockHash) == 0 {
break
}
}
return Transaction{}, errors.New("沒有找到交易")
}
//SignTransaction 簽名之前的交易
func (bc *Blockchain) SignTransaction(tx *Transaction, privKey ecdsa.PrivateKey) {
prevTXs := make(map[string]Transaction)
for _, vin := range tx.Vin {
prevTX, err := bc.FindTransaction(vin.Txid)//輸入中保存了引用輸出交易的ID,由此可以查到之前的交易
prevTXs[hex.EncodeToString(prevTX.ID)] = prevTX
}
tx.Sign(privKey, prevTXs)
}
//VerifyTransaction 驗證之前交易的簽名
func (bc *Blockchain) VerifyTransaction(tx *Transaction) bool {
prevTXs := make(map[string]Transaction)
for _, vin := range tx.Vin {
prevTX, err := bc.FindTransaction(vin.Txid)//輸入中保存了引用輸出交易的ID,由此可以查到之前的交易
prevTXs[hex.EncodeToString(prevTX.ID)] = prevTX
}
return tx.Verify(prevTXs)
}
~~~
## 對當前交易進行簽名和驗證交易簽名
簽名在`NewUTXOTransaction`中進行:
~~~
func NewUTXOTransaction(from, to string, amount int, bc *Blockchain) *Transaction {
...
tx := Transaction{nil, inputs, outputs}//inputs將簽名部分設為了nil
tx.ID = tx.Hash()
bc.SignTransaction(&tx, wallet.PrivateKey)//通過錢包的私鑰進行簽名
return &tx
}
~~~
在一筆交易被放入一個塊之前進行驗證:
~~~
func (bc *Blockchain) MineBlock(transactions []*Transaction) {
var lastHash []byte
for _, tx := range transactions {
if bc.VerifyTransaction(tx) != true {
log.Panic("ERROR: 非法交易。")
}
}
...
}
~~~
## 運行測試
>1、創建兩個錢包地址

>2、創建新的區塊鏈

>3、轉賬

>4、轉賬后各賬戶余額查詢

>5、打印區塊鏈

可以看到,轉賬交易有兩個輸出,一個是收款人獲得的幣,一個是給發送者的找零。
>6、打印本地所有錢包地址

- 重要更新說明
- linechain發布
- linechain新版設計
- 引言一
- 引言二
- 引言三
- vs-code設置及開發環境設置
- BoltDB數據庫應用
- 關于Go語言、VS-code的一些Tips
- 區塊鏈的架構
- 網絡通信與區塊鏈
- 單元測試
- 比特幣腳本語言
- 關于區塊鏈的一些概念
- 區塊鏈組件
- 區塊鏈第一版:基本原型
- 區塊鏈第二版:增加工作量證明
- 區塊鏈第三版:持久化
- 區塊鏈第四版:交易
- 區塊鏈第五版:實現錢包
- 區塊鏈第六版:實現UTXO集
- 區塊鏈第七版:網絡
- 階段小結
- 區塊鏈第八版:P2P
- P2P網絡架構
- 區塊鏈網絡層
- P2P區塊鏈最簡體驗
- libp2p建立P2P網絡的關鍵概念
- 區塊鏈結構層設計與實現
- 用戶交互層設計與實現
- 網絡層設計與實現
- 建立節點發現機制
- 向區塊鏈網絡請求區塊信息
- 向區塊鏈網絡發布消息
- 運行區塊鏈
- LineChain
- 系統運行流程
- Multihash
- 區塊鏈網絡的節點發現機制深入探討
- DHT
- Bootstrap
- 連接到所有引導節點
- Advertise
- 搜索其它peers
- 連接到搜到的其它peers
- 區塊鏈網絡的消息訂發布-訂閱機制深入探討
- LineChain:適用于智能合約編程的腳本語言支持
- LineChain:解決分叉問題
- LineChain:多重簽名
- libp2p升級到v0.22版本
- 以太坊基礎
- 重溫以太坊的樹結構
- 世界狀態樹
- (智能合約)賬戶存儲樹
- 交易樹
- 交易收據樹
- 小結
- 以太坊的存儲結構
- 以太坊狀態數據庫
- MPT
- 以太坊POW共識算法
- 智能合約存儲
- Polygon Edge
- block結構
- transaction數據結構
- 數據結構小結
- 關于本區塊鏈的一些說明
- UML工具-PlantUML
- libp2p介紹
- JSON-RPC
- docker制作:啟動多個應用系統
- Dockerfile
- docker-entrypoint.sh
- supervisord.conf
- docker run
- nginx.conf
- docker基礎操作整理
- jupyter計算交互環境
- git技巧一
- git技巧二
- 使用github項目的最佳實踐
- windows下package管理工具