在比特幣中藕施,沒有用戶賬戶舀锨,不需要也不會在任何地方存儲個人數(shù)據(jù)(比如姓名呼盆,護照號碼或者 SSN)访圃。但是相嵌,我們總要有某種途徑識別出你是交易輸出的所有者(即比特幣的擁有者)腿时。這就是一個真實的比特幣地址:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa克胳。這是史上第一個比特幣地址,據(jù)說屬于中本聰圈匆。比特幣地址是完全公開的漠另,如果你想要給某個人發(fā)送幣,只需要知道他的地址就可以了跃赚。
錢包的實現(xiàn)
錢包存儲的是公鑰和私鑰,定義錢包非常簡單,使用如下結(jié)構(gòu)體就能定義一個錢包了笆搓。
難的是實現(xiàn)錢包的算法,好在大部分語言都已經(jīng)封裝好了....
私鑰
私鑰可以是1和n-1之間的任何數(shù)字纬傲, 其中n是?個常數(shù)(n=1.158*1077, 略?于2256) 叹括, 并由?特幣所使?的橢圓曲線的階所定義要?成這樣的?個私鑰算墨, 我們隨機選擇?個256位的數(shù)字, 并檢查它是否?于n-1汁雷。
說白了就是一個隨機選出來的數(shù)字净嘀。但是私鑰?于?成?付?特幣所必需的簽名以證明資?的所有權(quán)。
公鑰
通過橢圓曲線算法可以從私鑰計算得到公鑰侠讯, 這是不可逆轉(zhuǎn)的過程: K = k * G 挖藏。 其中 k 是私鑰, G 是被稱為?成點的常數(shù)點厢漩, ? K 是所得公鑰膜眠。 其反向運算, 被稱為“尋找離散對數(shù)”——已知公鑰 K 來求出私鑰 k ——是?常困難的溜嗜, 就像去試驗所有可能的 k 值宵膨, 即暴?搜索。
還有就是炸宵,公鑰即身份辟躏!
實現(xiàn)公私鑰對
實現(xiàn)公私鑰對非常直觀:ECDSA 基于橢圓曲線焙压,所以我們需要一個橢圓曲線鸿脓。接下來抑钟,使用橢圓生成一個私鑰涯曲,然后再從私鑰生成一個公鑰。有一點需要注意:在基于橢圓曲線的算法中在塔,公鑰是曲線上的點幻件。因此,公鑰是 X蛔溃,Y 坐標的組合绰沥。在比特幣中篱蝇,這些坐標會被連接起來,然后形成一個公鑰徽曲。具體可以去網(wǎng)上參考橢圓曲線加密算法零截。
定義錢包
錢包中只需要保存用戶的公私鑰對wallet.go。
type Wallet struct {
//私鑰
PrivateKey ecdsa.PrivateKey
//公鑰(該屬性可以從私鑰推導)
PublicKey []byte
}
//構(gòu)造錢包
func NewWallet() *Wallet{
private, public := newPrivAndPub()
return &Wallet{private,public}
}
func newPrivAndPub()(ecdsa.PrivateKey,[]byte){
//聲明p-256曲線
p256curve := elliptic.P256()
//使用p256曲線生成私鑰,
//GenerateKey函數(shù)生成一對公鑰/私鑰秃臣。
//Reader是一個全局涧衙、共享的密碼用強隨機數(shù)生成器奥此。在Unix類型系統(tǒng)中,會從/dev/urandom讀戎苫ⅰ;而Windows中會調(diào)用CryptGenRandom API蠢终。
privKey, err := ecdsa.GenerateKey(p256curve, rand.Reader)
//go語法中判斷是否發(fā)生錯誤,如果錯誤寻拂,打印錯誤
if err != nil {
log.Panic(err)
}
//獲取最原始公鑰,即連接X兜喻,Y坐標得到的結(jié)果就是最原始公鑰
pubKey := append(privKey.PublicKey.X.Bytes(),privKey.PublicKey.Y.Bytes()...)
//返回結(jié)果
return *privKey,pubKey
}
實現(xiàn)比特地址的步驟和圖解
先確定版本號跟地址驗證
使用 RIPEMD160(SHA256(PubKey)) 哈希算法,取公鑰并對其哈希兩次
給哈希加上地址生成算法版本的前綴
對于第二步生成的結(jié)果朴皆,使用 SHA256(SHA256(payload)) 再哈希帕识,計算校驗和。校驗和是結(jié)果哈希的前四個字節(jié)遂铡。
將校驗和附加到 version+PubKeyHash 的組合中肮疗。
使用 Base58 對 version+PubKeyHash+checksum 組合進行編碼。
先聲明版本號version(得是16進制)扒接,和驗證addressCheckSum長度伪货,
const addressCheckSum = 4
var version = byte(0x00)
這里需要用到ripemd160()和Base58Encode()算法:
cd $GOPATH/src/golang.org/x
//如果沒有 golang.org/x 則創(chuàng)建相關(guān)目錄
git clone https://github.com/golang/crypto.git
Base58Encode()可能要自己上網(wǎng)找了,可以看我用的钾怔,在Github里
實現(xiàn)代碼如下:
//獲取地址
func (wallet *Wallet) GetAddress() []byte{
//對公鑰進行Ripemd160
pubkey160 := HashPubkey(wallet.PublicKey)
//組合version + pubkey160
payload := append([]byte{version},pubkey160...)
//生成校驗碼
checkSum := checksum(payload)
//組合version + pubkey160 + checksum
fullPayload := append(payload, checkSum...)
//進行base58編碼
return Base58Encode(fullPayload)
}
//對公鑰進行Ripemd160,
func HashPubkey(pubkey []byte) []byte{
pubkey256 := sha256.Sum256(pubkey)
//聲明ripemd160方法
ripemd160Hasher := ripemd160.New()
//傳進參數(shù)碱呼,注意格式的轉(zhuǎn)變,sha256.Sum256()返回一個[Size]byte格式宗侦,所以需要轉(zhuǎn)為切片
_, err := ripemd160Hasher.Write(pubkey256[:])
if err != nil {
log.Panic(err)
}
pubkey160 := ripemd160Hasher.Sum(nil)
return pubkey160
}
//生成校驗碼愚臀,把version + pubkey160的結(jié)果進行兩次sha256后取其前4個字節(jié)
func checksum(payload []byte) []byte {
first256 := sha256.Sum256(payload)
sec256 := sha256.Sum256(first256[:])
return sec256[:addressCheckSum]
}
驗證地址是否正確:
思路是驗證校驗碼就行了。
func ValidateAddress(address []byte) bool {
//解碼地址
pubKeyHash := Base58Decode(address)
//獲取校驗碼
srcCheckSum := pubKeyHash[len(pubKeyHash) - addressCheckSum:]
//獲取版本號
version := pubKeyHash[0]
//獲取數(shù)據(jù)
pubKey160 := pubKeyHash[1:len(pubKeyHash) - addressCheckSum]
//重新組合校驗碼
checkSum := checksum(append([]byte{version},pubKey160...))
//驗證校驗碼的
return bytes.Compare(srcCheckSum,checkSum) == 0
}
使用文件保存錢包的集合
上面我們已經(jīng)實現(xiàn)了錢包,但是我們無法保存每個錢包的地址,本小節(jié)我們就是要使用文件保存錢包的集合.
定義錢包集合的結(jié)構(gòu)體,在構(gòu)造實例的時候通過讀取文件獲取所有的錢包地址wallets.go:
//定義存儲的文件
const WalletsFile = "wallets.dat"
//錢包集合的結(jié)構(gòu)體
type Wallets struct {
//key是錢包地址矾利,value是錢包
Wallets map[string]*Wallet
}
//構(gòu)造錢包集合
//因為之后我們需要通過獲取來地址進行錢包地址的遍歷和根據(jù)地址獲得指定的錢包實例
//所以創(chuàng)建錢包過程需要單獨列出來
func NewWallets()(*Wallets, error) {
//因為只有一個錢包姑裂,隨意在這里使用指針類型傳遞
wallets := &Wallets{}
wallets.Wallets = make(map[string]*Wallet)
err := wallets.LoadFromFile()
return wallets,err
}
關(guān)于錢包地址的讀取和保存,主要實現(xiàn)對錢包集合的序列化數(shù)據(jù)保存到文件,把反序列化數(shù)據(jù)設(shè)置到Wallets結(jié)構(gòu)體的map字典中
//從文件中獲取錢包集合
func (ws *Wallets) LoadFromFile() error {
if _,err:=os.Stat(WalletsFile);os.IsNotExist(err){
return err;
}
//讀取文件中的序列化字節(jié)數(shù)據(jù)
contents , err := ioutil.ReadFile(WalletsFile)
if err != nil {
log.Panic(err)
}
//反序列
var wallets Wallets
gob.Register(elliptic.P256())
decoder := gob.NewDecoder(bytes.NewReader(contents))
err = decoder.Decode(&wallets)
if err != nil {
log.Panic(err)
}
//構(gòu)造錢包集合
ws.Wallets = wallets.Wallets
return nil
}
//保存錢包集合的序列化數(shù)據(jù)
func (ws Wallets) SaveToFile(){
var buffer bytes.Buffer
//這代碼只是一個標示馋袜,沒什么意思,只是一個良好的編程習慣,不寫也行
gob.Register(elliptic.P256())
//序列化
encoder := gob.NewEncoder( &buffer )
err := encoder.Encode(ws)
if err != nil {
log.Panic(err)
}
//把序列化數(shù)據(jù)寫入文件
err = ioutil.WriteFile(WalletsFile,buffer.Bytes(),0666)
if err != nil {
log.Panic(err)
}
}
而我們并不會實用NewWallet去新建的一個錢包,而是通過在集合中的CreateWallet方法進行錢包的新建,新建的錢包就會加入到集合中
//單獨把創(chuàng)建錢包列出來
//創(chuàng)建一個錢包,并把錢包加入到集合中
func (wallets *Wallets)CreateWallet() string {
//聲明錢包集合
wallet := NewWallet()
//獲取錢包地址
address := fmt.Sprintf("%s",wallet.GetAddress())
//把錢包加入集合
wallets.Wallets[address] = wallet
return address
}
有了錢包集合我們就可以通過GetAddresses進行錢包地址的遍歷和根據(jù)地址獲得指定的錢包實例
//根據(jù)地址獲取一個錢包實例
func (wallets Wallets) GetWallet(address string) Wallet{
//這要非常注意舶斧,因為Wallets中的map返回的是*Wallet
return *wallets.Wallets[address]
}
//獲取所有都錢包地址
func (wallets *Wallets) GetAddresses() []string{
var addresses []string
for address := range wallets.Wallets{
addresses = append(addresses,address)
}
return addresses
}
定義命令行工具測試
為了便于使用,我們通過客戶端命令行供用戶去進行使用,本小節(jié)我們添加的功能只有創(chuàng)建錢包和遍歷錢包集合的命令行功能欣鳖。createwallet命令實現(xiàn)錢包的創(chuàng)建,listaddresses命令實現(xiàn)錢包集合的遍歷,后面我不斷完善功能我可以添加更多不同的命令,這里我們定義cli.go作為客戶端命令行的主體部分,Run方法可以運行起整個客戶端命令行,之后我們添加不同命令對應(yīng)在Run中進行注冊即可完成。
cli.go代碼創(chuàng)建如下:
/*
@Time : 2019/9/6 下午3:15
@Author : yeyangfengqi
@File : cli
@Software: GoLand
@effect: 命令行工具
*/
package main
import (
"flag"
"fmt"
"log"
"os"
)
//定義命令行工具
type CLI struct {}
//命令提示幫助方式
func (cli *CLI)printUsage() {
fmt.Println("Usage:")
fmt.Println(" createwallet - 創(chuàng)建一個新的錢包地址")
fmt.Println(" listaddresses - 遍歷輸出所有的錢包地址")
}
//當用戶直接輸入 ./bitCoin但沒有參數(shù)時提示幫助信息
func (cli *CLI) validateArgs(){
if len(os.Args) < 2 {
cli.printUsage()
os.Exit(1)
}
}
//運行客戶端命令行
func (cli *CLI) Run () {
//沒有輸入時運行的函數(shù)
cli.validateArgs()
//注冊createwallet命令
createWalletCmd := flag.NewFlagSet("createwallet", flag.ExitOnError)
//注冊listaddresses命令
listAddressesCmd := flag.NewFlagSet("listaddresses", flag.ExitOnError)
//判斷輸入的函數(shù)
switch os.Args[1] {
case "createwallet":
err := createWalletCmd.Parse(os.Args[2:])
if err != nil {
log.Panic(err)
}
case "listaddresses":
err := listAddressesCmd.Parse(os.Args[2:])
if err != nil {
log.Panic(err)
}
}
//如果用戶輸入的是createwallet就執(zhí)行對應(yīng)方法
if createWalletCmd.Parsed() {
cli.createWallet()
}
//如果用戶輸入的是listaddresses就執(zhí)行對應(yīng)方法
if listAddressesCmd.Parsed() {
cli.listAddresses()
}
}
我們注冊的創(chuàng)建錢包的方法放在cli_createwallet.go中
/*
@Time : 2019/9/6 下午3:16
@Author : yeyangfengqi
@File : cli_createwallet
@Software: GoLand
@effect: 命令行工具——創(chuàng)建錢包
*/
package main
import "fmt"
func (cli *CLI)createWallet() {
//創(chuàng)建集合
wallets, _ := NewWallets()
//創(chuàng)建錢包并把錢包寫入map[string]*Wallet字典中
address := wallets.CreateWallet()
//保持集合序列化數(shù)據(jù)到文件中
wallets.SaveToFile()
fmt.Printf("你創(chuàng)建的新錢包地址是: %s\n", address)
}
我們注冊的遍歷錢包集合的方法放在cli_listaddresses.go中
/*
@Time : 2019/9/6 下午3:19
@Author : yeyangfengqi
@File : cli_listaddresses
@Software: GoLand
@effect: 命令行工具——查看所有錢包集合中地址
*/
package main
import (
"fmt"
"log"
)
func (cli *CLI) listAddresses () {
wallets, err := NewWallets()
if err != nil {
log.Panic(err)
}
//獲取所有的錢包地址
addresses := wallets.GetAddresses()
//遍歷輸出
for _, address := range addresses {
fmt.Println(address)
}
}
目錄結(jié)構(gòu)及代碼測試如下: