這裡有一份比特币位址的例子1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa。這是第一個比特币的位址,傳言就是中本聰本人的比特币。比特币位址是公開的。如果要發送比特币給其他人,就得知道他的位址。但是位址(盡管都是唯一的)不是區分你是錢包的擁有者的東西。事實上,這些位址都是人眼可讀的公鑰文本。在比特币中,你的辨別是一組(存放在你電腦或者其它你指定的地方)上的私鑰和公鑰對。比特币依賴加密算法的組合建立的密鑰,用以保證世界上沒有任何一個人可以繞開你實體機上的實體私鑰就可以操控你的比特币。現在探讨一下這個算法機制。
公鑰加密算法使用密鑰對–公鑰和私鑰。公鑰不敏感,可以公開給任何人。與此相反,私鑰則不應該暴露出來,除了私鑰持有者其它人都不能通路私鑰,因為這是持有者身份辨別。可以這麼說,在加密了的世界裡,你就是你的私鑰。
比特币的錢包本質上就是上面那些密鑰對。當你安裝錢包應用或者比特币用戶端生成新的位址時,一對密鑰就生成了。誰控制了私鑰就控制了所有的發送到這個密鑰(公鑰位址文本)的所有币。
私鑰與公鑰是随機的byte序列,是以列印出來也不是人可讀的。是以比特币使用了另一個算法來把公鑰轉成字元串,讓人類可讀。
If you’ve ever used a Bitcoin wallet application, it’s likely that a mnemonic pass phrase was generated for you. Such phrases are used instead of private keys and can be used to generate them. This mechanism is implemented in BIP-039.
如果你有使用過比特币錢包應用,那麼就好比給你生成了一個助記密文短語。這些短語可以用到代替私鑰,也可以生成私鑰?。這一機制基于BIP-039實作。
現在知道比特币中是什麼區分人員的。但是比特币中是怎麼檢測交易中output(及存在output中的币)所有權的?
數學和密碼學裡,有一個概念叫數字加密。該算法保證:
1. 資料在從發送者傳輸到接收者過程中不會被更改
2. 資料由确定的發送者建立
3. 發送者不能否認發送過這筆資料
通過一個給資料的簽名算法,獲得簽名,過後可以用來驗證。數字簽名和私鑰一起使用,然後用公鑰來驗證。(公鑰好比鎖,大家可以都有,私鑰就是鑰匙,隻有這個鎖能證明是這個私鑰能打開它,同樣反過來,隻有這個私鑰能打開這個鎖證明它是資料擁有者)。
要簽名資料得有兩個東西:
1. 需要簽名的資料
2. 簽名的私鑰
簽名操作産生簽名,這個簽名就存放在交易的input中。為了驗證簽名,還需要:
1. 剛被簽名的資料
2. 簽名
3. 公鑰
簡單來說,驗證過程可以這麼描述:檢測簽名是從這筆資料與私鑰一起計算得來的,而這個公鑰也是由該私鑰生成的。
Digital signatures are not encryption, you cannot reconstruct the data from a signature. This is similar to hashing: you run data through a hashing algorithm and get a unique representation of the data. The difference between signatures and hashes is key pairs: they make signature verification possible. But key pairs can also be used to encrypt data: a private key is used to encrypt, and a public key is used to decrypt the data. Bitcoin doesn’t use encryption algorithms though.
數字簽名不是加密,你不能在簽名中重構出資料。這和hash有點像,你通過hash算法計算資料然後傳回一個唯一的資料描述。差別簽名和hash的不同是密鑰對,密鑰對使得驗證簽名成為可能。但是密鑰對也可用于加密資料,私鑰用于加密,公鑰則用于解密資料。不過,比特币沒有用加密算法。
在比特币中,每一筆交易的input都是由建立了這筆交易的人簽名。交易在被塞到區塊前必須通過驗證。驗證意味着(除去了一些步驟):
1. 檢測input擁有權限使用前一交易中其關聯的output
2. 檢測交易簽名是正确的
如圖,簽名和驗證的過程:
現在我們重新過一下整個交易的生命周期
1. 首先,有包含coinbase交易的創世區塊,此時并沒有真正的input在coinbase交易裡,是以簽名在這一步是不需要的。而coinbase交易的output含有一個使用(RIPEMD16(SHA256(PubKey))算法的hash公鑰。
2. 當有人發送币時,會建立一筆交易。交易的input會關聯前面交易(可能會關聯多個交易)中的output。每回input都會存儲一個公鑰(沒有經過hash處理)和一個用整個交易算出的簽名。
3. 比特币網絡中其它的節點會收到這個交易然後驗證它。它們會檢測:input裡公鑰的hash值是否比對引用的output的hash值(這一步用于确認發送者隻發送了歸屬他的币);簽名是否正确(證明交易是由币的持有者發起的)。
4. 當礦工節點準備去挖新的區塊時,它會把交易塞到區塊中,然後開始挖礦。
5. 當區塊被挖出來時,網絡中每一個其它的節點都會收到該區塊被挖出來并被加到區塊鍊中的消息
6. 在區塊加入區塊鍊中後,交易就完成了,它的output就可以被新的交易引用(消費)。
前面說到公鑰和私鑰是兩個随機的byte數組序列。因為私鑰用于區分持币者,是以就需要滿足條件:随機算法必須産生真正的随機bytes。不能讓生成其他人已經撐有的私鑰。
比特币使用橢圓曲線來生成私鑰。橢圓曲線是一個複雜的數學概念,這裡就不詳細解說了,有興趣可以檢視這篇文章(警告:很多數學公式)。我們隻需要記住這個算法可以生成足夠大和随機的數字。比特币中用的橢圓能随機挑出一個介于0到2²⁵⁶(近似于10⁷⁷,要知道,宇宙中有10⁷⁸到10⁸²個原子)。這麼大的數字意味着幾乎任意兩次計算都是不可能産生相同的數字的。
另外,比特币使用(我們也将用)ECDS(Elliptic Curve Digital Signature Algorithm)算法來簽名交易。
現在我們把注意力回到比特币位址上來:
前面說的1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa這個位址。這是人類可讀的公鑰表現形式,如果我們把它給解碼了,公鑰看上去就像這樣(轉成了16進制系統中的byte串):
0062E907B15CBF27D5425399EBF6F0FB50EBB88F18C29B7D93
比特币使用基于Base58的算法來把公鑰轉成人眼可讀的格式,這個算法與Base64很像,但用于更短的字母表,有些字母被移除以避免某些利用字元相似的攻擊。是以,這些符号是沒有的:0(數字0)、O(大寫字母o)、I(大寫字母i)、l(小寫的L),因為他們實在太像了。當然,也沒有+-(加減)符号。
下圖展示從公鑰算出位址的過程:
是以看到上述的公鑰解碼後由3個部分組成:
Version Public key hash Checksum
00 62E907B15CBF27D5425399EBF6F0FB50EBB88F18 C29B7D93
因為使用哈希函數是單向的(即不能被逆轉),也不能從hash值裡找出公鑰。通過執行同樣的hash函數然後再比較這個解碼後的hash值是否一緻來驗證該公鑰用于生成該hash,如果一緻,則公鑰用于計算該hash。
重要的都說完了,開始撸代碼吧。有些概念在寫代碼前要弄清楚。
先定義錢包(wallet)的結構:
錢包除了密鑰對,其它什麼也沒有。現在需要Wallets類型來維護錢包集合,把它們的資料落地。Wallet的構造函數,有新的密鑰對生成。newKeyPair函數比較簡單,“ECDSA”算法基于橢圓曲線,是我們需要的。下一步,使用橢圓算法生成私鑰,然後用私鑰生成公鑰。注意一點,在橢圓曲線算法中,公鑰是在橢圓上的點集合。是以,公鑰是直角坐标系的坐标集合,比特币中,這些坐标串起來構成公鑰。
現在生成位址:
下面是把公鑰轉成Base58規範的位址步驟:
1. 擷取公鑰,使用RIPEMD160(SHA256(PubKey))執行兩次hash算法。
2. 給hash加上位址生成算法版本
3. 使用SHA256(SHA256(payload))hash計算第2步的結果,得到的hash值前4bytes就是校驗碼。
4. 把校驗碼附加到version+PubKeyHash組合。
5. 使用Base58編碼version+PubKeyHash+checksum組合
結果,你就算出了一個真正的比特币位址,你甚至可以在[blockchain.info][https://blockchain.info/]上找到它的餘額。但是剛保證餘額肯定是0,不論你生成多少次新的位址,再檢測也是0。這也就是為什麼選擇合适的公鑰加密算法非常重要:考慮到私鑰是随機的數字,生成相同的數字幾乎是不可能的,事實上,這個可能性會低到“永不發生”。
還有,要注意你不需要連接配接到任何的比特币節點去擷取它的位址。位址生成算法使用的是開源的算法組合,這些算法在很多程式設計語言和庫中都有實作。
現在修改Input和Output結構,讓其能使用位址。
注意,我們沒有再使用ScriptPubKey和ScriptSig,因為我們并不準備去實作一個腳本語言。相反,ScriptSig分成了Signature和PubKey。ScriptPubKey也改名成PubKeyHash。我們會實作與比特币中相同的output鎖/解鎖和input簽名邏輯,但是我們通過使用方法(method)來實作。
UsesKey方法負責檢測input使用了特别的密鑰來解鎖output。注意input裡存放的是原生的未進行hash處理過的公鑰,但是這個函數得到的是經過hash處理過的公鑰。
IsLockedWithKey負責檢測提供的公鑰hash是否能用于給output加鎖。它是UsesKey函數的補充,它們都用在FindUnspentTransactions中,用于建立交易之間的連接配接。
Lock就是簡單地把output鎖上。當把币發送經其它人時,我們是知道他們的位址的,是以這個函數會要求傳入這個位址,然後會被解碼,把公鑰的哈希抽出來再儲存到PubKeyHash字段中。
現在看看是能工作:
$ blockchain_go createwallet
Your new address: 13Uu7B1vDP4ViXqHFsWtbraM3EfQ3UkWXt
Your new address: 15pUhCbtrGh3JUx5iHnXjfpyHyTgawvG5h
Your new address: 1Lhqun1E9zZZhodiTqxfPQBcwr1CVDV2sy
$ blockchain_go createblockchain -address 13Uu7B1vDP4ViXqHFsWtbraM3EfQ3UkWXt
0000005420fbfdafa00c093f56e033903ba43599fa7cd9df40458e373eee724d
Done!
$ blockchain_go getbalance -address 13Uu7B1vDP4ViXqHFsWtbraM3EfQ3UkWXt
Balance of ‘13Uu7B1vDP4ViXqHFsWtbraM3EfQ3UkWXt’: 10
$ blockchain_go send -from 15pUhCbtrGh3JUx5iHnXjfpyHyTgawvG5h -to
13Uu7B1vDP4ViXqHFsWtbraM3EfQ3UkWXt -amount 5
2017/09/12 13:08:56 ERROR: Not enough funds
$ blockchain_go send -from 13Uu7B1vDP4ViXqHFsWtbraM3EfQ3UkWXt -to
15pUhCbtrGh3JUx5iHnXjfpyHyTgawvG5h -amount 6
00000019afa909094193f64ca06e9039849709f5948fbac56cae7b1b8f0ff162
Success!
Balance of ‘13Uu7B1vDP4ViXqHFsWtbraM3EfQ3UkWXt’: 4
$ blockchain_go getbalance -address 15pUhCbtrGh3JUx5iHnXjfpyHyTgawvG5h
Balance of ‘15pUhCbtrGh3JUx5iHnXjfpyHyTgawvG5h’: 6
$ blockchain_go getbalance -address 1Lhqun1E9zZZhodiTqxfPQBcwr1CVDV2sy
Balance of ‘1Lhqun1E9zZZhodiTqxfPQBcwr1CVDV2sy’: 0
不錯,現在實作交易簽名。
交易必須被簽名,這是比特币中,唯一能保證沒有人可以消費别人的币的機制。如果簽名不合法,交易也是不合法的。是以,交易也不會被加到區塊鍊中。
除了資料簽名,交易中有關簽名的所有點都實作了。交易中有哪幾部分是真正要簽名的?或者整個交易都要被簽名?選擇需要加密的資料是非常重要的。問題是被簽名的資料有獨特的方式差別資料資訊。舉個栗子,僅簽名output的值一點也沒用,因為這樣簽名并沒有考慮發送者和接收者。
考慮到交易解鎖前面交易的output,重新配置設定它們的值,然後鎖到新的output中,下列的資料必須是加密的:
1. 儲存在解鎖了的output公鑰的hash值。這可以辨識交易的發送者。
2. 儲存在新的、加鎖了的output公鑰的hash值。這可以辨識交易的接收者。
3. 新output的值。
In Bitcoin, locking/unlocking logic is stored in scripts, which are stored in ScriptSig and ScriptPubKey fields of inputs and outputs, respectively. Since Bitcoins allows different types of such scripts, it signs the whole content of ScriptPubKey.
在比特币中,加鎖/解鎖邏輯是存儲在腳本中的,分别存儲在input和output的ScriptSig、ScriptPubKey字段中。因為比特币允許不同的類型腳本,是以會對整個ScriptPubKey的内容進行簽名。
可以看到,我們并不需要去簽名input中的公鑰,是以,比特币裡并不是對整個交易簽名的,但是對input從output引用的ScriptPubKey進行了适度修剪。
A detailed process of getting a trimmed transaction copy is described here. It’s likely to be outdated, but I didn’t manage to find a more reliable source of information.
更詳細的擷取裁剪過的交易備份描述,可能比較老了,但是我找不到更可靠的資源了
看上去比較複雜,先從Sign開始編寫:
方法接收私鑰和前面交易的map。前面提到,為了簽名交易,需要通路交易中的input引用的ouput,是以需要存放了這些output的交易。
驗證函數:
這個方法比較簡單,首先我們拷貝一份交易的副本:
txCopy := tx.TrimmedCopy()
然後我們需要相同的橢圓來生成密鑰對:
curve := elliptic.P256()
給每一個input簽名:
這塊代碼與Sign中的方法一樣,因為在驗證過程中,我們需要的資料,得與被簽名的相同。
這一步我們把TXInput.Signature和TXInput.PubKey中的值抽出來,因為簽名是一個數字對,公鑰是X,Y坐标。在此前為了存儲而把它們給組合起來,現在需要拆開得到值來計算crypto/ecdsa計算。
我們使用從input中抽出來的公鑰建立了一個ecdsa.PublicKey公鑰,把input中抽出來的簽名用ecdsa.Verify驗證。如果所有input都驗證通過了,就傳回true。如果有一個失敗,都傳回false。
現在我們需要一個函數可以擷取此前的交易。因為這一操作需要與整個區塊鍊互動,是以得在Blockchain區塊鍊上加一個方法:
這些函數都比較簡單:FindTransaction用于通過ID找到交易(這需要周遊整個區塊鍊中的區塊);SignTransaction則負責給傳進來的交易找到其引用的其它交易,并給它簽名。VerifyTransaction和SignTransaction差不多,隻是它不是負責簽名,而是驗證簽名。
現在需要簽名與驗證簽名。簽名過程在NewUTXOTransaction函數中執行。
在交易被塞到區塊之前,需要驗證它:
OK,再運作一下程式看是否正常:
blockchaingocreatewalletYournewaddress:1AmVdDvvQ977oVCpUqz7zAPUEiXKrX5avRblockchaingocreatewalletYournewaddress:1AmVdDvvQ977oVCpUqz7zAPUEiXKrX5avR blockchain_go createwallet
Your new address: 1NE86r4Esjf53EL7fR86CsfTZpNN42Sfab
blockchaingocreateblockchain−address1AmVdDvvQ977oVCpUqz7zAPUEiXKrX5avR000000122348da06c19e5c513710340f4c307d884385da948a205655c6a9d008Done!blockchaingocreateblockchain−address1AmVdDvvQ977oVCpUqz7zAPUEiXKrX5avR000000122348da06c19e5c513710340f4c307d884385da948a205655c6a9d008Done!blockchain_go send -from 1AmVdDvvQ977oVCpUqz7zAPUEiXKrX5avR -to 1NE86r4Esjf53EL7fR86CsfTZpNN42Sfab -amount 6
0000000f3dbb0ab6d56c4e4b9f7479afe8d5a5dad4d2a8823345a1a16cf3347b
Success!
blockchaingogetbalance−address1AmVdDvvQ977oVCpUqz7zAPUEiXKrX5avRBalanceof‘1AmVdDvvQ977oVCpUqz7zAPUEiXKrX5avR′:4blockchaingogetbalance−address1AmVdDvvQ977oVCpUqz7zAPUEiXKrX5avRBalanceof‘1AmVdDvvQ977oVCpUqz7zAPUEiXKrX5avR′:4 blockchain_go getbalance -address 1NE86r4Esjf53EL7fR86CsfTZpNN42Sfab
Balance 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
}
goinstallgoinstall blockchain_go send -from 1AmVdDvvQ977oVCpUqz7zAPUEiXKrX5avR -to 1NE86r4Esjf53EL7fR86CsfTZpNN42Sfab -amount 1
2017/09/12 16:28:15 ERROR: Invalid transaction
總結
我們從前面幾章開始講了這麼久來實作比特币中的各種關鍵特性。我們實作了大多數,除了網絡連通,下一章,我們把交易弄完。