這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
翻譯的系列文章我已經放到了 GitHub 上:blockchain-tutorial,後續如有更新都會在 GitHub 上,可能就不在這裡同步了。如果想直接運行代碼,也可以 clone GitHub 上的教程倉庫,進入 src 目錄執行 make 即可。
引言
在上一篇文章中,我們已經初步實現了交易。相信你應該瞭解了交易中的一些天然屬性,這些屬性沒有絲毫“個人”色彩的存在:在比特幣中,沒有使用者賬戶,不需要也不會在任何地方儲存個人資料(比如姓名,護照號碼或者 SSN)。但是,我們總要有某種途徑識別出你是交易輸出的所有者(也就是說,你擁有在這些輸出上鎖定的幣)。這就是比特幣地址(address)需要完成的使命。在上一篇中,我們把一個由使用者定義的任一字元串當成是地址,現在我們將要實現一個跟比特幣一樣的真真實位址。
本文的代碼實現變化很大,請點擊 這裡 查看所有的代碼更改。
比特幣地址
這就是一個真實的比特幣地址:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa。這是史上第一個比特幣地址,據說屬於中本聰。比特幣地址是完全公開的,如果你想要給某個人發送幣,只需要知道他的地址就可以了。但是,地址(儘管地址也是獨一無二的)並不是用來證明你是一個“錢包”所有者的信物。實際上,所謂的地址,只不過是將公開金鑰表示成人類可讀的形式而已,因為原生的公開金鑰人類很難閱讀。在比特幣中,你的身份(identity)就是一對(或者多對)儲存在你的電腦(或者你能夠擷取到的地方)上的公開金鑰(public key)和私密金鑰(private key)。比特幣基於一些密碼編譯演算法的組合來建立這些密鑰,並且保證了在這個世界上沒有其他人能夠取走你的幣,除非拿到你的密鑰。下面,讓我們來討論一下這些演算法到底是什麼。
公開金鑰加密
公開金鑰加密(public-key cryptography)演算法使用的是成對的密鑰:公開金鑰和私密金鑰。公開金鑰並不是敏感資訊,可以告訴其他人。但是,私密金鑰絕對不能告訴其他人:只有所有者(owner)才能知道私密金鑰,能夠識別,評鑑和證明所有者身份的就是私密金鑰。在加密貨幣的世界中,你的私密金鑰代表的就是你,私密金鑰就是一切。
本質上,比特幣錢包也只不過是這樣的金鑰組而已。當你安裝一個錢包應用,或是使用一個比特幣用戶端來產生一個新地址時,它就會為你產生一對密鑰。在比特幣中,誰擁有了私密金鑰,誰就可以控制所以發送到這個公開金鑰的幣。
私密金鑰和公開金鑰只不過是隨機的位元組序列,因此它們無法在螢幕上列印,人類也無法通過肉眼去讀取。這就是為什麼比特幣使用了一個轉換演算法,將公開金鑰轉化為一個人類可讀的字串(也就是我們看到的地址)。
如果你用過比特幣錢包應用,很可能它會為你產生一個助記符。這樣的助記符可以用來替代私密金鑰,並且可以被用於產生私密金鑰。BIP-039 已經實現了這個機制。
好了,現在我們已經知道了在比特幣中證明使用者身份的是私密金鑰。那麼,比特幣如何檢查交易輸出(和儲存在裡面的幣)的所有權呢?
數位簽章
在數學和密碼學中,有一個數位簽章(digital signature)的概念,演算法可以保證:
- 當資料從發送方傳送到接收方時,資料不會被修改;
- 資料由某一確定的發送方建立;
- 發送方無法否認發送過資料這一事實。
通過在資料上應用簽名演算法(也就是對資料進行簽名),你就可以得到一個簽名,這個簽名晚些時候會被驗證。產生數位簽章需要一個私密金鑰,而驗證簽名需要一個公開金鑰。簽名有點類似於印章,比方說我做了一幅畫,完了用印章一蓋,就說明了這幅畫是我的作品。給資料產生簽名,就是給資料蓋了章。
為了對資料進行簽名,我們需要下面兩樣東西:
- 要簽名的資料
- 私密金鑰
應用簽名演算法可以產生一個簽名,並且這個簽名會被儲存在交易輸入中。為了對一個簽名進行驗證,我們需要以下三樣東西:
- 被簽名的資料
- 簽名
- 公開金鑰
簡單來說,驗證過程可以被描述為:檢查簽名是由被簽名資料加上私密金鑰得來,並且公開金鑰恰好是由該私密金鑰產生。
資料簽名並不是加密,你無法從一個簽名重新構造出資料。這有點像雜湊:你在資料上運行一個雜湊演算法,然後得到一個該資料的唯一表示。簽名與雜湊的區別在於金鑰組:有了金鑰組,才有簽名驗證。但是金鑰組也可以被用於加密資料:私密金鑰用於加密,公開金鑰用於解密資料。不過比特幣並不使用密碼編譯演算法。
在比特幣中,每一筆交易輸入都會由建立交易的人簽名。在被放入到一個塊之前,必須要對每一筆交易進行驗證。除了一些其他步驟,驗證意味著:
- 檢查交易輸入有權使用來自之前交易的輸出
- 檢查交易簽名是正確的
,對資料進行簽名和對簽名進行驗證的過程大致如下:
現在來回顧一個交易完整的生命週期:
- 起初,創世塊裡麵包含了一個 coinbase 交易。在 coinbase 交易中,沒有輸入,所以也就不需要簽名。coinbase 交易的輸出包含了一個雜湊過的公開金鑰(使用的是
RIPEMD16(SHA256(PubKey)) 演算法)
- 當一個人發送幣時,就會建立一筆交易。這筆交易的輸入會引用之前交易的輸出。每個輸入會儲存一個公開金鑰(沒有被雜湊)和整個交易的一個簽名。
- 比特幣網路中接收到交易的其他節點會對該交易進行驗證。除了一些其他事情,他們還會檢查:在一個輸入中,公開金鑰雜湊與所引用的輸出雜湊相匹配(這保證了發送方只能花費屬於自己的幣);簽名是正確的(這保證了交易是由幣的實際擁有者所建立)。
- 當一個礦工準備挖一個新塊時,他會將交易放到塊中,然後開始挖礦。
- 當新塊被挖出來以後,網路中的所有其他節點會接收到一條訊息,告訴其他人這個塊已經被挖出並被加入到區塊鏈。
- 當一個塊被加入到區塊鏈以後,交易就算完成,它的輸出就可以在新的交易中被引用。
橢圓曲線密碼編譯
正如之前提到的,公開金鑰和私密金鑰是隨機的位元組序列。私密金鑰能夠用於證明持幣人的身份,需要有一個條件:隨機演算法必鬚生成真正隨機的位元組。因為沒有人會想要產生一個私密金鑰,而這個私密金鑰意外地也被別人所有。
比特幣使用橢圓曲線來產生私密金鑰。橢圓曲線是一個複雜的數學概念,我們並不打算在這裡作太多解釋(如果你真的十分好奇,可以查看這篇文章,注意:有很多數學公式!)我們只要知道這些曲線可以產生非常大的隨機數就夠了。在比特幣中使用的曲線可以隨機選取在 0 與 2 ^ 2 ^ 56(大概是 10^77, 而整個可見的宇宙中,原子數在 10^78 到 10^82 之間) 的一個數。有如此高的一個上限,意味著幾乎不可能發生有兩次產生同一個私密金鑰的事情。
比特幣使用的是 ECDSA(Elliptic Curve Digital Signature Algorithm)演算法來對交易進行簽名,我們也會使用該演算法。
Base58
回到上面提到的比特幣地址:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa 。現在,我們已經知道了這是公開金鑰用人類可讀的形式表示而已。如果我們對它進行解碼,就會看到公開金鑰的本來面目(16 進位表示的位元組):
0062E907B15CBF27D5425399EBF6F0FB50EBB88F18C29B7D93
比特幣使用 Base58 演算法將公開金鑰轉換成人類可讀的形式。這個演算法跟著名的 Base64 很類似,區別在於它使用了更短的字母表:為了避免一些利用字母相似性的攻擊,從字母表中移除了一些字母。也就是,沒有這些符號:0(零),O(大寫的 o),I(大寫的i),l(小寫 L),因為這幾個字母看著很像。另外,也沒有 + 和 / 符號。
是從一個公開金鑰獲得一個地址的過程:
因此,上面提到的公開金鑰解碼後包含三個部分:
Version Public key hash Checksum00 62E907B15CBF27D5425399EBF6F0FB50EBB88F18 C29B7D93
由於雜湊函數是單向的(也就說無法逆轉回去),所以不可能從一個雜湊中提取公開金鑰。不過通過執行雜湊函數並進行雜湊比較,我們可以檢查一個公開金鑰是否被用於雜湊的產生。
好了,所有細節都已就緒,來寫代碼吧。很多概念只有當寫代碼的時候,才能理解地更透徹。
實現地址
我們先從錢包 Wallet 結構開始:
type Wallet struct { PrivateKey ecdsa.PrivateKey PublicKey []byte}type Wallets struct { Wallets map[string]*Wallet}func NewWallet() *Wallet { private, public := newKeyPair() wallet := Wallet{private, public} return &wallet}func newKeyPair() (ecdsa.PrivateKey, []byte) { curve := elliptic.P256() private, err := ecdsa.GenerateKey(curve, rand.Reader) pubKey := append(private.PublicKey.X.Bytes(), private.PublicKey.Y.Bytes()...) return *private, pubKey}
一個錢包只有一個金鑰組而已。我們需要 Wallets 類型來儲存多個錢包的組合,將它們儲存到檔案中,或者從檔案中進行載入。Wallet 的建構函式會產生一個新的金鑰組。newKeyPair 函數非常直觀:ECDSA 基於橢圓曲線,所以我們需要一個橢圓曲線。接下來,使用橢圓產生一個私密金鑰,然後再從私密金鑰產生一個公開金鑰。有一點需要注意:在基於橢圓曲線的演算法中,公開金鑰是曲線上的點。因此,公開金鑰是 X,Y 座標的組合。在比特幣中,這些座標會被串連起來,然後形成一個公開金鑰。
現在,來產生一個地址:
func (w Wallet) GetAddress() []byte { pubKeyHash := HashPubKey(w.PublicKey) versionedPayload := append([]byte{version}, pubKeyHash...) checksum := checksum(versionedPayload) fullPayload := append(versionedPayload, checksum...) address := Base58Encode(fullPayload) return address}func HashPubKey(pubKey []byte) []byte { publicSHA256 := sha256.Sum256(pubKey) RIPEMD160Hasher := ripemd160.New() _, err := RIPEMD160Hasher.Write(publicSHA256[:]) publicRIPEMD160 := RIPEMD160Hasher.Sum(nil) return publicRIPEMD160}func checksum(payload []byte) []byte { firstSHA := sha256.Sum256(payload) secondSHA := sha256.Sum256(firstSHA[:]) return secondSHA[:addressChecksumLen]}
將一個公開金鑰轉換成一個 Base58 地址需要以下步驟:
- 使用
RIPEMD160(SHA256(PubKey)) 雜湊演算法,取公開金鑰並對其雜湊兩次
- 給雜湊加上地址產生演算法版本的首碼
- 對於第二步產生的結果,使用
SHA256(SHA256(payload)) 再雜湊,計算校正和。校正和是結果雜湊的前四個位元組。
- 將校正和附加到
version+PubKeyHash 的組合中。
- 使用 Base58 對
version+PubKeyHash+checksum 組合進行編碼。
至此,就可以得到一個真實的比特幣地址,你甚至可以在 blockchain.info 查看它的餘額。不過我可以負責任地說,無論產生一個新的地址多少次,檢查它的餘額都是 0。這就是為什麼選擇一個合適的公開金鑰加密演算法是如此重要:考慮到私密金鑰是隨機數,產生同一個數位機率必須是儘可能地低。理想情況下,必須是低到“永遠”不會重複。
另外,注意:你並不需要串連到一個比特幣節點來獲得一個地址。地址產生演算法使用的多種開源演算法可以通過很多程式設計語言和庫實現。
現在我們需要修改輸入和輸出來使用地址:
type TXInput struct { Txid []byte Vout int Signature []byte PubKey []byte}func (in *TXInput) UsesKey(pubKeyHash []byte) bool { lockingHash := HashPubKey(in.PubKey) return bytes.Compare(lockingHash, pubKeyHash) == 0}type TXOutput struct { Value int PubKeyHash []byte}func (out *TXOutput) Lock(address []byte) { pubKeyHash := Base58Decode(address) pubKeyHash = pubKeyHash[1 : len(pubKeyHash)-4] out.PubKeyHash = pubKeyHash}func (out *TXOutput) IsLockedWithKey(pubKeyHash []byte) bool { return bytes.Compare(out.PubKeyHash, pubKeyHash) == 0}
注意,現在我們已經不再需要 ScriptPubKey 和 ScriptSig 欄位,因為我們不會實現一個指令碼語言。相反,ScriptSig 會被分為 Signature 和 PubKey 欄位,ScriptPubKey 被重新命名為 PubKeyHash。我們會實現跟比特幣裡一樣的輸出鎖定/解鎖和輸入簽名邏輯,不同的是我們會通過方法(method)來實現。
UsesKey 方法檢查輸入使用了指定密鑰來解鎖一個輸出。注意到輸入儲存的是原生的公開金鑰(也就是沒有被雜湊的公開金鑰),但是這個函數要求的是雜湊後的公開金鑰。IsLockedWithKey 檢查是否提供的公開金鑰雜湊被用於鎖定輸出。這是一個 UsesKey 的輔助函數,並且它們都被用於 FindUnspentTransactions 來形成交易之間的聯絡。
Lock 只是簡單地鎖定了一個輸出。當我們給某個人發送幣時,我們只知道他的地址,因為這個函數使用一個地址作為唯一的參數。然後,地址會被解碼,從中提取出公開金鑰雜湊並儲存在 PubKeyHash 欄位。
現在,來檢查一下是否都能如期工作:
$ blockchain_go createwalletYour new address: 13Uu7B1vDP4ViXqHFsWtbraM3EfQ3UkWXt$ blockchain_go createwalletYour new address: 15pUhCbtrGh3JUx5iHnXjfpyHyTgawvG5h$ blockchain_go createwalletYour new address: 1Lhqun1E9zZZhodiTqxfPQBcwr1CVDV2sy$ blockchain_go createblockchain -address 13Uu7B1vDP4ViXqHFsWtbraM3EfQ3UkWXt0000005420fbfdafa00c093f56e033903ba43599fa7cd9df40458e373eee724dDone!$ blockchain_go getbalance -address 13Uu7B1vDP4ViXqHFsWtbraM3EfQ3UkWXtBalance of '13Uu7B1vDP4ViXqHFsWtbraM3EfQ3UkWXt': 10$ blockchain_go send -from 15pUhCbtrGh3JUx5iHnXjfpyHyTgawvG5h -to 13Uu7B1vDP4ViXqHFsWtbraM3EfQ3UkWXt -amount 52017/09/12 13:08:56 ERROR: Not enough funds$ blockchain_go send -from 13Uu7B1vDP4ViXqHFsWtbraM3EfQ3UkWXt -to 15pUhCbtrGh3JUx5iHnXjfpyHyTgawvG5h -amount 600000019afa909094193f64ca06e9039849709f5948fbac56cae7b1b8f0ff162Success!$ blockchain_go getbalance -address 13Uu7B1vDP4ViXqHFsWtbraM3EfQ3UkWXtBalance of '13Uu7B1vDP4ViXqHFsWtbraM3EfQ3UkWXt': 4$ blockchain_go getbalance -address 15pUhCbtrGh3JUx5iHnXjfpyHyTgawvG5hBalance of '15pUhCbtrGh3JUx5iHnXjfpyHyTgawvG5h': 6$ blockchain_go getbalance -address 1Lhqun1E9zZZhodiTqxfPQBcwr1CVDV2syBalance of '1Lhqun1E9zZZhodiTqxfPQBcwr1CVDV2sy': 0
很好!現在我們來實現交易簽名。
實現簽名
交易必須被簽名,因為這是比特幣裡面保證發送方不會花費屬於其他人的幣的唯一方式。如果一個簽名是無效的,那麼這筆交易就會被認為是無效的,因此,這筆交易也就無法被加到區塊鏈中。
我們現在離實現交易簽名還差一件事情:用於簽名的資料。一筆交易的哪些部分需要簽名?又或者說,要對完整的交易進行簽名?選擇簽名的資料相當重要。因為用於簽名的這個資料,必須要包含能夠唯一識別資料的資訊。比如,如果僅僅對輸出值進行簽名並沒有什麼意義,因為簽名不會考慮發送方和接收方。
考慮到交易解鎖的是之前的輸出,然後重新分配裡面的價值,並鎖定新的輸出,那麼必須要簽名以下資料:
- 儲存在已解鎖輸出的公開金鑰雜湊。它識別了一筆交易的“發送方”。
- 儲存在新的鎖定輸出裡面的公開金鑰雜湊。它識別了一筆交易的“接收方”。
- 新的輸出值。
在比特幣中,鎖定/解鎖邏輯被儲存在指令碼中,它們被分別儲存在輸入和輸出的 ScriptSig 和 ScriptPubKey 欄位。由於比特幣允許這樣不同類型的指令碼,它對 ScriptPubKey 的整個內容進行了簽名。
可以看到,我們不需要對儲存在輸入裡面的公開金鑰簽名。因此,在比特幣裡, 所簽名的並不是一個交易,而是一個去除部分內容的輸入副本,輸入裡面儲存了被引用輸出的 ScriptPubKey 。
擷取修剪後的交易副本的詳細過程在這裡. 雖然它可能已經過時了,但是我並沒有找到另一個更可靠的來源。
看著有點複雜,來開始寫代碼吧。先從 Sign 方法開始:
func (tx *Transaction) Sign(privKey ecdsa.PrivateKey, prevTXs map[string]Transaction) { if tx.IsCoinbase() { return } txCopy := tx.TrimmedCopy() for inID, vin := range txCopy.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 r, s, err := ecdsa.Sign(rand.Reader, &privKey, txCopy.ID) signature := append(r.Bytes(), s.Bytes()...) tx.Vin[inID].Signature = signature }}
這個方法接受一個私密金鑰和一個之前交易的 map。正如上面提到的,為了對一筆交易進行簽名,我們需要擷取交易輸入所引用的輸出,因為我們需要儲存這些輸出的交易。
來一步一步地分析該方法:
if tx.IsCoinbase() { return}
coinbase 交易因為沒有實際輸入,所以沒有被簽名。
txCopy := tx.TrimmedCopy()
將會被簽署的是修剪後的交易副本,而不是一個完整交易:
func (tx *Transaction) TrimmedCopy() Transaction { var inputs []TXInput var outputs []TXOutput for _, vin := range tx.Vin { 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}
這個副本包含了所有的輸入和輸出,但是 TXInput.Signature 和 TXIput.PubKey 被設定為 nil。
接下來,我們會迭代副本中每一個輸入:
for inID, vin := range txCopy.Vin { prevTx := prevTXs[hex.EncodeToString(vin.Txid)] txCopy.Vin[inID].Signature = nil txCopy.Vin[inID].PubKey = prevTx.Vout[vin.Vout].PubKeyHash
在每個輸入中,Signature 被設定為 nil (僅僅是一個雙重檢驗),PubKey 被設定為所引用輸出的 PubKeyHash。現在,除了當前交易,其他所有交易都是“空的”,也就是說他們的 Signature 和 PubKey 欄位被設定為 nil。因此,輸入是被分開簽名的,儘管這對於我們的應用並不十分緊要,但是比特幣允許交易包含引用了不同地址的輸入。
txCopy.ID = txCopy.Hash() txCopy.Vin[inID].PubKey = nil
Hash 方法對交易進行序列化,並使用 SHA-256 演算法進行雜湊。雜湊後的結果就是我們要簽名的資料。在擷取完雜湊,我們應該重設 PubKey 欄位,以便於它不會影響後面的迭代。
現在,關鍵點:
r, s, err := ecdsa.Sign(rand.Reader, &privKey, txCopy.ID) signature := append(r.Bytes(), s.Bytes()...) tx.Vin[inID].Signature = signature
我們通過 privKey 對 txCopy.ID 進行簽名。一個 ECDSA 簽名就是一對數字,我們對這對數字串連起來,並儲存在輸入的 Signature 欄位。
現在,驗證函式:
func (tx *Transaction) Verify(prevTXs map[string]Transaction) bool { 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 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 := ecdsa.PublicKey{curve, &x, &y} if ecdsa.Verify(&rawPubKey, txCopy.ID, &r, &s) == false { return false } } return true}
這個方法十分直觀。首先,我們需要同一筆交易的副本:
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
這個部分跟 Sign 方法一模一樣,因為在驗證階段,我們需要的是與簽名相同的資料。
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):])
這裡我們解包儲存在 TXInput.Signature 和 TXInput.PubKey 中的值,因為一個簽名就是一對數字,一個公開金鑰就是一對座標。我們之前為了儲存將它們串連在一起,現在我們需要對它們進行解包在 crypto/ecdsa 函數中使用。
rawPubKey := ecdsa.PublicKey{curve, &x, &y} if ecdsa.Verify(&rawPubKey, txCopy.ID, &r, &s) == false { return false }}return true
在這裡:我們使用從輸入提取的公開金鑰建立了一個 ecdsa.PublicKey,通過傳入輸入中提取的簽名執行了 ecdsa.Verify。如果所有的輸入都被驗證,返回 true;如果有任何一個驗證失敗,返回 false.
現在,我們需要一個函數來獲得之前的交易。由於這需要與區塊鏈進行互動,我們將它放在了 Blockchain 的方法裡面:
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("Transaction is not found")}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) prevTXs[hex.EncodeToString(prevTX.ID)] = prevTX } tx.Sign(privKey, prevTXs)}func (bc *Blockchain) VerifyTransaction(tx *Transaction) bool { prevTXs := make(map[string]Transaction) for _, vin := range tx.Vin { prevTX, err := bc.FindTransaction(vin.Txid) prevTXs[hex.EncodeToString(prevTX.ID)] = prevTX } return tx.Verify(prevTXs)}
這幾個比較簡單:FindTransaction 通過 ID 找到一筆交易(這需要在區塊鏈上迭代所有區塊);SignTransaction 傳入一筆交易,找到它引用的交易,然後對它進行簽名;VerifyTransaction 做的是相同的事情,不過是對交易進行驗證。
現在,我們需要實際簽名和驗證交易。簽名在 NewUTXOTransaction 中進行:
func NewUTXOTransaction(from, to string, amount int, bc *Blockchain) *Transaction { ... tx := Transaction{nil, inputs, outputs} 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: Invalid transaction") } } ...}
就是這些了!讓我們再來檢查一下所有內容:
$ blockchain_go createwalletYour new address: 1AmVdDvvQ977oVCpUqz7zAPUEiXKrX5avR$ blockchain_go createwalletYour new address: 1NE86r4Esjf53EL7fR86CsfTZpNN42Sfab$ blockchain_go createblockchain -address 1AmVdDvvQ977oVCpUqz7zAPUEiXKrX5avR000000122348da06c19e5c513710340f4c307d884385da948a205655c6a9d008Done!$ blockchain_go send -from 1AmVdDvvQ977oVCpUqz7zAPUEiXKrX5avR -to 1NE86r4Esjf53EL7fR86CsfTZpNN42Sfab -amount 60000000f3dbb0ab6d56c4e4b9f7479afe8d5a5dad4d2a8823345a1a16cf3347bSuccess!$ blockchain_go getbalance -address 1AmVdDvvQ977oVCpUqz7zAPUEiXKrX5avRBalance of '1AmVdDvvQ977oVCpUqz7zAPUEiXKrX5avR': 4$ blockchain_go getbalance -address 1NE86r4Esjf53EL7fR86CsfTZpNN42SfabBalance of '1NE86r4Esjf53EL7fR86CsfTZpNN42Sfab': 6
一切正常!
現在來注釋掉 NewUTXOTransaction 裡面的bc.SignTransaction(&tx, wallet.PrivateKey) 的調用,因為未簽名的交易無法被打包:
func NewUTXOTransaction(from, to string, amount int, bc *Blockchain) *Transaction { ... tx := Transaction{nil, inputs, outputs} tx.ID = tx.Hash() // bc.SignTransaction(&tx, wallet.PrivateKey) return &tx}
$ go install$ blockchain_go send -from 1AmVdDvvQ977oVCpUqz7zAPUEiXKrX5avR -to 1NE86r4Esjf53EL7fR86CsfTZpNN42Sfab -amount 12017/09/12 16:28:15 ERROR: Invalid transaction
總結
到目前為止,我們已經完成了比特幣中的許多關鍵特性!除了網路外的所有事情都已基本完成,在下一篇文章中,我們將會完成交易部分。
連結:
- Full source codes
- Public-key cryptography
- Digital signatures
- Elliptic curve
- Elliptic curve cryptography
- ECDSA
- Technical background of Bitcoin addresses
- Address
- Base58
- A gentle introduction to elliptic curve cryptography
原文:Building Blockchain in Go. Part 5: Addresses