从本节开始,我们从源码层面关注比特币交易的构建过程。 其中,我们尤其会关注比特币解锁脚本(为了使用UTXO
)和新的锁定脚本(为了生成新的UTXO
)的创建细节。我们相信通过跟踪两种脚本的创建过程,我们将对于比特币的交易细节理解得更为深入。
新交易的创建会涉及到两个代码仓库(btcd
和btcwallet
)编译生成的三个可执行文件(btcd
,btcctl
,和btcwallet
)。 btcd
和btcwallet
代码版本号如下所示:
btcd
版本:[git commit log]: ed77733ec07dfc8a513741138419b8d9d3de9d2dbtcwallet
版本:[git commit log]: ae9416ad7623598121a7c8ad67a202c1be767155
读者如果没有阅读过之前的这两篇博客btcd源码解析和btcd源码解析——从“新区块的生成”开始,强烈建议先阅读完再来阅读本篇博客。
I. 相关命令
本篇博客从“发送一笔交易”的命令开始叙述,相应的命令如下: 1
./btcctl -u seafooler -P 123456 --wallet --simnet sendtoaddress SMZtiZkgwnsjy1WQNwoCrRwEbdsT8tktWU 10
SMZtiZkgwnsjy1WQNwoCrRwEbdsT8tktWU
是接收方的比特币地址,10是发送的数额。
II. 从btcctl到btcwallet
如 btcd源码解析——从“新区块的生成”开始中所说,btcctl
中利用MustRegisterCmd
注册了一个sendtoaddress
方法,如下所示: 1
2
3
4
5
6// init [walletsvrcmds.go]
func init() {
...
MustRegisterCmd("sendtoaddress", (*SendToAddressCmd)(nil), flags) // L691
...
}json
对象,然后通过sendPostRequest
函数发送到btcwallet
端处理,如下代码所示: 1
2
3
4
5
6
7
8
9// main [btcctl.go]
func main() { // L49
...
cmd, err := btcjson.NewCmd(method, params...) // L107
...
marshalledJSON, err := btcjson.MarshalCmd(1, cmd) // L130
...
result, err := sendPostRequest(marshalledJSON, cfg) // L138
...btcwallet
代码中的rpcHandlers
字典中注册了处理sendtoaddress
的handler
,代码如下所示: 1
2
3
4
5
6
7
8
9
10
11// rpcHandlers [methods.go]
var rpcHandlers = map[string]struct {
handler requestHandler
handlerWithChain requestHandlerChainRequired
...
noHelp bool
} {
...
"sendtoaddress": {handler: sendToAddress}, // L104
...
}
III. btcwallet中的实现——创建新交易
sendToAddress
函数主体如下所示: 1
2
3
4
5
6
7
8
9
10
11
12
13// sendToAddress [methods.go]
func sendToAddress(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
cmd := icmd.(*btcjson.SendToAddressCmd) // L1501
...
amt, err := btcutil.NewAmount(cmd.Amount) // L1512
...
pairs := map[string]btcutil.Amount{ // L1523
cmd.Address: amt,
}
...
return sendPairs(w, pairs, waddrmgr.DefaultAccountNum, 1, // L1528
txrules.DefaultRelayFeePerKb)
}btcctl
发送来的json
数据转换为SendToAddressCmd
类型的变量,然后依次构建出转账数额amt
(L1512)和转账map
(L1523), 最后调用sendPairs
函数。
A. wallet变量的传入
需要注意的是,sendPairs
中传入了wallet
变量w
,这是由lazyApplyHandler
函数调用sendToAddress
这个handler
时传入的。而lazyApplyHandler
函数中的w
又是由handlerClosure
函数中调用该函数时传入的,代码主体如下所示: 1
2
3
4
5
6
7// handlerClosure [server.go]
func (s *Server) handlerClosure(request *btcjson.Request) lazyHandler {
...
wallet := s.wallet
...
return lazyApplyHandler(request, wallet, chainClient)
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// handlerClosure [server.go] -> lazyApplyHandler [methods.go]
func lazyApplyHandler(request *btcjson.Request, w *wallet.Wallet, chainClient
chain.Interface) lazyHandler {
handlerData, ok := rpcHandlers[request.Method] // L169
if ok && handlerData.handlerWithChain != nil && w != nil && chainClient != nil {// L170
return func() (interface{}, *btcjson.RPCError) {
...
}
}
if ok && handlerData.handler != nil && w != nil { // L192
return func() (interface{}, *btcjson.RPCError)
...
resp, err := handlerData.handler(cmd, w) // L198
...
}
}
...
return func() (interface{}, *btcjson.RPCError) { // L207
...
}
}lazyApplyHandler
中有三个return
语句,分别对应于三种不同的request
情况,如下所示。我们的sendtoaddress
方法对应第2种情况。
- 若
btcctl
中传送来的方法没有在rpcHandlers
中进行过注册,则对应于L207行的返回。如博客btcd源码解析——从“新区块的生成”开始中所介绍的,generate
方法即对应这种情况 btcctl
中传送来的方法已经在rpcHandlers
中进行过注册,且处理该方法时无需与区块链链上数据进行交互,则对应于L192行的返回。我们这里的sendtoaddress
方法即对应这种情况。需要解释的是,这里说的“无需与区块链链上数据进行交互”是指处理前期,最终将交易发送到链上肯定还是要交互的,但在前期生成交易的过程是不需要交互的。btcctl
中传送来的方法已经在rpcHandlers
中进行过注册,且处理该方法时需要与区块链链上数据进行交互,则对应于L170行的返回。
B. 创建output
我们知道一笔交易主要可以分为两大部分:input
和output
。其中output
是相对比较简单的,所以我们在这篇博客中先介绍output
, input
的构建细节将交由下一篇博客进行介绍。 回到前面的sendPairs
函数,主体代码如下所示。其中L1377行即调用makeOutputs
函数生成output
。 1
2
3
4
5
6
7
8// sendToAddress [methods.go] -> sendPairs [methods.go]
func sendPairs(w *wallet.Wallet, amounts map[string]btcutil.Amount, account uint32,
minconf int32, feeSatPerKb btcutil.Amount) (string, error) {
outputs, err := makeOutputs(amounts, w.ChainParams()) // L1377
...
tx, err := w.SendOutputs(outputs, account, minconf, feeSatPerKb) // L1381
...
}makeOutputs
函数代码如下所示: 1
2
3
4
5
6
7
8
9
10
11
12
13// sendToAddress [methods.go] -> sendPairs [methods.go] -> makeOutputs [methods.go]
func makeOutputs(pairs map[string]btcutil.Amount, chainParams *chaincfg.Params)
([]*wire.TxOut, error) {
outputs := make([]*wire.TxOut, 0, len(pairs))
for addrStr, amt := range pairs {
addr, err := btcutil.DecodeAddress(addrStr, chainParams) // L1356
...
pkScript, err := txscript.PayToAddrScript(addr) // L1361
...
outputs = append(outputs, wire.NewTxOut(int64(amt), pkScript)) // L1366
}
return outputs, nil
}
1. 将字符串解码为地址
makeOutputs
函数中的L1356行利用DecodeAddress
函数将addrStr
解码为Address
类型。DecodeAddress
函数主体代码如下所示: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31// sendToAddress [methods.go] -> sendPairs [methods.go] -> makeOutputs [methods.go]
// -> DecodeAddress [address.go]
func DecodeAddress(addr string, defaultNet *chaincfg.Params) (Address, error) {
...
oneIndex := strings.LastIndexByte(addr, '1') // L142
if oneIndex > 1 {
...
} // L169
...
if len(addr) == 130 || len(addr) == 66 { // L173
serializedPubKey, err := hex.DecodeString(addr) // L174
...
return NewAddressPubKey(serializedPubKey, defaultNet) // L178
} // L179
...
decoded, netID, err := base58.CheckDecode(addr) // L182
...
switch len(decoded) { // L189
case ripemd160.Size: // L190
isP2PKH := netID == defaultNet.PubKeyHashAddrID
isP2SH := netID == defaultNet.ScriptHashAddrID
switch hash160 := decoded; {
...
case isP2PKH:
return newAddressPubKeyHash(hash160, netID) // L197
case isP2SH:
return newAddressScriptHashFromHash(hash160, netID) // L199
...
}
...
}DecodeAddress
函数在解码地址的过程中,分为几种不同的情况。由于L142到L169主要是对Bech32
格式的地址进行解码,笔者暂时对Bech32
格式理解得还不到位,这里先略过不讲。先介绍其他几种情况。 总的来说,在sendtoaddress
命令中可以接受作为地址的字符串包括以下两种形式:1)ECDSA public key;2)Common Bitcoin addr。 关于这两种形式,笔者也是强烈建议读者先阅读博客关于比特币地址的一些问题和解答,其中详细介绍了比特币地址九种形式的相互转化关系。 这里为了大家更加直观地理解,将该博客中的插图复制如下:
1) ECDSA public key 格式
若字符串的长度是130或者66,表明这是个ECDSA public key
格式的地址。 在L178行通过调用NewAddressPubKey
函数生成PubKey
格式的地址。
2) Common Bitcoin addr 格式
这个格式是我们最常见的地址,在比特币主网上以1或3开头,长度约为34位。 该格式的地址首先通过base58.CheckDecode
函数转变为RIPEMD-160 hash value
格式的地址 (decoded
),并返回相应的地址类型 (netID
),如L182所示。这里的地址类型是指PubKeyHash
(P2PKH
) 和ScriptHash
(P2SH
)两种类型。 对应于两种类型,分别生成两种地址,如L197和L199所示。
2. 构建锁定脚本
回到makeOutputs
函数。基于解码后生成的地址,调用PayToAddrScript
函数即可构建锁定脚本了,如L1361所示。 PayToAddrScript
函数的主题代码如下所示。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// sendToAddress [methods.go] -> sendPairs [methods.go] -> makeOutputs [methods.go]
// -> PayToAddrScript [standard.go]
func PayToAddrScript(addr btcutil.Address) ([]byte, error) {
...
switch addr := addr.(type) {
case *btcutil.AddressPubKeyHash: // L426
...
return payToPubKeyHashScript(addr.ScriptAddress()) // L431
case *btcutil.AddressScriptHash:
...
return payToScriptHashScript(addr.ScriptAddress())
case *btcutil.AddressPubKey:
...
return payToPubKeyScript(addr.ScriptAddress())
...
}PayToAddrScript
函数的逻辑还是比较简单的。对应于不同格式的地址,采用不同的函数生成锁定脚本。我们以AddressPubKeyHash
(也即P2PKH
)为例,进行简单介绍。 L431行的ScriptAddress
函数只是简单地返回addr
中的字节切片,并作为参数传入payToPubKeyHashScript
函数中。 payToPubKeyHashScript
函数的主体代码如下所示: 1
2
3
4
5
6
7// sendToAddress [methods.go] -> sendPairs [methods.go] -> makeOutputs [methods.go]
// -> PayToAddrScript [standard.go] -> payToPubKeyHashScript
func payToPubKeyHashScript(pubKeyHash []byte) ([]byte, error) {
return NewScriptBuilder().AddOp(OP_DUP).AddOp(OP_HASH160).
AddData(pubKeyHash).AddOp(OP_EQUALVERIFY).AddOp(OP_CHECKSIG).
Script()
}P2PKH script
的格式: > OP_DUP OP_HASH160
3. 构建output
再次回到makeOutputs
函数。 简单理解: output = amount + script
. 有了III.B.2节构建的script
,就很容易构建出output
了,代码如L1366所示。 L1366主要通过调用wire.NewTxOut
函数生成output
,NewTxOut
函数主体如下所示: 1
2
3
4
5
6
7
8// sendToAddress [methods.go] -> sendPairs [methods.go] -> makeOutputs [methods.go]
// -> NewTxOut [msgtx.go]
func NewTxOut(value int64, pkScript []byte) *TxOut {
return &TxOut{
Value: value,
PkScript: pkScript,
}
}NewTxOut
函数还是比较简单的,这里略去不讲。