前言
此篇帖子主要是回馈开源,对于想要了解区块链no.1的比特币基本概念很有帮助,我也是机缘巧合之下面试来到了一家区块链公司,2周前对于区块链还只是认识3个中文字,对于概念一无所知,花了1周时间看了些资料,又在新公司CTO和一个带我飞的公司前辈的指导下,第二周我尝试开始用golang调用btcd开源项目实现一些基础的简单功能,如部署btc环境,生成公私钥、交易、离线签名等功能,一套简单的流程下来能初步的理解一些区块链的基本概念和如今市值第一高的比特币的设计思路。
声明
文末会有demo的github地址,关于代码的结构和实现逻辑是针对初学者便于理解的出发点堆砌出来的,所以公私钥存数据库什么的行为只是方便调试和查错,以及为了以后其他区块链接入的方便,主要就是为了方便学习。
先介绍2个能帮助理解概念的链接
环境安装
这里选用的是bitcoin core,点击官网下载即可,该客户端会有3种模式,本机测试选用regtest,安装成功后,客户端会主动同步所有区块文件,我看了下要200多G,显然我们是要把这个停掉的,怎么做才能切换成regtest目录呢,安装bitcoin core的时候,会有一个安装目录,因为本人是mac环境,所以在这个目录下修改bitcoin.conf配置文件就可以了,在你查看的时候可能会有疑问,为什么没有这个文件,但是你只要运行下面的指令就够了
sudo vim bitcoin.conf
//配置如下
regtest=1
server=1
rpcuser=Rennbon
rpcpassword=qwe123456
rpcport=8332
rpcconnect=127.0.0.1
txindex=1
如需详细的配置文件属性功能介绍请自行查找资料,网上很多
如上的配置完成后,在进入bitcoin客户端会发现客户端上多了regtest字样了,这个时候就好比装好了数据库,可以用数据库支持的sql来实现自己的业务需求了,比如crud。
以下是代码相关,应该不难理解,所以描述不多
证书生成
不用慌,这里直接用btcd下已经实现的最简单的公私钥生成方法,只要生成一个私钥,那么公钥和address就都有了,其中address在btc里面就是你私藏一部分比特币家当的门牌号
复杂的生成还有HD钱包的概念,这里不做介绍
func (*CertService) GenerateSimpleKey() (*Key, error) {
privKey, err := btcec.NewPrivateKey(btcec.S256())
if err != nil {
return nil, err
}
privKey.Serialize()
//这边的compress会影响到后期Pubkey address的解析方案,解析与此处的方式不一致会导致签名验证不通过
privKeyWif, err := btcutil.NewWIF(privKey, &chaincfg.RegressionNetParams, false)
if err != nil {
return nil, err
}
pubKeySerial := privKey.PubKey().SerializeUncompressed()
pubKey, err := btcutil.NewAddressPubKey(pubKeySerial, &chaincfg.RegressionNetParams)
if err != nil {
return nil, err
}
addr := pubKey.EncodeAddress()
return &Key{PrivKey: privKeyWif.String(), PubKey: pubKey.String(), Address: addr}, nil
}
导入公私钥及地址
承接上一步的Key生成,这里主要介绍如何把生成的key导入到钱包中,对于3种不同的导入会有一些差别,这个请各自去体会吧。
/*
*获取新的地址
*account:账户名
*/
func (*BtcService) GetNewAddress(account string, mode AcountRunMode) (address, accountOut string, err error) {
key, err := certSrv.GenerateSimpleKey()
if err != nil {
return "", "", err
}
if err = dhSrv.AddAccount(account, key.PrivKey, key.PubKey, key.Address); err != nil {
return "", "", err
}
switch mode {
case NoneMode:
break
case PrvMode:
account, err = btcSrv.AddPrvkeyToWallet(key.PrivKey, account)
break
case PubMode:
account, err = btcSrv.AddPubkeyToWallet(key.PubKey, account)
break
case AddrMode:
account, err = btcSrv.AddAddressToWallet(key.PubKey, account)
break
default:
break
}
if err != nil {
return "", "", nil
}
return key.Address, account, nil
}
/* 导入privatekey,这个导入能在coin.core上直接listaccounts查看到,因为有私钥了 */
func (*BtcService) AddPrvkeyToWallet(prvkey, accoutIn string) (accountOut string, err error) {
wif, err := btcutil.DecodeWIF(prvkey)
if err != nil {
return "", err
}
if err = btcSrv.client.ImportPrivKeyLabel(wif, accoutIn); err != nil {
return "", err
}
return accoutIn, nil
}
/* 将publickey对应的address添加到链中,
pubKey 公钥
account 地址自定义名称 */
func (*BtcService) AddPubkeyToWallet(pubKey, accountIn string) (accountOut string, err error) {
//验证地址是否已存在
address, err := btcSrv.CheckAddressExisted(pubKey)
if err != nil {
return "", err
}
if err = btcSrv.client.ImportPubKey(pubKey); err != nil {
return "", err
}
//修改名字 忽略错误
if err = btcSrv.client.SetAccount(address, accountIn); err != nil {
return "", nil
}
return accountIn, nil
}
/* 将publickey对应的address添加到链中,
pubKey 公钥
account 地址自定义名称 */
func (*BtcService) AddAddressToWallet(pubKey, accountIn string) (accountOut string, err error) {
//验证地址是否已存在
address, err := btcSrv.CheckAddressExisted(pubKey)
if err != nil {
return "", err
}
if err = btcSrv.client.ImportAddress(address.EncodeAddress()); err != nil {
return "", err
}
//修改名字 忽略错误
if btcSrv.client.SetAccount(address, accountIn) != nil {
return "", nil
}
return accountIn, nil
}
/* 验证publickey对应的地址是否已存在于链中
pubkey 公钥 */
func (*BtcService) CheckAddressExisted(pubKey string) (btcutil.Address, error) {
address, err := btcutil.DecodeAddress(pubKey, &chaincfg.RegressionNetParams)
addrValid, err := btcSrv.client.ValidateAddress(address)
if err != nil {
return nil, err
}
if addrValid.IsWatchOnly {
return address, errors.ERR_DATA_EXISTS
}
return address, nil
}
交易离线签名
关于离线签名,这里实现了2种方法,一种是通过SignatureScript,一种是通过SignTxOutput,我都尝试了一下,感觉还是SignatureScript更简单一点,操作都在方法内部处理了,而SignTxOutput需要在外部拼好keyDB,这里要注意到publicKey的compress值,btcd官方用的是代码中的lookupKey,更好的使用其实是下方的mkGetKey,动态的获得参数,而不是写死。
//根据address获取未花费的tx
func (*BtcService) GetUnspentByAddress(address string) (unspents []btcjson.ListUnspentResult, err error) {
btcAdd, err := btcutil.DecodeAddress(address, &chaincfg.RegressionNetParams)
if err != nil {
return nil, err
}
adds := [1]btcutil.Address{btcAdd}
unspents, err = btcSrv.client.ListUnspentMinMaxAddresses(1, 999999, adds[:])
if err != nil {
return nil, err
}
return
}
//转账
//addrForm来源地址,addrTo去向地址
//transfer 转账金额
//fee 小费
func (*BtcService) SendAddressToAddress(addrFrom, addrTo string, transfer, fee float64) error {
//数据库获取prv pub key等信息,便于调试--------START------
actf, err := dhSrv.GetAccountByAddress(addrFrom)
if err != nil {
return err
}
//----------------------------------------END-----------
unspents, err := btcSrv.GetUnspentByAddress(addrFrom)
if err != nil {
return err
}
//各种参数声明 可以构建为内部小对象
outsu := float64(0) //unspent单子相加
feesum := fee //交易费总和
totalTran := transfer + feesum //总共花费
var pkscripts [][]byte //txin签名用script
tx := wire.NewMsgTx(wire.TxVersion) //构造tx
for _, v := range unspents {
if v.Amount == 0 {
continue
}
if outsu < totalTran {
outsu += v.Amount
{
//txin输入-------start-----------------
hash, _ := chainhash.NewHashFromStr(v.TxID)
outPoint := wire.NewOutPoint(hash, v.Vout)
txIn := wire.NewTxIn(outPoint, nil, nil)
tx.AddTxIn(txIn)
//设置签名用script
txinPkScript, err := hex.DecodeString(v.ScriptPubKey)
if err != nil {
return err
}
pkscripts = append(pkscripts, txinPkScript)
}
} else {
break
}
}
//家里穷钱不够
if outsu < totalTran {
return errors.ERR_NOT_ENOUGH_COIN
}
// 输出1, 给form----------------找零-------------------
addrf, err := btcutil.DecodeAddress(addrFrom, &chaincfg.RegressionNetParams)
if err != nil {
return err
}
pkScriptf, err := txscript.PayToAddrScript(addrf)
if err != nil {
return err
}
baf := int64((outsu - totalTran) * 1e8)
tx.AddTxOut(wire.NewTxOut(baf, pkScriptf))
//输出2,给to------------------付钱-----------------
addrt, err := btcutil.DecodeAddress(addrTo, &chaincfg.RegressionNetParams)
if err != nil {
return err
}
pkScriptt, err := txscript.PayToAddrScript(addrt)
if err != nil {
return err
}
bat := int64(transfer * 1e8)
tx.AddTxOut(wire.NewTxOut(bat, pkScriptt))
//-------------------输出填充end------------------------------
err = sign(tx, actf.PrvKey, pkscripts) //签名
if err != nil {
return err
}
//广播
txHash, err := btcSrv.client.SendRawTransaction(tx, false)
if err != nil {
return err
}
//这里最好也记一下当前的block count,以便监听block count比此时高度
//大6的时候去获取当前TX是否在公链有效
dhSrv.AddTx(txHash.String(), addrFrom, []string{addrFrom, addrTo})
fmt.Println("Transaction successfully signed")
fmt.Println(txHash.String())
return nil
}
//这个方法ListAddressTransactions method not found;btcd NOTE: This is a btcwallet extension.
func (*BtcService) GetTxByAddress(addrs []string, name string) (interface{}, error) {
ct := len(addrs)
addresses := make([]btcutil.Address, 0, ct)
for _, v := range addrs {
address, err := btcutil.DecodeAddress(v, &chaincfg.RegressionNetParams)
if err != nil {
log.Println("一个废物")
} else {
addresses = append(addresses, address)
}
}
txs, err := btcSrv.client.ListAddressTransactions(addresses, name)
if err != nil {
return nil, err
}
return txs, nil
}
//验证交易是否被公链证实
//txid:交易id
func (*BtcService) CheckTxMergerStatus(txId string) error {
txHash, err := chainhash.NewHashFromStr(txId)
if err != nil {
return err
}
txResult, err := btcSrv.client.GetTransaction(txHash)
if err != nil {
return err
}
//pow共识机制当6个块确认后很难被修改
if txResult.Confirmations < 6 {
return errors.ERR_UNCONFIRMED
}
return nil
}
//签名
//privkey的compress方式需要与TxIn的
func sign(tx *wire.MsgTx, privKey string, pkScripts [][]byte) error {
wif, err := btcutil.DecodeWIF(privKey)
if err != nil {
return err
}
/* lookupKey := func(a btcutil.Address) (*btcec.PrivateKey, bool, error) {
return wif.PrivKey, false, nil
} */
for i, _ := range tx.TxIn {
script, err := txscript.SignatureScript(tx, i, pkScripts[i], txscript.SigHashAll, wif.PrivKey, false)
//script, err := txscript.SignTxOutput(&chaincfg.RegressionNetParams, tx, i, pkScripts[i], txscript.SigHashAll, txscript.KeyClosure(lookupKey), nil, nil)
if err != nil {
return err
}
tx.TxIn[i].SignatureScript = script
vm, err := txscript.NewEngine(pkScripts[i], tx, i,
txscript.StandardVerifyFlags, nil, nil, -1)
if err != nil {
return err
}
err = vm.Execute()
if err != nil {
return err
}
log.Println("Transaction successfully signed")
}
return nil
}
//离线签名signTxOut是获取keyDB使用,区分addres的compress状态
func mkGetKey(keys map[string]addressToKey) txscript.KeyDB {
if keys == nil {
return txscript.KeyClosure(func(addr btcutil.Address) (*btcec.PrivateKey, bool, error) {
return nil, false, errors.ERR_NOPE
})
}
return txscript.KeyClosure(func(addr btcutil.Address) (*btcec.PrivateKey, bool, error) {
a2k, ok := keys[addr.EncodeAddress()]
if !ok {
return nil, false, errors.ERR_NOPE
}
return a2k.key, a2k.compressed, nil
})
}
type addressToKey struct {
key *btcec.PrivateKey
compressed bool
}
demo地址
demo里有简单的单元测试及mongodb的简单使用方便调试,只是为了方便!!!
https://github.com/Rennbon/blockchainDemo.git
以后应该还会有其他区块链的尝试,希望自己能走的一步一步往上爬。
这里import的本地路径没有替换成Github路径,只是为了我自己方便。
参考相关
- http://book.8btc.com/master_bitcoin
- http://learnmeabitcoin.com
- https://bitcoin.org/en/developer-documentation
- https://blog.csdn.net/u010662978/article/details/79195284
- https://github.com/btcsuite/btcd
- https://blog.csdn.net/niyuelin1990/article/details/79897675
- https://blog.csdn.net/ffzhihua/article/details/80706122
- http://www.infoq.com/cn/articles/bitcoin-and-block-chain-part02
网友评论