2.6 地址

尹成發表於2018-11-08

比特幣地址

這裡有一份比特幣地址的例子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)演算法來簽名交易。

Base58

現在我們把注意力回到比特幣地址上來: 
前面說的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)的結構:

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”演算法基於橢圓曲線,是我們需要的。下一步,使用橢圓演算法生成私鑰,然後用私鑰生成公鑰。注意一點,在橢圓曲線演算法中,公鑰是在橢圓上的點集合。因此,公鑰是直角座標系的座標集合,比特幣中,這些座標串起來構成公鑰。 
現在生成地址:

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規範的地址步驟: 
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結構,讓其能使用地址。

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。我們會實現與比特幣中相同的output鎖/解鎖和input簽名邏輯,但是我們通過使用方法(method)來實現。 
UsesKey方法負責檢測input使用了特別的金鑰來解鎖output。注意input裡存放的是原生的未進行hash處理過的公鑰,但是這個函式得到的是經過hash處理過的公鑰。 
IsLockedWithKey負責檢測提供的公鑰hash是否能用於給output加鎖。它是UsesKey函式的補充,它們都用在FindUnspentTransactions中,用於建立交易之間的連線。 
Lock就是簡單地把output鎖上。當把幣傳送經其它人時,我們是知道他們的地址的,因此這個函式會要求傳入這個地址,然後會被解碼,把公鑰的雜湊抽出來再儲存到PubKeyHash欄位中。 
現在看看是能工作:

$ blockchain_go createwallet

Your new address: 13Uu7B1vDP4ViXqHFsWtbraM3EfQ3UkWXt

$ blockchain_go createwallet

Your new address: 15pUhCbtrGh3JUx5iHnXjfpyHyTgawvG5h

$ blockchain_go createwallet

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!

$ blockchain_go getbalance -address 13Uu7B1vDP4ViXqHFsWtbraM3EfQ3UkWXt

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開始編寫:

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。前面提到,為了簽名交易,需要訪問交易中的input引用的ouput,因此需要存放了這些output的交易。

驗證函式:

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() 
給每一個input簽名:

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中的值抽出來,因為簽名是一個數字對,公鑰是X,Y座標。在此前為了儲存而把它們給組合起來,現在需要拆開得到值來計算crypto/ecdsa計算。

rawPubKey := ecdsa.PublicKey{curve, &x, &y}
        if ecdsa.Verify(&rawPubKey, txCopy.ID, &r, &s) == false {
                return false
        }
}
return true

我們使用從input中抽出來的公鑰建立了一個ecdsa.PublicKey公鑰,把input中抽出來的簽名用ecdsa.Verify驗證。如果所有input都驗證通過了,就返回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和SignTransaction差不多,只是它不是負責簽名,而是驗證簽名。 
現在需要簽名與驗證簽名。簽名過程在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")
                }
        }
        ...
}

OK,再執行一下程式看是否正常: 
blockchaingocreatewalletYournewaddress:1AmVdDvvQ977oVCpUqz7zAPUEiXKrX5avRblockchaingocreatewalletYournewaddress:1AmVdDvvQ977oVCpUqz7zAPUEiXKrX5avR blockchain_go createwallet 
Your new address: 1NE86r4Esjf53EL7fR86CsfTZpNN42Sfab 
blockchaingocreateblockchainaddress1AmVdDvvQ977oVCpUqz7zAPUEiXKrX5avR000000122348da06c19e5c513710340f4c307d884385da948a205655c6a9d008Done!blockchaingocreateblockchain−address1AmVdDvvQ977oVCpUqz7zAPUEiXKrX5avR000000122348da06c19e5c513710340f4c307d884385da948a205655c6a9d008Done!blockchain_go send -from 1AmVdDvvQ977oVCpUqz7zAPUEiXKrX5avR -to 1NE86r4Esjf53EL7fR86CsfTZpNN42Sfab -amount 6 
0000000f3dbb0ab6d56c4e4b9f7479afe8d5a5dad4d2a8823345a1a16cf3347b 
Success! 
blockchaingogetbalanceaddress1AmVdDvvQ977oVCpUqz7zAPUEiXKrX5avRBalanceof1AmVdDvvQ977oVCpUqz7zAPUEiXKrX5avR: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 
總結 
我們從前面幾章開始講了這麼久來實現比特幣中的各種關鍵特性。我們實現了大多數,除了網路連通,下一章,我們把交易弄完。

 

相關文章