文章目录
5. 构造解锁脚本
上一篇博客的最后,我们讲到
AddAllInputScripts
函数体中针对每一个非witness
的input
调用了SignTxOutput
函数。该函数主要用来对当前input
构造解锁脚本。
解锁脚本构造的最关键部分在于签名的构造,因而该函数中是用了Sign
这个词。
此外,SignTxOutput
函数名中虽然写的是output
,但这个output
是UTXO
的意思,也就是当前交易的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
函数将得到的sigScript
和previousScript
进行合并。老实说,该函数的实现我没看懂(/捂脸哭)。看了一圈注释,好像也没写清楚,而且这个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
}
其中各种判别函数(如isPubkey
,isPubkeyHash
等)主要是对其中的特定操作码进行判断。以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
函数的实现细节。感兴趣的读者,可以去看看。
没有时间去看这几篇博客的读者,只需记住CalcSignatureHash
和calcSignatureHash
函数的功能:即生成签名所需的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
格式的解锁脚本。