在上一篇文章中,我们已经初步实现了交易。相信你应该了解了交易中的一些天然属性,这些属性没有丝毫“个人”色彩的存在:在比特币中,没有用户账户,不需要也不会在任何地方存储个人数据(比如姓名,护照号码或者 SSN)。但是,我们总要有某种途径识别出你是交易输出的所有者(也就是说,你拥有在这些输出上锁定的币)。这就是比特币地址(address)需要完成的使命。在上一篇中,我们把一个由用户定义的任意字符串当成是地址,现在我们将要实现一个跟比特币一样的真实地址。
学会什么是Base58编码和解码
学会创建私钥和公钥
学会使用公钥生成钱包地址
学会将钱包地址进行持久化存储
首先打开Goland开发工具
打开工程:mypublicchain
创建项目:将上一次的项目代码,day03_05_Transaction
,复制为day04_06_Address
说明:我们每一章节的项目代码,都是在上一个章节上进行添加。所以拷贝上一次的项目代码,然后进行新内容的添加或修改。
base58.go
打开day04_06_Address
目录里的BLC包,新建base58.go
文件。添加Base58的编码和解码方法,代码如下:
package BLC
import (
"math/big"
"bytes"
)
//base64
/*
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
0(零),O(大写的o),I(大写的i),l(小写的L),+,/
*/
var b58Alphabet = []byte("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz")
//字节数组转Base58,编码
func Base58Encode(input []byte)[]byte{
var result [] byte
x := big.NewInt(0).SetBytes(input)
base :=big.NewInt(int64(len(b58Alphabet)))
zero:=big.NewInt(0)
mod:= &big.Int{}
for x.Cmp(zero) !=0{
x.DivMod(x,base,mod)
result = append(result,b58Alphabet[mod.Int64()])
}
ReverseBytes(result)
for b:=range input{
if b == 0x00{
result = append([]byte{b58Alphabet[0]},result...)
}else {
break
}
}
return result
}
//Base58转字节数组,解码
func Base58Decode(input[] byte)[]byte{
result:=big.NewInt(0)
zeroBytes := 0
for b:=range input{
if b == 0x00{
zeroBytes++
}
}
payload := input[zeroBytes:]
for _, b := range payload {
charIndex := bytes.IndexByte(b58Alphabet, b)
result.Mul(result, big.NewInt(58))
result.Add(result, big.NewInt(int64(charIndex)))
}
decoded := result.Bytes()
decoded = append(bytes.Repeat([]byte{byte(0x00)}, zeroBytes), decoded...)
return decoded
}
Wallet.go
打开day04_06_Address
目录里的BLC包,创建Wallet.go
文件。在Wallet.go
文件中编写代码如下:
package BLC
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"log"
"crypto/sha256"
"golang.org/x/crypto/ripemd160"
"bytes"
)
//step1:创建一个钱包
type Wallet struct {
//1.私钥
PrivateKey ecdsa.PrivateKey
//2.公钥
PublicKey [] byte
}
//step2:产生一对密钥
func newKeyPair() (ecdsa.PrivateKey, []byte) {
/*
1.通过椭圆曲线算法,随机产生私钥
2.根据私钥生成公钥
elliptic:椭圆
curve:曲线
ecc:椭圆曲线加密
ecdsa:elliptic curve digital signature algorithm,椭圆曲线数字签名算法
比特币使用SECP256K1算法,p256是ecdsa算法中的一种
*/
//椭圆加密
curve := elliptic.P256() //椭圆加密算法,得到一个椭圆曲线值,全称:SECP256k1
private, err := ecdsa.GenerateKey(curve, rand.Reader)
if err != nil {
log.Panic(err)
}
//生成公钥
pubKey := append(private.PublicKey.X.Bytes(), private.PublicKey.Y.Bytes()...)
return *private, pubKey
}
//step3:提供一个方法用于获取钱包
func NewWallet() *Wallet {
privateKey, publicKey := newKeyPair()
//fmt.Println("privateKey:", privateKey, ",publicKey:", publicKey)
return &Wallet{privateKey, publicKey}
}
//step4:根据一个公钥获取对应的地址
/*
将公钥sha2561次,再160,1次
然后version+hash
*/
func (w *Wallet) GetAddress() [] byte {
//1.先将公钥进行一次hash256,一次160,得到pubKeyHash
pubKeyHash := PubKeyHash(w.PublicKey)
//2.添加版本号
versioned_payload := append([]byte{version}, pubKeyHash...)
// 3.获取校验和,将pubKeyhash,两次sha256后,取前4位
checkSumBytes := CheckSum(versioned_payload)
full_payload := append(versioned_payload, checkSumBytes...)
//fmt.Println(len(full_payload))
//4.Base58
address := Base58Encode(full_payload)
return address
}
//一次sha256,再一次ripemd160,得到publicKeyHash
func PubKeyHash(publicKey [] byte) []byte {
//1.sha256
hasher := sha256.New()
hasher.Write(publicKey)
hash := hasher.Sum(nil)
//2.ripemd160
ripemder := ripemd160.New()
ripemder.Write(hash)
pubKeyHash := ripemder.Sum(nil)
//返回
return pubKeyHash
}
const version = byte(0x00)
const addressChecksumLen = 4
//获取验证码:将公钥哈希两次sha256,取前4位,就是校验和
func CheckSum(payload []byte) []byte {
firstSHA := sha256.Sum256(payload)
secondSHA := sha256.Sum256(firstSHA[:])
return secondSHA[:addressChecksumLen]
}
//判断地址是否有效
/*
根据地址,base58解码后获取byte[],获取校验和数组
使用
*/
func IsValidForAddress(address []byte) bool {
full_payload := Base58Decode(address)
//fmt.Println("检验version_public_checksumBytes:",full_payload)
checkSumBytes := full_payload[len(full_payload)-addressChecksumLen:]
//fmt.Println("检验checkSumBytes:",checkSumBytes)
versioned_payload := full_payload[:len(full_payload)-addressChecksumLen]
//fmt.Println("检验version_ripemd160:",versioned_payload)
checkBytes := CheckSum(versioned_payload)
//fmt.Println("检验checkBytes:",checkBytes)
if bytes.Compare(checkSumBytes, checkBytes) == 0 {
return true
}
return false
}
Wallets.go
文件打开day04_06_Address
目录里的BLC包,创建Wallets.go
文件。在Wallets.go
文件中编写代码如下:
添加代码如下:
package BLC
import (
"fmt"
"os"
"io/ioutil"
"log"
"encoding/gob"
"crypto/elliptic"
"bytes"
)
//1.创建钱包
type Wallets struct {
WalletsMap map[string]*Wallet
}
//2.创建一个钱包集合
//创建钱包集合:文件中存在从文件中读取,否则新建一个
const walletFile = "Wallets.dat"
func NewWallets() *Wallets {
//wallets := &WalletsMap{}
//wallets.WalletsMap = make(map[string]*Wallet)
//return wallets
//1.判断钱包文件是否存在
if _, err := os.Stat(walletFile); os.IsNotExist(err) {
fmt.Println("文件不存在")
wallets := &Wallets{}
wallets.WalletsMap = make(map[string]*Wallet)
return wallets
}
//2.否则读取文件中的数据
fileContent, err := ioutil.ReadFile(walletFile)
if err != nil {
log.Panic(err)
}
var wallets Wallets
gob.Register(elliptic.P256())
decoder := gob.NewDecoder(bytes.NewReader(fileContent))
err = decoder.Decode(&wallets)
if err != nil {
log.Panic(err)
}
return &wallets
}
//3.创建一个新钱包
func (ws *Wallets) CreateNewWallet() {
wallet := NewWallet()
fmt.Printf("创建钱包地址:%s\n", wallet.GetAddress())
ws.WalletsMap[string(wallet.GetAddress())] = wallet
//将钱包保存
ws.SaveWallets()
}
/*
要让数据对象能在网络上传输或存储,我们需要进行编码和解码。
现在比较流行的编码方式有JSON,XML等。然而,Go在gob包中为我们提供了另一种方式,该方式编解码效率高于JSON。
gob是Golang包自带的一个数据结构序列化的编码/解码工具
*/
func (ws *Wallets) SaveWallets() {
var content bytes.Buffer
//注册的目的,为了可以序列化任何类型,wallet结构体中有接口类型。将接口进行注册
gob.Register(elliptic.P256()) //gob是Golang包自带的一个数据结构序列化的编码/解码工具
encoder := gob.NewEncoder(&content)
err := encoder.Encode(ws)
if err != nil {
log.Panic(err)
}
//将序列化后的数据写入到文件,原来的文件中的内容会被覆盖掉
err = ioutil.WriteFile(walletFile, content.Bytes(), 0644)
if err != nil {
log.Panic(err)
}
}
CLI.go
文件打开day04_06_Address
目录里的BLC包,修改CLI.go
文件。添加两个命令,创建钱包地址和打印钱包地址。
修改步骤:
修改步骤:
step1:修改printUsage()方法,添加两个命令说明
step2:修改Run()方法,增加两个命令,并解析
createWalletCmd := flag.NewFlagSet("createwallet", flag.ExitOnError)
addressListsCmd := flag.NewFlagSet("addresslists",flag.ExitOnError)
修改完后代码如下:
package BLC
import (
"os"
"fmt"
"flag"
"log"
)
//step1:
//CLI结构体
type CLI struct {
//Blockchain *BlockChain
}
//step2:添加Run方法
func (cli *CLI) Run() {
//判断命令行参数的长度
isValidArgs()
//1.创建flagset标签对象
createWalletCmd := flag.NewFlagSet("createwallet", flag.ExitOnError)
addressListsCmd := flag.NewFlagSet("addresslists",flag.ExitOnError)
sendBlockCmd := flag.NewFlagSet("send", flag.ExitOnError)
//fmt.Printf("%T\n",addBlockCmd) //*FlagSet
printChainCmd := flag.NewFlagSet("printchain", flag.ExitOnError)
createBlockChainCmd := flag.NewFlagSet("createblockchain", flag.ExitOnError)
getBalanceCmd := flag.NewFlagSet("getbalance", flag.ExitOnError)
//2.设置标签后的参数
//flagAddBlockData:= addBlockCmd.String("data","helloworld..","交易数据")
flagFromData := sendBlockCmd.String("from", "", "转帐源地址")
flagToData := sendBlockCmd.String("to", "", "转帐目标地址")
flagAmountData := sendBlockCmd.String("amount", "", "转帐金额")
flagCreateBlockChainData := createBlockChainCmd.String("address", "", "创世区块交易地址")
flagGetBalanceData := getBalanceCmd.String("address", "", "要查询的某个账户的余额")
//3.解析
switch os.Args[1] {
case "send":
err := sendBlockCmd.Parse(os.Args[2:])
if err != nil {
log.Panic(err)
}
//fmt.Println("----",os.Args[2:])
case "printchain":
err := printChainCmd.Parse(os.Args[2:])
if err != nil {
log.Panic(err)
}
//fmt.Println("====",os.Args[2:])
case "createblockchain":
err := createBlockChainCmd.Parse(os.Args[2:])
if err != nil {
log.Panic(err)
}
case "getbalance":
err := getBalanceCmd.Parse(os.Args[2:])
if err != nil {
log.Panic(err)
}
case "createwallet":
err := createWalletCmd.Parse(os.Args[2:])
if err != nil {
log.Panic(err)
}
case "addresslists":
err := addressListsCmd.Parse(os.Args[2:])
if err != nil {
log.Panic(err)
}
default:
printUsage()
os.Exit(1) //退出
}
if sendBlockCmd.Parsed() {
if *flagFromData == "" || *flagToData == "" || *flagAmountData == "" {
printUsage()
os.Exit(1)
}
//cli.addBlock([]*Transaction{})
fmt.Println(*flagFromData)
fmt.Println(*flagToData)
fmt.Println(*flagAmountData)
//fmt.Println(JSONToArray(*flagFrom))
//fmt.Println(JSONToArray(*flagTo))
//fmt.Println(JSONToArray(*flagAmount))
from := JSONToArray(*flagFromData)
to := JSONToArray(*flagToData)
amount := JSONToArray(*flagAmountData)
for i := 0; i < len(from); i++ {
if !IsValidForAddress([]byte(from[i])) || !IsValidForAddress([]byte(to[i])) {
fmt.Println("钱包地址无效")
printUsage()
os.Exit(1)
}
}
cli.send(from, to, amount)
}
if printChainCmd.Parsed() {
cli.printChains()
}
if createBlockChainCmd.Parsed() {
//if *flagCreateBlockChainData == "" {
if !IsValidForAddress([]byte(*flagCreateBlockChainData)){
fmt.Println("创建地址无效")
printUsage()
os.Exit(1)
}
cli.createGenesisBlockchain(*flagCreateBlockChainData)
}
if getBalanceCmd.Parsed() {
//if *flagGetBalanceData == "" {
if !IsValidForAddress([]byte(*flagGetBalanceData)){
fmt.Println("查询地址无效")
printUsage()
os.Exit(1)
}
cli.getBalance(*flagGetBalanceData)
}
if createWalletCmd.Parsed() {
//创建钱包
cli.createWallet()
}
//获取所有的钱包地址
if addressListsCmd.Parsed(){
cli.addressLists()
}
}
func isValidArgs() {
if len(os.Args) < 2 {
printUsage()
os.Exit(1)
}
}
func printUsage() {
fmt.Println("Usage:")
fmt.Println("\tcreatewallet -- 创建钱包")
fmt.Println("\taddresslists -- 输出所有钱包地址")
fmt.Println("\tcreateblockchain -address DATA -- 创建创世区块")
fmt.Println("\tsend -from From -to To -amount Amount - 交易数据")
fmt.Println("\tprintchain - 输出信息:")
fmt.Println("\tgetbalance -address DATA -- 查询账户余额")
}
CLI_createwallet.go
打开day04_06_Address
目录里的BLC包,新建CLI_createwallet.go
文件。
并编写代码如下:
package BLC
func (cli *CLI) createWallet(){
wallets:= NewWallets()
wallets.CreateNewWallet()
}
CLI_addresslists.go
打开day04_06_Address
目录里的BLC包,新建CLI_addresslists.go
文件。
并编写代码如下:
package BLC
import "fmt"
func (cli *CLI)addressLists(){
fmt.Println("打印所有的钱包地址。。")
//获取
Wallets:=NewWallets()
for address,_ := range Wallets.WalletsMap{
fmt.Println("address:",address)
}
}
main.go
无需修改在main.go
中代码不变,依然如下:
package main
import (
"./BLC"
)
func main() {
//1.测试Block
//block:=BLC.NewBlock("I am a block",make([]byte,32,32),1)
//fmt.Println(block)
//2.测试创世区块
//genesisBlock :=BLC.CreateGenesisBlock("Genesis Block..")
//fmt.Println(genesisBlock)
//3.测试区块链
//genesisBlockChain := BLC.CreateBlockChainWithGenesisBlock()
//fmt.Println(genesisBlockChain)
//fmt.Println(genesisBlockChain.Blocks)
//fmt.Println(genesisBlockChain.Blocks[0])
//4.测试添加新区块
//blockChain:=BLC.CreateBlockChainWithGenesisBlock()
//blockChain.AddBlockToBlockChain("Send 100RMB To Wangergou",blockChain.Blocks[len(blockChain.Blocks)-1].Height+1,blockChain.Blocks[len(blockChain.Blocks)-1].Hash)
//blockChain.AddBlockToBlockChain("Send 300RMB To lixiaohua",blockChain.Blocks[len(blockChain.Blocks)-1].Height+1,blockChain.Blocks[len(blockChain.Blocks)-1].Hash)
//blockChain.AddBlockToBlockChain("Send 500RMB To rose",blockChain.Blocks[len(blockChain.Blocks)-1].Height+1,blockChain.Blocks[len(blockChain.Blocks)-1].Hash)
//
//fmt.Println(blockChain)
//5.测试序列化和反序列化
//block:=BLC.NewBlock("helloworld",make([]byte,32,32),0)
//data:=block.Serilalize()
//fmt.Println(block)
//fmt.Println(data)
//block2:=BLC.DeserializeBlock(data)
//fmt.Println(block2)
//6.创建区块,存入数据库
//打开数据库
//block:=BLC.NewBlock("helloworld",make([]byte,32,32),0)
//db,err := bolt.Open("my.db",0600,nil)
//if err != nil{
// log.Fatal(err)
//}
//
//defer db.Close()
//
//err = db.Update(func(tx *bolt.Tx) error {
// //获取bucket,没有就创建新表
// b := tx.Bucket([]byte("blocks"))
// if b == nil{
// b,err = tx.CreateBucket([] byte("blocks"))
// if err !=nil{
// log.Panic("创建表失败")
// }
// }
// //添加数据
// err = b.Put([]byte("l"),block.Serilalize())
// if err !=nil{
// log.Panic(err)
// }
//
// return nil
//})
//if err != nil{
// log.Panic(err)
//}
//err = db.View(func(tx *bolt.Tx) error {
// b := tx.Bucket([]byte("blocks"))
// if b !=nil{
// data := b.Get([]byte("l"))
// //fmt.Printf("%s\n",data)//直接打印会乱码
// //反序列化
// block2:=BLC.DeserializeBlock(data)
// //fmt.Println(block2)
// fmt.Printf("%v\n",block2)
//
// }
// return nil
//})
//7.测试创世区块存入数据库
//blockchain:=BLC.CreateBlockChainWithGenesisBlock("Genesis Block..")
//fmt.Println(blockchain)
//defer blockchain.DB.Close()
//8.测试新添加的区块
//blockchain.AddBlockToBlockChain("Send 100RMB to wangergou")
//blockchain.AddBlockToBlockChain("Send 100RMB to lixiaohua")
//blockchain.AddBlockToBlockChain("Send 100RMB to rose")
//fmt.Println(blockchain)
//blockchain.PrintChains()
//9.CLI操作
cli:=BLC.CLI{}
cli.Run()
}
这就是一个真实的比特币地址:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa。这是史上第一个比特币地址,据说属于中本聪。比特币地址是完全公开的,如果你想要给某个人发送币,只需要知道他的地址就可以了。但是,地址(尽管地址也是独一无二的)并不是用来证明你是一个“钱包”所有者的信物。实际上,所谓的地址,只不过是将公钥表示成人类可读的形式而已,因为原生的公钥人类很难阅读。在比特币中,你的身份(identity)就是一对(或者多对)保存在你的电脑(或者你能够获取到的地方)上的公钥(public key)和私钥(private key)。比特币基于一些加密算法的组合来创建这些密钥,并且保证了在这个世界上没有其他人能够取走你的币,除非拿到你的密钥。下图是创建钱包后到产生地址的流程图。
1、对于比特币来说,钱不是支付给个人的,而是支付给某一把私钥。这就是交易匿名性的根本原因,因为没有人知道,那些私钥背后的主人是谁。所以,比特币交易的第一件事,就是你必须拥有自己的公钥和私钥。
2、去网上那些比特币交易所开户,它们会让你首先生成一个比特币钱包(wallet)。这个钱包不是用来存放比特币,而是存放你的公钥和私钥。软件会帮你生成这两把钥匙,然后放在钱包里面。根据协议,公钥的长度是512位。这个长度不太方便传播,因此协议又规定,要为公钥生成一个160位的指纹。所谓指纹,就是一个比较短的、易于传播的哈希值。160位是二进制,写成十六进制,大约是26到35个字符,比如 1Ko29e6QcUXzhrtHD5aizMa6ustL88RTWR。这个字符串就叫做钱包的地址,它是唯一的,即每个钱包的地址肯定都是不一样的。
3、你向别人收钱时,只要告诉对方你的钱包地址即可,对方向这个地址付款。由于你是这个地址的拥有者,所以你会收到这笔钱。由于你是否拥有某个钱包地址,是由私钥证明的,所以一定要保护好私钥。同样的,你向他人支付比特币,千万不能写错他人的钱包地址,否则你的比特币就支付到了另一个不同的人了。
下面,让我们来讨论一下这些算法到底是什么。
公钥加密(public-key cryptography)算法使用的是成对的密钥:公钥和私钥。公钥并不是敏感信息,可以告诉其他人。但是,私钥绝对不能告诉其他人:只有所有者(owner)才能知道私钥,能够识别,鉴定和证明所有者身份的就是私钥。在加密货币的世界中,你的私钥代表的就是你,私钥就是一切。
本质上,比特币钱包也只不过是这样的密钥对而已。当你安装一个钱包应用,或是使用一个比特币客户端来生成一个新地址时,它就会为你生成一对密钥。在比特币中,谁拥有了私钥,谁就可以控制所有发送到这个公钥的币。
私钥和公钥只不过是随机的字节序列,因此它们无法在屏幕上打印,人类也无法通过肉眼去读取。这就是为什么比特币使用了一个转换算法,将公钥转化为一个人类可读的字符串(也就是我们看到的地址)。
如果你用过比特币钱包应用,很可能它会为你生成一个助记符。这样的助记符可以用来替代私钥,并且可以被用于生成私钥。BIP-039 已经实现了这个机制。
正如之前提到的,公钥和私钥是随机的字节序列。私钥能够用于证明持币人的身份,需要有一个条件:随机算法必须生成真正随机的字节。因为没有人会想要生成一个私钥,而这个私钥意外地也被别人所有。
比特币使用椭圆曲线来产生私钥。椭圆曲线是一个复杂的数学概念,我们并不打算在这里作太多解释(如果你真的十分好奇,可以查看这篇文章,注意:有很多数学公式!)我们只要知道这些曲线可以生成非常大的随机数就够了。在比特币中使用的曲线可以随机选取在 0 与 2 ^ 2 ^ 56(大概是 10^77, 而整个可见的宇宙中,原子数在 10^78 到 10^82 之间) 的一个数。有如此高的一个上限,意味着几乎不可能发生有两次生成同一个私钥的事情。
椭圆曲线算法(ECC,Elliptic Curve Cryptography)
比特币使用的是 ECDSA(Elliptic Curve Digital Signature Algorithm)算法来对交易进行签名,我们也会使用该算法。
比特币钱包随机生成私钥的安全性
每个用户的私钥是由比特币钱包随机生成的。可能有人会有疑问,这样随机生成的私钥安不安全,会不会我随机生成私钥跟别人的私钥恰好重复了?这个你不用过于担心,因为,比特币私钥是一串很长的文本,理论上比特币私钥的总数是:1461501637330902918203684832716283019655932542976
这是个什么概念?地球上的沙子是7.5乘以10的18次方,然后你想象一下,每粒沙子又是一个地球,这时的沙子总数是56.25乘以10的36次方,仍然远小于比特币私钥的总数。所以在这样一个庞大的地址空间下,私钥重复的概率微乎其微。
Base64就是一种基于64个可打印字符来表示二进制数据的方法
Base64使用了26个小写字母、26个大写字母、10个数字以及两个符号(例如“+”和“/”),用于在电子邮件这样的基于文本的媒介中传输二进制数据。
Base64通常用于编码邮件中的附件。
Base64字符集:
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
Base58是一种基于文本的二进制编码格式,是用于Bitcoin中使用的一种独特的编码方式,主要用于产生Bitcoin的钱包地址。
Base58字符集:
ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz123456789
Base58Check
Base58Check是一种常用在比特币中的Base58编码格式,增加了错误校验码来检查数据在转录中出现的错误。在Base58Check中,对数据添加了一个称作“版本字节”的前缀,这个前缀用来明确需要编码的数据的类型。
Base58Check的作用
回到上面提到的比特币地址:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa 。现在,我们已经知道了这是公钥用人类可读的形式表示而已。如果我们对它进行解码,就会看到公钥的本来面目(16 进制表示的字节):0062E907B15CBF27D5425399EBF6F0FB50EBB88F18C29B7D93
比特币使用 Base58 算法将公钥转换成人类可读的形式。这个算法跟著名的 Base64 很类似,区别在于它使用了更短的字母表:为了避免一些利用字母相似性的攻击,从字母表中移除了一些字母。也就是,没有这些符号:0(零),O(大写的 o),I(大写的i),l(小写的 L),因为这几个字母看着很像。另外,也没有 + 和 / 符号。
比特币地址生成过程中的Base58Check的步骤如下:
1. 获取到公钥
2. 计算公钥的 SHA-256 哈希值(对公钥进行第一次hash256运算)
3. 计算 RIPEMD-160 哈希值(对第一次hash256的结果进行ripeMD160运算)
4. 添加版本前缀(将ripeMD160运算的结果前增加版本编号)
5. 计算两次hash(对加上版本编号的hash值计算两次hash)
6. 获取校验码(获取两次hash后前四个字节,作为校验码)
7. 形成比特币地址的16进制格式(将校验码作为比特币地址的后缀,与添加版本前缀的hash值拼接)
8. 进行Base58编码处理(对比特币地址的16进制格式进行Base58编码,就形成了比特币地址)
因此,为了使用Base58Check编码格式对数据(数字)进行编码,首先我们要对数据添加一个称作“版本字节”的前缀,这个前缀用来明确需要编码的数据的类型。
例如,比特币地址的前缀是0(十六进制是0x00),而对私钥编码时前缀是128(十六进制是0x80)。
所以公钥解码后包含三个部分:
Version Public key hash Checksum
00 62E907B15CBF27D5425399EBF6F0FB50EBB88F18 C29B7D93
由于哈希函数是单向的(也就说无法逆转回去),所以不可能从一个哈希中提取公钥。不过通过执行哈希函数并进行哈希比较,我们可以检查一个公钥是否被用于哈希的生成。
好了,所有细节都已就绪,来写代码吧。很多概念只有当写代码的时候,才能理解地更透彻。
由于Base58是在货币中特有的,并不是像Base64那样是通用编码方式,所以golang自带的包中没有实现,(Base64是有的,在"encoding/base64"包下),那么就需要我们自己写工具方法实现编码和解码。
在BLC包下,新建一个go文件,命名为base58.go,并添加代码如下:
package BLC
import (
"math/big"
"bytes"
)
//base64
/*
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
0(零),O(大写的o),I(大写的i),l(小写的L),+,/
*/
var b58Alphabet = []byte("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz")
//字节数组转Base58,解码
func Base58Encode(input []byte)[]byte{
var result [] byte
x := big.NewInt(0).SetBytes(input)
base :=big.NewInt(int64(len(b58Alphabet)))
zero:=big.NewInt(0)
mod:= &big.Int{}
for x.Cmp(zero) !=0{
x.DivMod(x,base,mod)
result = append(result,b58Alphabet[mod.Int64()])
}
ReverseBytes(result)
for b:=range input{
if b == 0x00{
result = append([]byte{b58Alphabet[0]},result...)
}else {
break
}
}
return result
}
//Base58转字节数组,解码
func Base58Decode(input[] byte)[]byte{
result:=big.NewInt(0)
zeroBytes := 0
for b:=range input{
if b == 0x00{
zeroBytes++
}
}
payload := input[zeroBytes:]
for _, b := range payload {
charIndex := bytes.IndexByte(b58Alphabet, b)
result.Mul(result, big.NewInt(58))
result.Add(result, big.NewInt(int64(charIndex)))
}
decoded := result.Bytes()
decoded = append(bytes.Repeat([]byte{byte(0x00)}, zeroBytes), decoded...)
return decoded
}
有一个点需要单独强调一下:
for b:=range input{
if b == 0x00{
result = append([]byte{b58Alphabet[0]},result...)
}else {
break
}
}
对于Base58编码,最后总会拼接下标0对应的Base58字符,就是1。所以我们所产生的地址的第一位,都是1。比如:1NH3bAuMAyXHnCrBmykPcKciBG6W3Dc5vY。
我们先从钱包 Wallet
结构开始:
在BLC包下,新建Wallet.go
文件,并添加Wallet结构体对象。
//step1:创建一个钱包
type Wallet struct {
//1.私钥
PrivateKey ecdsa.PrivateKey
//2.公钥
PublicKey [] byte
}
//step2:产生一对密钥
func newKeyPair() (ecdsa.PrivateKey, []byte) {
/*
1.通过椭圆曲线算法,随机产生私钥
2.根据私钥生成公钥
elliptic:椭圆
curve:曲线
ecc:椭圆曲线加密
ecdsa:elliptic curve digital signature algorithm,椭圆曲线数字签名算法
比特币使用SECP256K1算法,p256是ecdsa算法中的一种
*/
//椭圆加密
curve := elliptic.P256() //椭圆加密算法,得到一个椭圆曲线值,全称:SECP256k1
private, err := ecdsa.GenerateKey(curve, rand.Reader)
if err != nil {
log.Panic(err)
}
//生成公钥
pubKey := append(private.PublicKey.X.Bytes(), private.PublicKey.Y.Bytes()...)
return *private, pubKey
}
//step3:提供一个方法用于获取钱包
func NewWallet() *Wallet {
privateKey, publicKey := newKeyPair()
//fmt.Println("privateKey:", privateKey, ",publicKey:", publicKey)
return &Wallet{privateKey, publicKey}
}
一个钱包只有一个密钥对而已。我们需要 Wallets
类型来保存多个钱包的组合,将它们保存到文件中,或者从文件中进行加载。Wallet
的构造函数会生成一个新的密钥对。newKeyPair
函数非常直观:ECDSA 基于椭圆曲线,所以我们需要一个椭圆曲线。接下来,使用椭圆生成一个私钥,然后再从私钥生成一个公钥。有一点需要注意:在基于椭圆曲线的算法中,公钥是曲线上的点。因此,公钥是 X,Y 坐标的组合。在比特币中,这些坐标会被连接起来,然后形成一个公钥。
现在,来生成一个地址:
//step4:根据一个公钥获取对应的地址
/*
将公钥sha2561次,再160,1次
然后version+hash
*/
func (w *Wallet) GetAddress() [] byte {
//1.先将公钥进行一次hash256,一次160,得到pubKeyHash
pubKeyHash := PubKeyHash(w.PublicKey)
//2.添加版本号
versioned_payload := append([]byte{version}, pubKeyHash...)
// 3.获取校验和,将pubKeyhash,两次sha256后,取前4位
checkSumBytes := CheckSum(versioned_payload)
full_payload := append(versioned_payload, checkSumBytes...)
//fmt.Println(len(full_payload))
//4.Base58
address := Base58Encode(full_payload)
return address
}
//一次sha256,再一次ripemd160,得到publicKeyHash
func PubKeyHash(publicKey [] byte) []byte {
//1.sha256
hasher := sha256.New()
hasher.Write(publicKey)
hash := hasher.Sum(nil)
//2.ripemd160
ripemder := ripemd160.New()
ripemder.Write(hash)
pubKeyHash := ripemder.Sum(nil)
//返回
return pubKeyHash
}
const version = byte(0x00)
const addressChecksumLen = 4
//获取验证码:将公钥哈希两次sha256,取前4位,就是校验和
func CheckSum(payload []byte) []byte {
firstSHA := sha256.Sum256(payload)
secondSHA := sha256.Sum256(firstSHA[:])
return secondSHA[:addressChecksumLen]
}
至此,就可以得到一个真实的比特币地址,你甚至可以在 blockchain.info 查看它的余额。不过我可以负责任地说,无论生成一个新的地址多少次,检查它的余额都是 0。这就是为什么选择一个合适的公钥加密算法是如此重要:考虑到私钥是随机数,生成同一个数字的概率必须是尽可能地低。理想情况下,必须是低到“永远”不会重复。
我们还可以添加一个方法,用于验证一个地址的有效性。验证原理就是,将地址进行Base58解码,得到数据:版本号+hash数据+checksum,将版本号+hash数据,重新生成新的checksum,和原来的checksum进行比较,如果不同,说明地址无效。
//判断地址是否有效
/*
根据地址,base58解码后获取byte[],获取校验和数组
使用
*/
func IsValidForAddress(address []byte) bool {
full_payload := Base58Decode(address)
//fmt.Println("检验version_public_checksumBytes:",full_payload)
checkSumBytes := full_payload[len(full_payload)-addressChecksumLen:]
//fmt.Println("检验checkSumBytes:",checkSumBytes)
versioned_payload := full_payload[:len(full_payload)-addressChecksumLen]
//fmt.Println("检验version_ripemd160:",versioned_payload)
checkBytes := CheckSum(versioned_payload)
//fmt.Println("检验checkBytes:",checkBytes)
if bytes.Compare(checkSumBytes, checkBytes) == 0 {
return true
}
return false
}
接下来,在CLI.go
文件,修改Run(),添加一个命令,用于创建钱包:
func (cli *CLI) Run() {
//判断命令行参数的长度
isValidArgs()
//1.创建flagset标签对象
createWalletCmd := flag.NewFlagSet("createwallet", flag.ExitOnError)
...
//3.解析
switch os.Args[1] {
...
case "createwallet":
err := createWalletCmd.Parse(os.Args[2:])
if err != nil {
log.Panic(err)
}
default:
printUsage()
os.Exit(1) //退出
}
...
if createWalletCmd.Parsed() {
//创建钱包
cli.createWallet()
}
}
然后创建一个go文件,CLI_createwallet.go
,并添加代码如下:
package BLC
func (cli *CLI) createWallet(){
wallets:= NewWallets()
wallets.CreateNewWallet()
}
接下来,我们代码测试一下:
在终端中输入以下命令:
hanru:day04_06_Address ruby$ ./bc createwallet
运行效果如下:
另外,注意:你并不需要连接到一个比特币节点来获得一个地址。地址生成算法使用的多种开源算法可以通过很多编程语言和库实现。
一个钱包对象,存储了一对私钥和公钥,公钥生成一个地址。现在我们可以创建一个钱包集合,可以存储多个地址。
新建一个Wallets.go
文件,并添加Wallets结构体:
//1.创建钱包
type Wallets struct {
WalletsMap map[string]*Wallet
}
对于map集合,一个钱包地址,对应了一个钱包对象。
目前的钱包地址创建后,程序结束后内存就销毁了,那么我们需要将创建好的钱包地址,存储到本地文件中,进行持久化保存。
//2.创建一个钱包集合
//创建钱包集合:文件中存在从文件中读取,否则新建一个
const walletFile = "Wallets.dat"
func NewWallets() *Wallets {
//wallets := &WalletsMap{}
//wallets.WalletsMap = make(map[string]*Wallet)
//return wallets
//1.判断钱包文件是否存在
if _, err := os.Stat(walletFile); os.IsNotExist(err) {
fmt.Println("文件不存在")
wallets := &Wallets{}
wallets.WalletsMap = make(map[string]*Wallet)
return wallets
}
//2.否则读取文件中的数据
fileContent, err := ioutil.ReadFile(walletFile)
if err != nil {
log.Panic(err)
}
var wallets Wallets
gob.Register(elliptic.P256())
decoder := gob.NewDecoder(bytes.NewReader(fileContent))
err = decoder.Decode(&wallets)
if err != nil {
log.Panic(err)
}
return &wallets
}
//3.创建一个新钱包
func (ws *Wallets) CreateNewWallet() {
wallet := NewWallet()
fmt.Printf("创建钱包地址:%s\n", wallet.GetAddress())
ws.WalletsMap[string(wallet.GetAddress())] = wallet
//将钱包保存
ws.SaveWallets()
}
/*
要让数据对象能在网络上传输或存储,我们需要进行编码和解码。
现在比较流行的编码方式有JSON,XML等。然而,Go在gob包中为我们提供了另一种方式,该方式编解码效率高于JSON。
gob是Golang包自带的一个数据结构序列化的编码/解码工具
*/
func (ws *Wallets) SaveWallets() {
var content bytes.Buffer
//注册的目的,为了可以序列化任何类型,wallet结构体中有接口类型。将接口进行注册
gob.Register(elliptic.P256()) //gob是Golang包自带的一个数据结构序列化的编码/解码工具
encoder := gob.NewEncoder(&content)
err := encoder.Encode(ws)
if err != nil {
log.Panic(err)
}
//将序列化后的数据写入到文件,原来的文件中的内容会被覆盖掉
err = ioutil.WriteFile(walletFile, content.Bytes(), 0644)
if err != nil {
log.Panic(err)
}
}
接下来,我们一步一步讲解钱包地址持久化的过程。
首先,我们需要先定义一个文件:Wallets.dat
,用于存储钱包集合中的数据。然后创建了一个NewWallets()
方法,用于获取一个Wallets对象,而Wallets里存储钱包地址对应钱包对象。那么我们首先判断钱包文件是否存在,如果不存在,我们就直接创建一个空的Wallets钱包集合对象。
if _, err := os.Stat(walletFile); os.IsNotExist(err) {
fmt.Println("文件不存在")
wallets := &Wallets{}
wallets.WalletsMap = make(map[string]*Wallet)
return wallets
}
如果文件存在,那么我们应该从文件中读取钱包数据。
//2.否则读取文件中的数据
fileContent, err := ioutil.ReadFile(walletFile)
if err != nil {
log.Panic(err)
}
var wallets Wallets
gob.Register(elliptic.P256())
decoder := gob.NewDecoder(bytes.NewReader(fileContent))
err = decoder.Decode(&wallets)
if err != nil {
log.Panic(err)
}
return &wallets
当获取到一个Wallets钱包集合对象后,可以创建新的钱包对象以及地址,并进行存储。所以创建一个CreateNewWallet()
方法,并添加代码如下:
//3.创建一个新钱包
func (ws *Wallets) CreateNewWallet() {
wallet := NewWallet()
fmt.Printf("创建钱包地址:%s\n", wallet.GetAddress())
ws.WalletsMap[string(wallet.GetAddress())] = wallet
//将钱包保存
ws.SaveWallets()
}
每当创建一个钱包对象后,都应该将该对象存入到文件中,所以接下来我们再创建一个SaveWallets()
方法,用于保存数据,代码如下:
/*
要让数据对象能在网络上传输或存储,我们需要进行编码和解码。
现在比较流行的编码方式有JSON,XML等。然而,Go在gob包中为我们提供了另一种方式,该方式编解码效率高于JSON。
gob是Golang包自带的一个数据结构序列化的编码/解码工具
*/
func (ws *Wallets) SaveWallets() {
var content bytes.Buffer
//注册的目的,为了可以序列化任何类型,wallet结构体中有接口类型。将接口进行注册
gob.Register(elliptic.P256()) //gob是Golang包自带的一个数据结构序列化的编码/解码工具
encoder := gob.NewEncoder(&content)
err := encoder.Encode(ws)
if err != nil {
log.Panic(err)
}
//将序列化后的数据写入到文件,原来的文件中的内容会被覆盖掉
err = ioutil.WriteFile(walletFile, content.Bytes(), 0644)
if err != nil {
log.Panic(err)
}
}
最后,我们在CLI.go
中,添加一个命令,用于打印出Wallets钱包集合中的所有的地址。
修改Run()方法代码如下:
//step2:添加Run方法
func (cli *CLI) Run() {
//判断命令行参数的长度
isValidArgs()
//1.创建flagset标签对象
addressListsCmd := flag.NewFlagSet("addresslists",flag.ExitOnError)
...
//3.解析
switch os.Args[1] {
...
case "addresslists":
err := addressListsCmd.Parse(os.Args[2:])
if err != nil {
log.Panic(err)
}
default:
printUsage()
os.Exit(1) //退出
}
//获取所有的钱包地址
if addressListsCmd.Parsed(){
cli.addressLists()
}
}
在BLC包下,创建一个go文件,命名为CLI_addresslists.go
,并添加代码如下:
package BLC
import "fmt"
func (cli *CLI)addressLists(){
fmt.Println("打印所有的钱包地址。。")
//获取
Wallets:=NewWallets()
for address,_ := range Wallets.WalletsMap{
fmt.Println("address:",address)
}
}
最后,我们测试一下程序,在终端输入以下命令:
hanru:day04_06_Address ruby$ ./bc addresslists
运行效果如下:
通过本章节的学习,我们知道了如何创建一个钱包,钱包中如何创建一对秘钥。我们可以根据公钥生成钱包地址,这个过程虽然有点繁琐,但是不难理解。首先将公钥,进行一次sha256,一次ripemd160,进行hash散列,生成公钥hash(也叫指纹)。再用公钥hash前加1个byte的版本号,一般都是0x00,然后进行两次sha256,获取前4位,作为checksum,然后就得到了版本号+公钥hash+checksum的数据。最后进行一次Base58编码,就得到了钱包地址。
程序中的钱包地址都是存储在map中,为了进行持久化,我们还需要将创建好的钱包数据,保存到本地文件中。