btcd源码解析——交易创建 (3) —— 构造解锁脚本

5. 构造解锁脚本

上一篇博客的最后,我们讲到
AddAllInputScripts函数体中针对每一个非witnessinput调用了SignTxOutput函数。该函数主要用来对当前input构造解锁脚本。
解锁脚本构造的最关键部分在于签名的构造,因而该函数中是用了Sign这个词。
此外,SignTxOutput函数名中虽然写的是output,但这个outputUTXO的意思,也就是当前交易的input

在继续本篇博客的阅读之前,强烈建议读者先阅读关于签名机制的三篇博客签名机制(1) —— 基础知识, 签名机制(2) ——签名流程签名机制(3) —— 源码分析,其中介绍了各种签名哈希类型下message的内容。

5.1. SignTxOutput函数

SignTxOutput函数的主题内容如下所示:

// SignTxOutput [sign.go]
func SignTxOutput(chainParams *chaincfg.Params, tx *wire.MsgTx, idx int,      
    pkScript []byte, hashType SigHashType, kdb KeyDB, sdb ScriptDB,      
    previousScript []byte) ([]byte, error) {
    
    sigScript, class, addresses, nrequired, err := sign(chainParams, tx,             // L437
        idx, pkScript, hashType, kdb, sdb)
    ...
    if class == ScriptHashTy {                                                       // L443
        // TODO keep the sub addressed and pass down to merge.      
        realSigScript, _, _, _, err := sign(chainParams, tx, idx,                    // L445
            sigScript, hashType, kdb, sdb)      
        ...
        builder := NewScriptBuilder()                                                // L452
        builder.AddOps(realSigScript)      
        builder.AddData(sigScript)                                                   // L454
        
        sigScript, _ = builder.Script()     
    }
    
    mergedScript := mergeScripts(chainParams, tx, idx, pkScript, class,              // L461
        addresses, nrequired, sigScript, previousScript)
    return mergedScript, nil
}

从代码中容易看出,针对脚本类型的不同,解锁脚本的构造分为两大类:1)ScriptHashTy类型;2)其他类。
其他类调用了sign函数一次(L437行),scripthash类则调用了sign函数两次(L437行和L445行)。
此外,L461行调用mergeScripts函数将得到的sigScriptpreviousScript进行合并。老实说,该函数的实现我没看懂(/捂脸哭)。看了一圈注释,好像也没写清楚,而且这个previousScript貌似一般情况下为空值。因此,这里我们就不对mergeScripts函数展开讲解了。

以下分5.2和5.3两小节分别介绍其他类中的PubKeyHashTy类型和ScriptHashTy类型。

5.2. PubKeyHashTy类型的解锁脚本

SignTxOutput中的L437行代码所示,对于PubKeyHashTy类型的脚本,主要通过调用sign函数构造解锁脚本。
sign函数的主体内容如下所示:

// SignTxOutput [sign.go] -> sign
func sign(chainParams *chaincfg.Params, tx *wire.MsgTx, idx int, subScript []byte, 
	hashType SigHashType, kdb KeyDB, sdb ScriptDB) ([]byte, ScriptClass, 
	[]btcutil.Address, int, error) {
    
    class, addresses, nrequired, err := ExtractPkScriptAddrs(subScript,                     // L160
        chainParams)
    ...    
    switch class {
    case PubKeyTy:
        ...
    case PubKeyHashTy:                                                                      // L181
        // look up key for address
        key, compressed, err := kdb.GetKey(addresses[0])                                    // L183
        ...
        script, err := SignatureScript(tx, idx, subScript, hashType,                        // L188
            key, compressed)
        ...
        return script, class, addresses, nrequired, nil
    case ScriptHashTy:                                                                       // L195
        script, err := sdb.GetScript(addresses[0])                                           // L196
        ...
        return script, class, addresses, nrequired, nil                                      // L201
    ...
    }
}

sign首先在L160行调用ExtractPkScriptAddrs函数对锁定脚本(subScript)进行解析,得到脚本类型(class)和地址(addresses).
然后根据脚本类型,进行不同方式的处理。本小节,我们主要关注L181行的PubKeyHashTy类型。
对于该类型,首先在L183行调用GetKey函数获取到本地数据库保存的私钥(key),然后在L188行调用SignatureScript函数构造解锁脚本。
以下分别介绍ExtractPkScriptAddrs函数和SignatureScript函数。

5.2.1. ExtractPkScriptAddrs函数

ExtractPkScriptAddrs函数的主体内容如下所示:

// SignTxOutput [sign.go] -> sign -> ExtractPkScriptAddrs [standard.go]
func ExtractPkScriptAddrs(pkScript []byte, chainParams *chaincfg.Params) 
    (ScriptClass, []btcutil.Address, int, error) {
    ...
    pops, err := parseScript(pkScript)                                                  // L530
    ...
    scriptClass := typeOfScript(pops)                                                  // L535
    switch scriptClass {
    case PubKeyHashTy:
        ...
        addr, err := btcutil.NewAddressPubKeyHash(pops[2].data,             			// L543
            chainParams)
        ...
    ...
    case ScriptHashTy:
        ...
        addr, err := btcutil.NewAddressScriptHashFromHash(pops[1].data,      			// L555
            chainParams)
        ...
    ...
    }
    
    return scriptClass, addrs, requiredSigs, nil 
}

由于锁定脚本pkScript是一个字节码切片,首先在L530行调用parseScript函数将字节码切片解析成操作码切片(pops)。
基于pops,可以调用typeOfScript函数判断出锁定脚本的类型 (L535行)。
针对PubKeyHashTy类型的脚本,调用NewAddressPubKeyHash函数生成所需的地址(addr).
以下分别介绍typeOfScript函数和NewAddressPubKeyHash函数

5.2.1.1. typeOfScript函数

typeOfScript函数主要是一些判断语句,如下所示:

// SignTxOutput [sign.go] -> sign -> ExtractPkScriptAddrs [standard.go] -> typeOfScript
func typeOfScript(pops []parsedOpcode) ScriptClass {
    if isPubkey(pops) {      
        return PubKeyTy
    } else if isPubkeyHash(pops) {      
        return PubKeyHashTy
    } else if isWitnessPubKeyHash(pops) {      
        return WitnessV0PubKeyHashTy
    } else if isScriptHash(pops) {
        return ScriptHashTy
    } ...
    return NonStandardTy
}

其中各种判别函数(如isPubkeyisPubkeyHash等)主要是对其中的特定操作码进行判断。以isPubkeyHash函数为例,其主体代码如下所示:

// SignTxOutput [sign.go] -> sign -> ExtractPkScriptAddrs [standard.go] -> typeOfScript -> isPubkeyHash
func isPubkeyHash(pops []parsedOpcode) bool {      
    return len(pops) == 5 &&            
        pops[0].opcode.value == OP_DUP &&            
        pops[1].opcode.value == OP_HASH160 &&            
        pops[2].opcode.value == OP_DATA_20 &&            
        pops[3].opcode.value == OP_EQUALVERIFY &&            
        pops[4].opcode.value == OP_CHECKSIG
}
5.2.1.2. NewAddressPubKeyHash函数

NewAddressPubKeyHash函数如下所示。

// SignTxOutput [sign.go] -> sign -> ExtractPkScriptAddrs [standard.go] -> 
// NewAddressPubKeyHash [address.go]
func NewAddressPubKeyHash(pkHash []byte, net *chaincfg.Params) (*AddressPubKeyHash, error) {      
    return newAddressPubKeyHash(pkHash, net.PubKeyHashAddrID)
}

可见NewAddressPubKeyHash函数主要是调用了newAddressPubKeyHash函数,后者的主体如下:

// SignTxOutput [sign.go] -> sign -> ExtractPkScriptAddrs [standard.go] -> 
// NewAddressPubKeyHash [address.go] -> newAddressPubKeyHash
func newAddressPubKeyHash(pkHash []byte, netID byte) 
(*AddressPubKeyHash, error) {
    if len(pkHash) != ripemd160.Size {      
        return nil, errors.New("pkHash must be 20 bytes")
    }

    addr := &AddressPubKeyHash{netID: netID}
    copy(addr.hash[:], pkHash)
    return addr, nil

newAddressPubKeyHash函数也比较简单,主要是对锁定脚本中的hash值做了一些判断和封装。

5.2.2. SignatureScript函数

回到5.2小节sign函数的L188行。其主要是调用了SignatureScript函数针对当前的input构造解锁脚本。
SignatureScript函数主体如下所示:

// SignTxOutput [sign.go] -> sign -> SignatureScript
func SignatureScript(tx *wire.MsgTx, idx int, subscript []byte, hashType 
    SigHashType, privKey *btcec.PrivateKey, compress bool) ([]byte, error) {
    sig, err := RawTxInSignature(tx, idx, subscript, hashType, privKey)             	// L98
    ...
    pk := (*btcec.PublicKey)(&privKey.PublicKey)                                        // L103
    var pkData []byte
    if compress {      
        pkData = pk.SerializeCompressed()
    } else {      
        pkData = pk.SerializeUncompressed()
    }                                                                                   // L109
    
    return NewScriptBuilder().AddData(sig).AddData(pkData).Script()             		// L111
}

SignatureScript在L98行调用RawTxInSignature函数对当前input进行签名,得到签名数据(sig)。
在L103行到L109行构建公钥数据(pkData).
L111行将签名数据和公钥数据相结合,即得到解锁脚本,其与我们熟悉的解锁脚本形式(如下所示)是完全一致的。

<sig> <public key>

以下我们来关注RawTxInSignature函数

5.2.2.1. RawTxInSignature函数
// SignTxOutput [sign.go] -> sign -> SignatureScript -> RawTxInSignature
func RawTxInSignature(tx *wire.MsgTx, idx int, subScript []byte,      
    hashType SigHashType, key *btcec.PrivateKey) ([]byte, error) {

    hash, err := CalcSignatureHash(subScript, hashType, tx, idx)                     	// L77
    ...
    signature, err := key.Sign(hash)                                                   // L81
    ...
    return append(signature.Serialize(), byte(hashType)), nil                         // L86
}

我们知道,构建签名的过程一般包含两个阶段:1)构建用于签名的消息(message);2)使用私钥对该消息进行签名。
L77行即调用CalcSignatureHash来生成message
CalcSignatureHash函数内部主要调用了calcSignatureHash函数,而calcSignatureHash函数是比较复杂的。
如本篇博客开篇所述,笔者已经写了三篇博客来介绍这个message的构建过程,其中第三篇博客签名机制(3) —— 源码分析便详细分析了calcSignatureHash函数的实现细节。感兴趣的读者,可以去看看。
没有时间去看这几篇博客的读者,只需记住CalcSignatureHashcalcSignatureHash函数的功能:即生成签名所需的message.

L81行调用Sign函数对该message进行签名,得到签名结果signature

L86行将signature序列化并添加上签名哈希类型返回,其中签名哈希类型也在开篇推荐的三篇博客中予以了介绍。

5.3. ScriptHashTy类型的解锁脚本

在5.2小节中,我们介绍了PubKeyHashTy类型解锁脚本的构造过程。本小节,我们介绍ScriptHashTy类型解锁脚本的构造。
回顾5.1小节中的SignTxOutput函数,当处理PubKeyHashTy类型时,只执行了L437行的sign调用;而这里处理ScriptHashTy类型时,除了执行L437行的sign调用,还需要执行L445行的sign调用。

5.3.1. 第一次执行sign函数

sign函数主体如下所示:

// SignTxOutput [sign.go] -> sign
func sign(chainParams *chaincfg.Params, tx *wire.MsgTx, idx int, subScript []byte, 
	hashType SigHashType, kdb KeyDB, sdb ScriptDB) ([]byte, ScriptClass, []btcutil.Address, int, error) {
    
    class, addresses, nrequired, err := ExtractPkScriptAddrs(subScript,                     // L160
        chainParams)
    ...    
    switch class {
    ...
    case ScriptHashTy:                                                                       // L195
        script, err := sdb.GetScript(addresses[0])                                           // L196
        ...
        return script, class, addresses, nrequired, nil                                      // L201
    ...
    }
}

L160行的ExtractPkScriptAddrs函数已经在上一篇博客中进行过介绍。在该函数中,ScriptHashTy类型的处理流程和PubKeyHashTy类型相似,这里略过。

L195行至L201行对ScriptHashTy类型进行处理,该处的处理流程非常简单。L196行调用GetScript函数从本地数据库中读取地址所对应的脚本 (script).

此处,我们需要岔开一笔来介绍一下ScriptHashTy类型的脚本格式。
ScriptHashTy类型的锁定脚本(locking script)和解锁脚本(unlocking script)格式分别如下所示:

Locking script: HASH160 <redeem script hash> EQUALVERIFY

Unlocking script: <sigscript> <redeem script>

L196行读取的script即为unlocking script中的redeem script.
获得该script后,再对该script进行签名,因而需要重新调用一次sign函数。
因此,在sign函数中处理ScriptHashTy类型时,采取了直接返回该script的方式。

5.3.2. 第二次执行sign函数

回顾5.1小节中的SignTxOutput函数,其L445行对5.3.1.小节查询得到的script(也即返回值sigScript)进行签名。
此时的script可以看作是普通的其他类解锁脚本 (如PubKeyHashTy类型),因而在L445行进入到sign函数后,完全按照5.2小节的流程进行。

签名得到realSigScript脚本后,在SignTxOutput函数的L452至L456对realSigScript脚本和5.3.1.小节得到的sigScript进行拼接,最后得到unlocking script格式的解锁脚本。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值