btcd
是一个用Go语言(golang)编写的比特币全节点替代实现。btcsuite
是一个Go语言的 btc
库集合,我们可以使用它来构建比特币交易。
1. 环境准备
要发送 btc
交易,首先我们需要能访问到 btc
网络。这里以测试链为例,使用 docker 来启动一个 btcd
全节点。docker-compose.yml
文件如下:
networks:
btcd:
services:
btcd:
image: mengbin92/btcd:0.24.2
container_name: btcd_full_node
volumes:
- ./btcd:/root/.btcd
ports:
- 8334:8334
networks:
- btcd
btcd
的配置文件在 btcd
目录下,btcd.conf
如下:
; 构建并维护一个完整的基于哈希的交易索引,使所有交易都可以通过 getrawtransaction RPC 获得。
txindex=1
; 构建和维护基于地址的完整交易索引,使 searchrawtransactions RPC 可用。
addrindex=1
# for rpcserver
rpcuser=rpcuser
rpcpass=rpcpassword
rpclisten=0.0.0.0:8334
# 连接测试链
testnet=1
之后通过 docker-compose up -d
启动 btcd
全节点。
2. 初始化 RPC 客户端
func loadRPCClient() *rpcclient.Client {
viper.SetConfigFile("./config/config.yaml")
err := viper.ReadInConfig()
if err != nil {
panic(err)
}
rpcuser = viper.GetString("btc.rpcuser")
rpcpass = viper.GetString("btc.rpcpass")
endpoint = viper.GetString("btc.endpoint")
rpccert = viper.GetString("btc.rpccert")
// 使用tls链接,所以需要导入btcd生成的rpc证书
cert, err := os.ReadFile(rpccert)
if err != nil {
panic(err)
}
connCfg := &rpcclient.ConnConfig{
Host: endpoint,
User: rpcuser,
Pass: rpcpass,
HTTPPostMode: true,
Certificates: cert,
}
client, err = rpcclient.New(connCfg, nil)
if err != nil {
panic(err)
}
return client
}
配置文件内容如下:
version: "3.8"
btc:
rpcuser: rpcuser
rpcpass: rpcpass
rpccert: ./btcd/rpc.cert
endpoint: 127.0.0.1:8334
3. 构建交易输出
这里以向地址 tb1qndsh2mllf8g2hf29svazpxksa3ns4zga3n55mc
转账 100000 sat
为例,现在我们需要构建交易输出:
// BuildTxOut 构建一个比特币交易输出(TxOut)
func BuildTxOut(addr string, amount int64, params chaincfg.Params) (*wire.TxOut, []byte, error) {
// 解析比特币地址
destinationAddress, err := btcutil.DecodeAddress(addr, ¶ms)
if err != nil {
return nil, nil, err
}
// 生成支付到地址的脚本
pkScript, err := txscript.PayToAddrScript(destinationAddress)
if err != nil {
return nil, nil, err
}
// 创建一个新的交易输出,金额单位为 satoshis
return wire.NewTxOut(amount, pkScript), pkScript, nil
}
4. 获取发送者的余额
这里通过 SearchRawTransactionsVerbose
获取指定地址相关的交易,然后再通过 GetTxOut
获取发送者的余额。
// GetUTXOs 获取指定比特币地址的所有未花费交易输出(UTXOs)
func GetUTXOs(addr string) ([]*btcjson.ListUnspentResult, error) {
// 解析比特币地址
address, err := btcutil.DecodeAddress(addr, &chaincfg.TestNet3Params)
if err != nil {
return nil, err
}
// 使用SearchRawTransactionsVerbose获取与地址相关的所有交易
transactions, err := client.SearchRawTransactionsVerbose(address, 0, 100, true, false, nil)
if err != nil {
return nil, err
}
// 用于存储UTXO的切片
utxos := []*btcjson.ListUnspentResult{}
// 遍历所有交易
for _, tx := range transactions {
// 将交易ID字符串转换为链哈希对象
txid, err := chainhash.NewHashFromStr(tx.Txid)
if err != nil {
log.Fatalf("Invalid txid: %v", err)
}
// 遍历交易的输出
for _, vout := range tx.Vout {
// 检查输出地址是否是我们关心的地址
if vout.ScriptPubKey.Address != addr {
continue
}
// 使用GetTxOut方法获取交易输出,确认该输出是否未花费
utxo, err := client.GetTxOut(txid, vout.N, true)
if err != nil {
panic(err)
}
// 如果交易输出未花费,则将其添加到UTXO切片中
if utxo != nil {
utxo := &btcjson.ListUnspentResult{
TxID: tx.Txid,
Vout: uint32(vout.N),
Address: addr,
ScriptPubKey: vout.ScriptPubKey.Hex,
Amount: vout.Value, // 单位为BTC
Confirmations: int64(tx.Confirmations),
Spendable: true,
}
utxos = append(utxos, utxo)
}
}
}
// 返回UTXO集合
return utxos, nil
}
5. 构建交易输入
现在我们已经拿到了发送者的余额,接下来需要构建交易输入。
func BuildTxIn(wif *btcutil.WIF, amount int64, txOut *wire.TxOut, params *chaincfg.Params) (*wire.MsgTx, error) {
// 解析比特币地址
fromAddr, err := btcutil.NewAddressWitnessPubKeyHash(btcutil.Hash160(wif.SerializePubKey()), params)
if err != nil {
return nil, errors.Wrap(err, "解析比特币地址失败")
}
// 获取UTXOs
utxos, err := GetUTXOs(fromAddr.EncodeAddress())
if err != nil {
return nil, errors.Wrap(err, "获取UTXOs失败")
}
msgTx := wire.NewMsgTx(wire.TxVersion)
// 创建一个新的交易输入,金额单位为 satoshis
totalInput := int64(0)
for _, utxo := range utxos {
// totalInput 大于 amount,用于计算交易费
if totalInput > amount {
break
}
txHash, err := chainhash.NewHashFromStr(utxo.TxID)
if err != nil {
return nil, errors.Wrap(err, "解析交易哈希失败")
}
txIn := wire.NewTxIn(&wire.OutPoint{Hash: *txHash, Index: uint32(utxo.Vout)}, nil, nil)
msgTx.AddTxIn(txIn)
totalInput += int64(utxo.Amount * 1e8)
}
msgTx.AddTxOut(txOut)
// 交易费
// 假定交易费率为每字节 1sat
fee := int64(msgTx.SerializeSize())
// 找零
change := totalInput - amount
// 这里假定找零一定大于交易费,交易费太少的话可能导致交易一直无法确认
// 如果change <= fee的话,零钱会转给出块的矿工
if change > fee {
changePkScript, err := txscript.PayToAddrScript(fromAddr)
if err != nil {
return nil, errors.Wrap(err, "生成找零地址的脚本失败")
}
txOut := wire.NewTxOut(change-fee, changePkScript)
msgTx.AddTxOut(txOut)
}
// 签署交易
// 发送方地址为SegWit的P2WPKH 地址,所以要消费该地址的UTXO,只能通过见证输入进行消费
for i, txIn := range msgTx.TxIn {
prevOutputScript, err := hex.DecodeString(utxos[i].ScriptPubKey)
if err != nil {
panic(err)
}
txHash, err := chainhash.NewHashFromStr(utxos[i].TxID)
if err != nil {
return nil, errors.Wrap(err, "解析交易哈希失败")
}
outPoint := wire.OutPoint{Hash: *txHash, Index: uint32(utxos[i].Vout)}
prevOutputFetcher := txscript.NewMultiPrevOutFetcher(map[wire.OutPoint]*wire.TxOut{
outPoint: {Value: int64(utxos[i].Amount * 1e8), PkScript: prevOutputScript},
})
sigHashes := txscript.NewTxSigHashes(msgTx, prevOutputFetcher)
sigScript, err := txscript.WitnessSignature(msgTx, sigHashes, int(utxos[i].Vout), int64(utxos[i].Amount*1e8), prevOutputScript, txscript.SigHashAll, wif.PrivKey, true)
if err != nil {
return nil, errors.Wrap(err, "签名交易失败")
}
txIn.Witness = sigScript
}
return msgTx, nil
}
至此,我们就已经完成了交易的构建过程,接下来就是将交易广播到区块链网络,等待确认。
6. 广播交易
广播交易可以使用 SendRawTransaction
,函数会将交易提交到服务器,然后服务器将其转发到网络。该函数会返回交易哈希,如果交易成功广播,那么哈希值会是一个有效的哈希值。
声明:本作品采用署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)进行许可,使用时请注明出处。
Author: mengbin
blog: mengbin
Github: mengbin92
cnblogs: 恋水无意
腾讯云开发者社区:孟斯特