0%

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

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

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

IV. 构造解锁脚本

A. SignTxOutput函数

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 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函数展开讲解了。

以下分IV.A和IV.B两小节分别介绍其他类中的PubKeyHashTy类型和ScriptHashTy类型。

B. PubKeyHashTy类型的解锁脚本

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

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
// 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函数。

1. ExtractPkScriptAddrs函数

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 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函数

1) typeOfScript函数

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

1
2
3
4
5
6
7
8
9
10
11
12
13
// 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函数为例,其主体代码如下所示:
1
2
3
4
5
6
7
8
9
// 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
}

2) NewAddressPubKeyHash函数

NewAddressPubKeyHash函数如下所示。

1
2
3
4
5
// 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函数,后者的主体如下:
1
2
3
4
5
6
7
8
9
10
11
// 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值做了一些判断和封装。

2. SignatureScript函数

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 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函数

1) RawTxInSignature函数
1
2
3
4
5
6
7
8
9
10
// 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来生成messageCalcSignatureHash函数内部主要调用了calcSignatureHash函数,而calcSignatureHash函数是比较复杂的。 如本篇博客开篇所述,笔者已经写了三篇博客来介绍这个message的构建过程,其中第三篇博客签名机制(3) —— 源码分析便详细分析了calcSignatureHash函数的实现细节。感兴趣的读者,可以去看看。 没有时间去看这几篇博客的读者,只需记住CalcSignatureHashcalcSignatureHash函数的功能:即生成签名所需的message.

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

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

C. ScriptHashTy类型的解锁脚本

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

1. 第一次执行sign函数

sign函数主体如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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的方式。

2. 第二次执行sign函数

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

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