【重要】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) }