btcd源碼解析——交易創建

1. 寫在前面

從本節開始,我們從源碼層面關注比特幣交易的構建過程。
其中,我們尤其會關注比特幣解鎖腳本(爲了使用UTXO)和新的鎖定腳本(爲了生成新的UTXO)的創建細節。我們相信通過跟蹤兩種腳本的創建過程,我們將對於比特幣的交易細節理解得更爲深入。

新交易的創建會涉及到兩個代碼倉庫(btcdbtcwallet)編譯生成的三個可執行文件(btcdbtcctl,和btcwallet)。
btcdbtcwallet代碼版本號如下所示:

  • btcd版本:[git commit log]: ed77733ec07dfc8a513741138419b8d9d3de9d2d
  • btcwallet版本:[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字典中註冊了處理sendtoaddresshandler,代碼如下所示:

// 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種情況。

  1. btcctl中傳送來的方法沒有在rpcHandlers中進行過註冊,則對應於L207行的返回。如博客btcd源碼解析——從“新區塊的生成”開始中所介紹的,`generate方法即對應這種情況
  2. btcctl中傳送來的方法已經在rpcHandlers中進行過註冊,且處理該方法時無需與區塊鏈鏈上數據進行交互,則對應於L192行的返回。我們這裏的sendtoaddress方法即對應這種情況。需要解釋的是,這裏說的“無需與區塊鏈鏈上數據進行交互”是指處理前期,最終將交易發送到鏈上肯定還是要交互的,但在前期生成交易的過程是不需要交互的。
  3. btcctl中傳送來的方法已經在rpcHandlers中進行過註冊,且處理該方法時需要與區塊鏈鏈上數據進行交互,則對應於L170行的返回。

4.2. 創建output

我們知道一筆交易主要可以分爲兩大部分:inputoutput。其中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函數生成outputNewTxOut函數主體如下所示:

// 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函數還是比較簡單的,這裏略去不講。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章