【重要】GO实现BTC-V5-钱包
本章节重点部分为 INput output的类定义与使用 交易传输改写 在中间部分
ecdsa签名demo
package main
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"fmt"
"math/big"
"os"
)
func main() {
// 1、创建私钥
// 2、创建公钥
// 3、私钥对数据进行签名(对数据的哈希值进行签名)
// 4、使用数据,签名,公钥进行校验
//选择一个椭圆曲线(在elliptic包)
curve := elliptic.P256()
//获取私钥
privateKey,err := ecdsa.GenerateKey(curve,rand.Reader)
if err != nil{
os.Exit(1)
}
//通过私钥获取公钥
pubKey := privateKey.PublicKey
//获取数据哈希
data := "hello"
dataHash := sha256.Sum256([]byte(data))
//func Sign(rand io.Reader, priv *PrivateKey, hash []byte) (r, s *big.Int, err error) {
r,s,err := ecdsa.Sign(rand.Reader,privateKey,dataHash[:])
//一般传输过程中,会把r,s拼成字节流再传输
fmt.Printf("r:%x,len(r):%d\n",r.Bytes(),len(r.Bytes()))
fmt.Printf("r:%x,len(s):%d\n",s.Bytes(),len(s.Bytes()))
signature := append(r.Bytes(),s.Bytes()...)
if err != nil{
os.Exit(1)
}
//传输中....: 数据, 签名signature,公钥
//在接收端从中把r和s切出来
var r1 big.Int
var s1 big.Int
r1Data := signature[:len(signature)/2]
s1Data := signature[len(signature)/2:]
r1.SetBytes(r1Data)
s1.SetBytes(s1Data)
fmt.Printf("r1 : %x, len(r1): %d\n", r1.Bytes(), len(r1.Bytes()))
fmt.Printf("s1 : %x, len(s1): %d\n", s1.Bytes(), len(s1.Bytes()))
//func Verify(pub *PublicKey, hash []byte, r, s *big.Int) bool {
res := ecdsa.Verify(&pubKey,dataHash[:],&r1,&s1)
fmt.Printf("res:%v\n",res)
}
生成新的比特币地址
1. 创建一个结构WalletKeyPair秘钥对,保存公钥和私钥
2. 给这个结构提供一个方法`GetAddress`:私钥->公钥->地址
定义钱包结构
//1. 创建一个结构WalletKeyPair秘钥对,保存公钥和私钥
//2. 给这个结构提供一个方法GetAddress:私钥->公钥->地址
type WalletKeyPair struct {
PrivateKey *ecdsa.PrivateKey
// type PublicKey struct {
// elliptic.Curve
// X, Y *big.Int
//}
//我们可以将公钥的X,Y进行字节流拼接后传输,这样在对端再进行切割还原,好处是可以方便后面的编码
PublicKey []byte
}
func NewWalletKeyPair() *WalletKeyPair {
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
log.Panic(err)
}
publicKeyRaw := privateKey.PublicKey
publicKey := append(publicKeyRaw.X.Bytes(), publicKeyRaw.Y.Bytes()...)
return &WalletKeyPair{PrivateKey: privateKey, PublicKey: publicKey}
}
根据公钥获取地址
func (w *WalletKeyPair) GetAddress() string {
hash := sha256.Sum256(w.PublicKey)
//创建一个hash160对象
//向hash160中write数据
//做哈希运算
rip160Haher := ripemd160.New()
_, err := rip160Haher.Write(hash[:])
if err != nil {
log.Panic(err)
}
//Sum函数会把我们的结果与Sum参数append到一起,然后返回,我们传入nil,防止数据污染
publicHash := rip160Haher.Sum(nil)
version := 0x00
//21字节的数据
payload := append([]byte{byte(version)}, publicHash...)
first := sha256.Sum256(payload)
second := sha256.Sum256(first[:])
//4字节校验码
checksum := second[0:4]
//25字节
payload = append(payload, checksum...)
address := base58.Encode(payload)
return address
}
获取钱包实例
//创建Wallets, 返回Wallets的实例
func NewWallets() *Wallets {
var ws Wallets
ws.WalletsMap = make(map[string]*WalletKeyPair)
//1. 把所有的钱包从本地加载出来
//TODO
//2. 把实例返回
return &ws
}
创建新的钱包
//这个Wallets是对外的,WalletKeyPair是对内的
//Wallets调用WalletKeypPair
func (ws *Wallets) CreateWallet() string {
//滴啊用NewWalletKeyPair
wallet := NewWalletKeyPair()
//将返回的walletKeypair添加到WalletMap中
address := wallet.GetAddress()
ws.WalletsMap[address] = wallet
//
//保存到本地文件
//TODO
return address
}
调用
func (cli *CLI) CreateWallet() {
ws := NewWallets()
address := ws.CreateWallet()
fmt.Printf("新的钱包地址为: %s\n", address)
}
保存钱包到文件
注意两个知识点:
1. gob.Register:对于结构中的interface类型,需要明确类型 在gob进行注册 编码解码时都需要
2. iotutil.WriteFile实现文件写入
//保存钱包到文件
func (ws *Wallets) SaveToFile() bool {
var buffer bytes.Buffer
//将接口类型明确注册一下,否则gob编码失败!
gob.Register(elliptic.P256())
encoder := gob.NewEncoder(&buffer)
err := encoder.Encode(ws)
if err != nil {
fmt.Printf("钱包序列化失败!, err: %v\n", err)
return false
}
content := buffer.Bytes()
//func WriteFile(filename string, data []byte, perm os.FileMode) error {
err = ioutil.WriteFile(WalletName, content, 0600)
if err != nil {
fmt.Printf("钱包创建失败!\n")
return false
}
return true
}
加载钱包
1. ioutil.ReadFile
2. 别忘了校验解码结果
func (ws *Wallets) LoadFromFile() bool {
//判断文件是否存在
if !IsFileExist(WalletName) {
fmt.Printf("钱包文件不存在,准备创建!\n")
return true
}
//读取文件
//func ReadFile(filename string) ([]byte, error) {
content, err := ioutil.ReadFile(WalletName)
if err != nil {
return false
}
gob.Register(elliptic.P256())
//gob解码
decoder := gob.NewDecoder(bytes.NewReader(content))
var wallets Wallets
err = decoder.Decode(&wallets)
if err != nil {
fmt.Printf("err : %v\n", err)
return false
}
//赋值给ws
ws.WalletsMap = wallets.WalletsMap
return true
}
遍历钱包,打印所有地址
func (ws *Wallets) ListAddress() []string {
//遍历ws.WalletsMap结构返回key即可
var addresses []string
for address, _ := range ws.WalletsMap {
addresses = append(addresses, address)
}
return addresses
}
在commands.go中调用
func (cli *CLI) ListAddresses() {
ws := NewWallets()
addresses := ws.ListAddress()
for _, address := range addresses {
fmt.Printf("address : %s\n", address)
}
}
重点部分
进行改写
改写TXInput和TXOutput
type TXInput struct {
TXID []byte //交易id
Index int64 //output的索引
//Address string //解锁脚本,先使用地址来模拟
Signature []byte //交易签名
PubKey []byte //公钥本身,不是公钥哈希
}
type TXOutput struct {
Value float64 //转账金额
//Address string //锁定脚本
PubKeyHash []byte //是公钥的哈希,不是公钥本身
}
地址可以反推出公钥哈希 并且附给 TXOutput 中的 PubKeyHash []byte 在转账给地址时需要先反推出公钥哈希

TXOutput提供Lock方法
给定地址,得到地址的公钥哈希,锁定这个output
//给定转账地址,得到这个地址的公钥哈希,完成对output的锁定
func (output *TXOutput) Lock(address string) {
//address -> public key hash
//25字节
decodeInfo := base58.Decode(address)
pubKeyHash := decodeInfo[1:len(decodeInfo)-4]
output.PubKeyHash = pubKeyHash
}
创建TXOutput
由于创建过程需要调用Lock方法,所以我们自顶一个NewTXOutput方法
func NewTXOutput(value float64, address string) TXOutput {
output := TXOutput{Value: value}
output.Lock(address)
return output
}
重点部分
多参照下面的图,只是把Address地址改变了 逻辑并没用太大的改变 在如下改写代码时候也应该对照看这两个图对应,以免混淆
input中存储公钥 做哈希处理后 和output种的公钥哈希进行比较
在获取余额这个过程,不要打开钱包,因为有可能查看余额的人不是地址本人 [通过地址反推出公钥哈希即可]
改写NewCoinbaseTx
将奖励值定义为常量 const reward = 12.5
func NewCoinbaseTx(miner string, data string) *Transaction {
//我们在后面的程序中,需要识别一个交易是否为coinbase,所以我们需要设置一些特殊的值,用于判断
inputs := []TXInput{TXInput{nil, -1, nil, []byte(data)}}
//outputs := []TXOutput{TXOutput{12.5, miner}}
output := NewTXOutput(reward, miner)
outputs := []TXOutput{output}
tx := Transaction{nil, inputs, outputs}
tx.SetTXID()
return &tx
}
改写NewTransaction
func NewTransaction(from, to string, amount float64, bc *BlockChain) *Transaction {
//1. 打开钱包
ws := NewWallets()
//获取秘钥对
wallet := ws.WalletsMap[from]
if wallet == nil {
fmt.Printf("%s 的私钥不存在,交易创建失败!\n", from)
return nil
}
//2. 获取公钥,私钥
//privateKey := wallet.PrivateKey //目前使用不到,步骤三签名时使用
publickKey := wallet.PublicKey
pubKeyHash := hashPubkHash(from)
utxos := make(map[string][]int64) //标识能用的utxo
var resValue float64 //这些utxo存储的金额
//假如李四转赵六4,返回的信息为:
//utxos[0x333] = int64{0, 1}
//resValue : 5
//1. 遍历账本,找到属于付款人的合适的金额,把这个outputs找到
utxos, resValue = bc.FindNeedUtxos(pubKeyHash, amount)
//2. 如果找到钱不足以转账,创建交易失败。
if resValue < amount { fmt.Printf("余额不足,交易失败!\n") return nil } var inputs []TXInput var outputs []TXOutput //3. 将outputs转成inputs for txid /*0x333*/ , indexes := range utxos { for _, i /*0, 1*/ := range indexes { input := TXInput{[]byte(txid), i, nil, publickKey} inputs = append(inputs, input) } } //4. 创建输出,创建一个属于收款人的output //output := TXOutput{amount, to} output := NewTXOutput(amount, to) outputs = append(outputs, output) //5. 如果有找零,创建属于付款人output if resValue > amount {
//output1 := TXOutput{resValue - amount, from}
output1 := NewTXOutput(resValue-amount, from)
outputs = append(outputs, output1)
}
//创建交易
tx := Transaction{nil, inputs, outputs}
//6. 设置交易id
tx.SetTXID()
//7. 返回交易结构
return &tx
}
改写GetAddress函数
func (w *WalletKeyPair) GetAddress() string {
publicHash := HashPubKey(w.PublicKey)
version := 0x00
//21字节的数据
payload := append([]byte{byte(version)}, publicHash...)
checksum := CheckSum(payload)
//25字节
payload = append(payload, checksum...)
address := base58.Encode(payload)
return address
}
func HashPubKey(pubKey []byte) []byte {
hash := sha256.Sum256(pubKey)
//创建一个hash160对象
//向hash160中write数据
//做哈希运算
rip160Haher := ripemd160.New()
_, err := rip160Haher.Write(hash[:])
if err != nil {
log.Panic(err)
}
//Sum函数会把我们的结果与Sum参数append到一起,然后返回,我们传入nil,防止数据污染
publicHash := rip160Haher.Sum(nil)
return publicHash
}
func CheckSum(payload []byte) []byte {
first := sha256.Sum256(payload)
second := sha256.Sum256(first[:])
//4字节校验码
checksum := second[0:4]
return checksum
}
改写GetBalance
func (bc *BlockChain) GetBalance(address string) {
//这个过程,不要打开钱包,因为有可能查看余额的人不是地址本人 [通过地址反推出公钥哈希即可]
decodeInfo := base58.Decode(address)
pubKeyHash := decodeInfo[1:len(decodeInfo)-4]
utxoinfos := bc.FindMyUtoxs(pubKeyHash)
var total = 0.0
//所有的output都在utxoinfos内部
//获取余额时,遍历utxoinfos获取output即可
for _, utxoinfo := range utxoinfos {
total += utxoinfo.Output.Value //10, 3, 1
}
fmt.Printf("%s 的余额为: %f\n", address, total)
}
【重要】地址有效性校验
如果不进行地址有效性校验 在查询余额转账 如果把结尾的字母改了 不能准确识别 解码后取前面字符 这也是checksum校验码的作用
在转账发送方地址 接收方地址、查询余额使用到地址的地方都应该进行地址有效性校验
func IsValidAddress(address string) bool {
//1. 将输入的地址进行解码得到25字节
//2. 取出前21个字节, 运行CheckSum函数, 得到checksum1
//3. 取出后4个字节, 德奥checksum2
//4. 比较checksum1与checksum2,如果相同则地址有效,反之无效
decodeInfo := base58.Decode(address)
if len(decodeInfo) != 25 {
return false
}
payload := decodeInfo[0:len(decodeInfo)-4]
//自己求出来的校验码
checksum1 := CheckSum(payload)
//解出来的校验码
checksum2 := decodeInfo[len(decodeInfo)-4:]
return bytes.Equal(checksum1, checksum2)
}







