區(qū)塊鏈學習筆記——錢包

在比特幣中藕施,沒有用戶賬戶舀锨,不需要也不會在任何地方存儲個人數(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)比特地址的步驟和圖解

最原始公鑰推導地址過程
  1. 先確定版本號跟地址驗證

  2. 使用 RIPEMD160(SHA256(PubKey)) 哈希算法,取公鑰并對其哈希兩次

  3. 給哈希加上地址生成算法版本的前綴

  4. 對于第二步生成的結(jié)果朴皆,使用 SHA256(SHA256(payload)) 再哈希帕识,計算校驗和。校驗和是結(jié)果哈希的前四個字節(jié)遂铡。

  5. 將校驗和附加到 version+PubKeyHash 的組合中肮疗。

  6. 使用 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)及代碼測試如下:


代碼測試

代碼地址

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末茴厉,一起剝皮案震驚了整個濱河市观堂,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌呀忧,老刑警劉巖师痕,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異而账,居然都是意外死亡胰坟,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進店門泞辐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來笔横,“玉大人,你說我怎么就攤上這事咐吼。” “怎么了厢塘?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵肌幽,是天一觀的道長。 經(jīng)常有香客問我格嘁,道長廊移,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任懂诗,我火速辦了婚禮响禽,結(jié)果婚禮上荚醒,老公的妹妹穿的比我還像新娘。我一直安慰自己界阁,他們只是感情好,可當我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布贮竟。 她就那樣靜靜地躺著咕别,像睡著了一般写穴。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上偿短,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天昔逗,我揣著相機與錄音篷朵,去河邊找鬼。 笑死控硼,一個胖子當著我的面吹牛艾少,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播幔妨,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼误堡,長吁一口氣:“原來是場噩夢啊……” “哼雏吭!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起悉抵,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤姥饰,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后审磁,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體岂座,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡费什,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年吕喘,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片募舟。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡拱礁,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出呢灶,到底是詐尸還是另有隱情钉嘹,我是刑警寧澤,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布缨睡,位于F島的核電站奖年,受9級特大地震影響沛贪,放射性物質(zhì)發(fā)生泄漏震贵。R本人自食惡果不足惜猩系,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一之碗、第九天 我趴在偏房一處隱蔽的房頂上張望季希。 院中可真熱鬧,春花似錦博敬、人聲如沸峰尝。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽硼补。三九已至,卻和暖如春已骇,著一層夾襖步出監(jiān)牢的瞬間票编,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工鲤竹, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留宛裕,地道東北人论泛。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像岩榆,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子犹撒,可洞房花燭夜當晚...
    茶點故事閱讀 44,700評論 2 354

推薦閱讀更多精彩內(nèi)容