在比特币中,没有用户账户,不需要也不会在任何地方存储个人数据(比如姓名,护照号码或者 SSN)。但是,我们总要有某种途径识别出你是交易输出的所有者(即比特币的拥有者)。这就是一个真实的比特币地址:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa。这是史上第一个比特币地址,据说属于中本聪。比特币地址是完全公开的,如果你想要给某个人发送币,只需要知道他的地址就可以了。
钱包的实现
钱包存储的是公钥和私钥,定义钱包非常简单,使用如下结构体就能定义一个钱包了。
难的是实现钱包的算法,好在大部分语言都已经封装好了....
私钥
私钥可以是1和n-1之间的任何数字, 其中n是⼀个常数(n=1.158*1077, 略⼩于2256) , 并由⽐特币所使⽤的椭圆曲线的阶所定义要⽣成这样的⼀个私钥, 我们随机选择⼀个256位的数字, 并检查它是否⼩于n-1。
说白了就是一个随机选出来的数字。但是私钥⽤于⽣成⽀付⽐特币所必需的签名以证明资⾦的所有权。
公钥
通过椭圆曲线算法可以从私钥计算得到公钥, 这是不可逆转的过程: K = k * G 。 其中 k 是私钥, G 是被称为⽣成点的常数点, ⽽ K 是所得公钥。 其反向运算, 被称为“寻找离散对数”——已知公钥 K 来求出私钥 k ——是⾮常困难的, 就像去试验所有可能的 k 值, 即暴⼒搜索。
还有就是,公钥即身份!
实现公私钥对
实现公私钥对非常直观:ECDSA 基于椭圆曲线,所以我们需要一个椭圆曲线。接下来,使用椭圆生成一个私钥,然后再从私钥生成一个公钥。有一点需要注意:在基于椭圆曲线的算法中,公钥是曲线上的点。因此,公钥是 X,Y 坐标的组合。在比特币中,这些坐标会被连接起来,然后形成一个公钥。具体可以去网上参考椭圆曲线加密算法。
定义钱包
钱包中只需要保存用户的公私钥对wallet.go。
type Wallet struct {
//私钥
PrivateKey ecdsa.PrivateKey
//公钥(该属性可以从私钥推导)
PublicKey []byte
}
//构造钱包
func NewWallet() *Wallet{
private, public := newPrivAndPub()
return &Wallet{private,public}
}
func newPrivAndPub()(ecdsa.PrivateKey,[]byte){
//声明p-256曲线
p256curve := elliptic.P256()
//使用p256曲线生成私钥,
//GenerateKey函数生成一对公钥/私钥。
//Reader是一个全局、共享的密码用强随机数生成器。在Unix类型系统中,会从/dev/urandom读取;而Windows中会调用CryptGenRandom API。
privKey, err := ecdsa.GenerateKey(p256curve, rand.Reader)
//go语法中判断是否发生错误,如果错误,打印错误
if err != nil {
log.Panic(err)
}
//获取最原始公钥,即连接X,Y坐标得到的结果就是最原始公钥
pubKey := append(privKey.PublicKey.X.Bytes(),privKey.PublicKey.Y.Bytes()...)
//返回结果
return *privKey,pubKey
}
实现比特地址的步骤和图解
先确定版本号跟地址验证
使用 RIPEMD160(SHA256(PubKey)) 哈希算法,取公钥并对其哈希两次
给哈希加上地址生成算法版本的前缀
对于第二步生成的结果,使用 SHA256(SHA256(payload)) 再哈希,计算校验和。校验和是结果哈希的前四个字节。
将校验和附加到 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 则创建相关目录
git clone https://github.com/golang/crypto.git
Base58Encode()可能要自己上网找了,可以看我用的,在Github里
实现代码如下:
//获取地址
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()
//传进参数,注意格式的转变,sha256.Sum256()返回一个[Size]byte格式,所以需要转为切片
_, err := ripemd160Hasher.Write(pubkey256[:])
if err != nil {
log.Panic(err)
}
pubkey160 := ripemd160Hasher.Sum(nil)
return pubkey160
}
//生成校验码,把version + pubkey160的结果进行两次sha256后取其前4个字节
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]
//获取数据
pubKey160 := pubKeyHash[1:len(pubKeyHash) - addressCheckSum]
//重新组合校验码
checkSum := checksum(append([]byte{version},pubKey160...))
//验证校验码的
return bytes.Compare(srcCheckSum,checkSum) == 0
}
使用文件保存钱包的集合
上面我们已经实现了钱包,但是我们无法保存每个钱包的地址,本小节我们就是要使用文件保存钱包的集合.
定义钱包集合的结构体,在构造实例的时候通过读取文件获取所有的钱包地址wallets.go:
//定义存储的文件
const WalletsFile = "wallets.dat"
//钱包集合的结构体
type Wallets struct {
//key是钱包地址,value是钱包
Wallets map[string]*Wallet
}
//构造钱包集合
//因为之后我们需要通过获取来地址进行钱包地址的遍历和根据地址获得指定的钱包实例
//所以创建钱包过程需要单独列出来
func NewWallets()(*Wallets, error) {
//因为只有一个钱包,随意在这里使用指针类型传递
wallets := &Wallets{}
wallets.Wallets = make(map[string]*Wallet)
err := wallets.LoadFromFile()
return wallets,err
}
关于钱包地址的读取和保存,主要实现对钱包集合的序列化数据保存到文件,把反序列化数据设置到Wallets结构体的map字典中
//从文件中获取钱包集合
func (ws *Wallets) LoadFromFile() error {
if _,err:=os.Stat(WalletsFile);os.IsNotExist(err){
return err;
}
//读取文件中的序列化字节数据
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)
}
//构造钱包集合
ws.Wallets = wallets.Wallets
return nil
}
//保存钱包集合的序列化数据
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)
}
//把序列化数据写入文件
err = ioutil.WriteFile(WalletsFile,buffer.Bytes(),0666)
if err != nil {
log.Panic(err)
}
}
而我们并不会实用NewWallet去新建的一个钱包,而是通过在集合中的CreateWallet方法进行钱包的新建,新建的钱包就会加入到集合中
//单独把创建钱包列出来
//创建一个钱包,并把钱包加入到集合中
func (wallets *Wallets)CreateWallet() string {
//声明钱包集合
wallet := NewWallet()
//获取钱包地址
address := fmt.Sprintf("%s",wallet.GetAddress())
//把钱包加入集合
wallets.Wallets[address] = wallet
return address
}
有了钱包集合我们就可以通过GetAddresses进行钱包地址的遍历和根据地址获得指定的钱包实例
//根据地址获取一个钱包实例
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
}
定义命令行工具测试
为了便于使用,我们通过客户端命令行供用户去进行使用,本小节我们添加的功能只有创建钱包和遍历钱包集合的命令行功能。createwallet命令实现钱包的创建,listaddresses命令实现钱包集合的遍历,后面我不断完善功能我可以添加更多不同的命令,这里我们定义cli.go作为客户端命令行的主体部分,Run方法可以运行起整个客户端命令行,之后我们添加不同命令对应在Run中进行注册即可完成。
cli.go代码创建如下:
/*
@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 - 创建一个新的钱包地址")
fmt.Println(" listaddresses - 遍历输出所有的钱包地址")
}
//当用户直接输入 ./bitCoin但没有参数时提示帮助信息
func (cli *CLI) validateArgs(){
if len(os.Args) < 2 {
cli.printUsage()
os.Exit(1)
}
}
//运行客户端命令行
func (cli *CLI) Run () {
//没有输入时运行的函数
cli.validateArgs()
//注册createwallet命令
createWalletCmd := flag.NewFlagSet("createwallet", flag.ExitOnError)
//注册listaddresses命令
listAddressesCmd := flag.NewFlagSet("listaddresses", flag.ExitOnError)
//判断输入的函数
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就执行对应方法
if createWalletCmd.Parsed() {
cli.createWallet()
}
//如果用户输入的是listaddresses就执行对应方法
if listAddressesCmd.Parsed() {
cli.listAddresses()
}
}
我们注册的创建钱包的方法放在cli_createwallet.go中
/*
@Time : 2019/9/6 下午3:16
@Author : yeyangfengqi
@File : cli_createwallet
@Software: GoLand
@effect: 命令行工具——创建钱包
*/
package main
import "fmt"
func (cli *CLI)createWallet() {
//创建集合
wallets, _ := NewWallets()
//创建钱包并把钱包写入map[string]*Wallet字典中
address := wallets.CreateWallet()
//保持集合序列化数据到文件中
wallets.SaveToFile()
fmt.Printf("你创建的新钱包地址是: %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)
}
}
目录结构及代码测试如下: