文章目錄
1. 寫在前面
從本節開始,我們從源碼層面關注比特幣交易的構建過程。
其中,我們尤其會關注比特幣解鎖腳本(爲了使用UTXO
)和新的鎖定腳本(爲了生成新的UTXO
)的創建細節。我們相信通過跟蹤兩種腳本的創建過程,我們將對於比特幣的交易細節理解得更爲深入。
新交易的創建會涉及到兩個代碼倉庫(btcd
和btcwallet
)編譯生成的三個可執行文件(btcd
,btcctl
,和btcwallet
)。
btcd
和btcwallet
代碼版本號如下所示:
btcd
版本:[git commit log]: ed77733ec07dfc8a513741138419b8d9d3de9d2dbtcwallet
版本:[git commit log]: ae9416ad7623598121a7c8ad67a202c1be767155
讀者如果沒有閱讀過之前的這兩篇博客btcd源碼解析和btcd源碼解析——從“新區塊的生成”開始,強烈建議先閱讀完再來閱讀本篇博客。
2. 相關命令
本篇博客從“發送一筆交易”的命令開始敘述,相應的命令如下:
./btcctl -u seafooler -P 123456 --wallet --simnet sendtoaddress SMZtiZkgwnsjy1WQNwoCrRwEbdsT8tktWU 10
其中SMZtiZkgwnsjy1WQNwoCrRwEbdsT8tktWU
是接收方的比特幣地址,10是發送的數額。
3. 從btcctl到btcwallet
如 btcd源碼解析——從“新區塊的生成”開始中所說,btcctl
中利用MustRegisterCmd
註冊了一個sendtoaddress
方法,如下所示:
// init [walletsvrcmds.go]
func init() {
...
MustRegisterCmd("sendtoaddress", (*SendToAddressCmd)(nil), flags) // L691
...
}
該方法和相應的參數會被序列化成json
對象,然後通過sendPostRequest
函數發送到btcwallet
端處理,如下代碼所示:
// 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
,代碼如下所示:
// rpcHandlers [methods.go]
var rpcHandlers = map[string]struct {
handler requestHandler
handlerWithChain requestHandlerChainRequired
...
noHelp bool
} {
...
"sendtoaddress": {handler: sendToAddress}, // L104
...
}
4. btcwallet中的實現——創建新交易
sendToAddress
函數主體如下所示:
// 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)
}
其中,L1501首先將btcctl
發送來的json
數據轉換爲SendToAddressCmd
類型的變量,然後依次構建出轉賬數額amt
(L1512)和轉賬map
(L1523), 最後調用sendPairs
函數。
4.1. wallet變量的傳入
需要注意的是,sendPairs
中傳入了wallet
變量w
,這是由lazyApplyHandler
函數調用sendToAddress
這個handler
時傳入的。而lazyApplyHandler
函數中的w
又是由handlerClosure
函數中調用該函數時傳入的,代碼主體如下所示:
// handlerClosure [server.go]
func (s *Server) handlerClosure(request *btcjson.Request) lazyHandler {
...
wallet := s.wallet
...
return lazyApplyHandler(request, wallet, chainClient)
}
// 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行的返回。
4.2. 創建output
我們知道一筆交易主要可以分爲兩大部分:input
和output
。其中output
是相對比較簡單的,所以我們在這篇博客中先介紹output
, input
的構建細節將交由下一篇博客進行介紹。
回到前面的sendPairs
函數,主體代碼如下所示。其中L1377行即調用makeOutputs
函數生成output
。
// 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
函數代碼如下所示:
// 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
}
4.2.1. 將字符串解碼爲地址
makeOutputs
函數中的L1356行利用DecodeAddress
函數將addrStr
解碼爲Address
類型。DecodeAddress
函數主體代碼如下所示:
// 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。
關於這兩種形式,筆者也是強烈建議讀者先閱讀博客關於比特幣地址的一些問題和解答,其中詳細介紹了比特幣地址九種形式的相互轉化關係。
這裏爲了大家更加直觀地理解,將該博客中的插圖複製如下:
4.2.1.1 ECDSA public key 格式
若字符串的長度是130或者66,表明這是個ECDSA public key
格式的地址。
在L178行通過調用NewAddressPubKey
函數生成PubKey
格式的地址。
4.2.1.2 Common Bitcoin addr 格式
這個格式是我們最常見的地址,在比特幣主網上以1或3開頭,長度約爲34位。
該格式的地址首先通過base58.CheckDecode
函數轉變爲RIPEMD-160 hash value
格式的地址 (decoded
),並返回相應的地址類型 (netID
),如L182所示。這裏的地址類型是指PubKeyHash
(P2PKH
) 和ScriptHash
(P2SH
)兩種類型。
對應於兩種類型,分別生成兩種地址,如L197和L199所示。
4.2.2. 構建鎖定腳本
回到makeOutputs
函數。基於解碼後生成的地址,調用PayToAddrScript
函數即可構建鎖定腳本了,如L1361所示。
PayToAddrScript
函數的主題代碼如下所示。
// 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
函數的主體代碼如下所示:
// 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 OP_EQUALVERIFY OP_CHECKSIG
4.2.3. 構建output
再次回到makeOutputs
函數。
簡單理解: output = amount + script
.
有了4.2.2節構建的script
,就很容易構建出output
了,代碼如L1366所示。
L1366主要通過調用wire.NewTxOut
函數生成output
,NewTxOut
函數主體如下所示:
// 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
函數還是比較簡單的,這裏略去不講。