比特币多重签名

比特币开发学习笔记(四)

多重签名

这一节,我们构造一个多重签名地址,然后把该地址保存的钱取出来花掉。

构造一个多重签名地址并不难,构造一个多重签名交易需要两个参数,一个是花钱 (签名)时所需的最低签名数(m),一个是用于验证这些签名的公钥列表(n)。 我们称之为 m-of-n 签名,这里我们演示一下 2-of-3 签名交易。

先构造三个演示用的地址:

addr0 = cli("getnewaddress")
addr1 = cli("getnewaddress")
addr2 = cli("getnewaddress")
print(addr0)
print(addr1)
print(addr2)
mktWLyZELZZ5y9WUkYSQ9DcoYVKaoxYyvT
mrNZkSyAkJertHHyP8jAiWdHxTnGVBQny8
msrhGDs5wZLaebbSKg61d8aGX1jMaWDiUv

P2PKH 地址并不能直接用来生成多重签名的 redeem 脚本,不过 Bitcoin Core 可以根据地址得到它自己管理的这些地址的公钥。

不过,要提供给他人多重签名(多方“陌生人”合作是多重签名的常见场景)的合作方,必 须要提供完整的公钥。调用 validateaddress 方法可以拿到指定地址的完整公钥。

result = cli("validateaddress", addr1)
print(result)
pubkey1 = json.loads(result)["pubkey"]
result = cli("validateaddress", addr2)
print(result)
pubkey2 = json.loads(result)["pubkey"]
{
"isvalid": true,
"address": "mrNZkSyAkJertHHyP8jAiWdHxTnGVBQny8",
"scriptPubKey": "76a91477133c830409e0d99199a7eee9c45a8c6615118d88ac",
"ismine": true,
"iswatchonly": false,
"isscript": false,
"pubkey": "02ca755edd03f9572c9e98729a31cb17f1aceac5a7fa38d81ab7fe5806f413dbd0",
"iscompressed": true,
"account": "",
"timestamp": 1505141050,
"hdkeypath": "m/0'/0'/55'",
"hdmasterkeyid": "c2dca756bb49e1d462cd9ba959d13be9af00d2ae"
}
{
"isvalid": true,
"address": "msrhGDs5wZLaebbSKg61d8aGX1jMaWDiUv",
"scriptPubKey": "76a914875d47e9ed977f409acce2c3489823c1c827d62988ac",
"ismine": true,
"iswatchonly": false,
"isscript": false,
"pubkey": "0337b7a28ae11ac35fd27cb3ab0777544932cecc6d432ae1cdbb256e934bdf68f6",
"iscompressed": true,
"account": "",
"timestamp": 1505141051,
"hdkeypath": "m/0'/0'/56'",
"hdmasterkeyid": "c2dca756bb49e1d462cd9ba959d13be9af00d2ae"
}

在实际使用的场景中,我们可以将自己的完整公钥提供给其他(在自己节点的)合作者一起做 多重签名:

result = cli("createmultisig", "2", json.dumps([addr0, pubkey1, pubkey2]))
print(result)
sig = json.loads(result)
{
"address": "2NDMnSeG6pSnx7Qdza7bsNbtD91T2jtQg3T",
"redeemScript": "522102ac14f0f7f6565c985b6c61f77f20117bc812c87dd8a1230d111453d82d0471a42102ca755edd03f9572c9e98729a31cb17f1aceac5a7fa38d81ab7fe5806f413dbd0210337b7a28ae11ac35fd27cb3ab0777544932cecc6d432ae1cdbb256e934bdf68f653ae"
}
result = cli("createmultisig", "2", json.dumps([addr0, addr1, pubkey2]))
print(result)
{
"address": "2NDMnSeG6pSnx7Qdza7bsNbtD91T2jtQg3T",
"redeemScript": "522102ac14f0f7f6565c985b6c61f77f20117bc812c87dd8a1230d111453d82d0471a42102ca755edd03f9572c9e98729a31cb17f1aceac5a7fa38d81ab7fe5806f413dbd0210337b7a28ae11ac35fd27cb3ab0777544932cecc6d432ae1cdbb256e934bdf68f653ae"
}
result = cli("createmultisig", "2", json.dumps([addr0, addr1, addr2]))
print(result)
"address": "2NDMnSeG6pSnx7Qdza7bsNbtD91T2jtQg3T",
"redeemScript": "522102ac14f0f7f6565c985b6c61f77f20117bc812c87dd8a1230d111453d82d0471a42102ca755edd03f9572c9e98729a31cb17f1aceac5a7fa38d81ab7fe5806f413dbd0210337b7a28ae11ac35fd27cb3ab0777544932cecc6d432ae1cdbb256e934bdf68f653ae"
}

在上面的例子中,我刻意用同一组公钥/地址对中的不同内容,按addr 1+2+3 的一致顺序 进行了多次签名,我们可以看到签名结果是一样的。对于当前的钱包自己的管理的地址,它 能找到对应的公钥,而我们要到”陌生人“那里去参与多重签名的时候,就要提供完整的公钥 了。

实际使用场景中,我们要保有 pubkey 或 redeem script 至少一方,如果丢了 redeem script,我们还可以用公钥组重签一个,如果少了某个至多(n-m)个公钥,至少还能用其它 公钥和 redeem script 把钱取出来花掉。如果两个都丢了,这笔钱就永远无法花掉。

用 createmultisig 生成签名的时候,它不会把地址或 redeem script 保存进钱包,需 要的话,要用 addmultisigaddress 方法把公钥/地址列表保存起来,它会返回对应的多重 签名地址。

接着我们向这个地址发送一些 UTXO 。多重签名地址跟普通地址收钱没有区别,我们直接发送 即可:

txid = cli("sendtoaddress", sig["address"], "15.00")
print(txid)
4e2162c77cc98a5531cb136041063a6027f48a1be16a432cbc0143eddcce9b61

拿到交易id后,我们看一下它的内容:

content = cli("getrawtransaction", txid, "1")
print(content)
tx = json.loads(content)
{
"hex": "02000000025df0506609f66dd6291be8d61856e44495b2391d0c462c402ac9d1f483fe8d94010000006a473044022055186501c673c3b9298efb428dd4973e3313181e6f7350c588c1961f4176953902202a3db3b7791451e9a3ccf776172d3979431def37611ae7c93ace4e95fd7c51ff0121022a7658ba8ea589f2f06ad609ba3e87081fa1218e2150525162916d30d2da8b95feffffff625f84f0d9f2a97a4482a56b7a52b8cae32fe7808e4bd0da0db2a39c7e2f4ba7000000006a47304402203aee2f37d60452fd2e66a123ebf2f24f1febeb07145939c12545325afbb05af502200e8db4330ed9466538f0e19a7e95002262fc3d42c749cc36a56ea87468b825820121022a7658ba8ea589f2f06ad609ba3e87081fa1218e2150525162916d30d2da8b95feffffff02f047cd1d000000001976a914a5627150617eb8b93ce548e99fb7b73be782791d88ac002f68590000000017a914dca01a630c895a9901b3c979d078ab13294bf99b87d1000000",
"txid": "4e2162c77cc98a5531cb136041063a6027f48a1be16a432cbc0143eddcce9b61",
"hash": "4e2162c77cc98a5531cb136041063a6027f48a1be16a432cbc0143eddcce9b61",
"size": 370,
"vsize": 370,
"version": 2,
"locktime": 209,
"vin": [
{
"txid": "948dfe83f4d1c92a402c460c1d39b29544e45618d6e81b29d66df6096650f05d",
"vout": 1,
"scriptSig": {
"asm": "3044022055186501c673c3b9298efb428dd4973e3313181e6f7350c588c1961f4176953902202a3db3b7791451e9a3ccf776172d3979431def37611ae7c93ace4e95fd7c51ff[ALL] 022a7658ba8ea589f2f06ad609ba3e87081fa1218e2150525162916d30d2da8b95",
"hex": "473044022055186501c673c3b9298efb428dd4973e3313181e6f7350c588c1961f4176953902202a3db3b7791451e9a3ccf776172d3979431def37611ae7c93ace4e95fd7c51ff0121022a7658ba8ea589f2f06ad609ba3e87081fa1218e2150525162916d30d2da8b95"
},
"sequence": 4294967294
},
{
"txid": "a74b2f7e9ca3b20ddad04b8e80e72fe3cab8527a6ba582447aa9f2d9f0845f62",
"vout": 0,
"scriptSig": {
"asm": "304402203aee2f37d60452fd2e66a123ebf2f24f1febeb07145939c12545325afbb05af502200e8db4330ed9466538f0e19a7e95002262fc3d42c749cc36a56ea87468b82582[ALL] 022a7658ba8ea589f2f06ad609ba3e87081fa1218e2150525162916d30d2da8b95",
"hex": "47304402203aee2f37d60452fd2e66a123ebf2f24f1febeb07145939c12545325afbb05af502200e8db4330ed9466538f0e19a7e95002262fc3d42c749cc36a56ea87468b825820121022a7658ba8ea589f2f06ad609ba3e87081fa1218e2150525162916d30d2da8b95"
},
"sequence": 4294967294
}
],
"vout": [
{
"value": 4.99992560,
"n": 0,
"scriptPubKey": {
"asm": "OP_DUP OP_HASH160 a5627150617eb8b93ce548e99fb7b73be782791d OP_EQUALVERIFY OP_CHECKSIG",
"hex": "76a914a5627150617eb8b93ce548e99fb7b73be782791d88ac",
"reqSigs": 1,
"type": "pubkeyhash",
"addresses": [
"mvbRicvC2G9rdt5MCSb91CFWC293j3ZetN"
]
}
},
{
"value": 15.00000000,
"n": 1,
"scriptPubKey": {
"asm": "OP_HASH160 dca01a630c895a9901b3c979d078ab13294bf99b OP_EQUAL",
"hex": "a914dca01a630c895a9901b3c979d078ab13294bf99b87",
"reqSigs": 1,
"type": "scripthash",
"addresses": [
"2NDMnSeG6pSnx7Qdza7bsNbtD91T2jtQg3T"
]
}
}
]
}

现在我们生成一个新地址,从这个UTXO中取出钱来:

addr3 = cli("getnewaddress")
print(addr3)
newtx = cli("createrawtransaction", 
    json.dumps([{"txid":tx["txid"], "vout":1}]), 
    json.dumps({addr3:14.9999}))
print(newtx)

现在拿出两个私钥用来签名,因为 sig 是一个 2-of-3 签名,用任意两个地址的私钥都可以:

priv0 = cli("dumpprivkey", addr0)
priv1 = cli("dumpprivkey", addr1)
print(priv0)
print(priv1)

接下来我们先对交易做第一次签名:

result = cli("signrawtransaction", newtx, 
             json.dumps([{"txid":tx["txid"], "vout":1, 
                          "scriptPubKey":tx["vout"][1]["scriptPubKey"]["hex"],
                          "redeemScript":sig["redeemScript"]}]),
            json.dumps([priv0]))
sig_first = json.loads(result)
print(result)
{
"hex": "0200000001619bcedced4301bc2c436ae11b8af427603a06416013cb31558ac97cc762214e01000000b40047304402205e24e336a66d8944c676ca3377dd9608fa0053f9680adc151ee0dffe33324c6c02200578533ae3a0771eb6a1881b20818deff896ceba264e7724d4d68ea8e0281ed3014c69522102ac14f0f7f6565c985b6c61f77f20117bc812c87dd8a1230d111453d82d0471a42102ca755edd03f9572c9e98729a31cb17f1aceac5a7fa38d81ab7fe5806f413dbd0210337b7a28ae11ac35fd27cb3ab0777544932cecc6d432ae1cdbb256e934bdf68f653aeffffffff01f0076859000000001976a91493887891808d513309b8eb86b8d7089d9e2df6d088ac00000000",
"complete": false,
"errors": [
{
"txid": "4e2162c77cc98a5531cb136041063a6027f48a1be16a432cbc0143eddcce9b61",
"vout": 1,
"scriptSig": "0047304402205e24e336a66d8944c676ca3377dd9608fa0053f9680adc151ee0dffe33324c6c02200578533ae3a0771eb6a1881b20818deff896ceba264e7724d4d68ea8e0281ed3014c69522102ac14f0f7f6565c985b6c61f77f20117bc812c87dd8a1230d111453d82d0471a42102ca755edd03f9572c9e98729a31cb17f1aceac5a7fa38d81ab7fe5806f413dbd0210337b7a28ae11ac35fd27cb3ab0777544932cecc6d432ae1cdbb256e934bdf68f653ae",
"sequence": 4294967295,
"error": "Operation not valid with the current stack size"
}
]
}

然后我们对第一次签名的结果做第二次签名:

result = cli("signrawtransaction", sig_first["hex"], 
             json.dumps([{"txid":tx["txid"], "vout":1, 
                          "scriptPubKey":tx["vout"][1]["scriptPubKey"]["hex"],
                          "redeemScript":sig["redeemScript"]}]),
            json.dumps([priv1]))
sig_second = json.loads(result)
print(result)
{
"hex": "0200000001619bcedced4301bc2c436ae11b8af427603a06416013cb31558ac97cc762214e01000000fdfd000047304402205e24e336a66d8944c676ca3377dd9608fa0053f9680adc151ee0dffe33324c6c02200578533ae3a0771eb6a1881b20818deff896ceba264e7724d4d68ea8e0281ed301483045022100f579ca81047173e24f85751eff25a958a9f57b44303595aa1f3ac7c3f9584b36022006daa7df8e01bb9ba2d26a7ba952bb90ef023af4140899d0c094f1fb9e86f6b9014c69522102ac14f0f7f6565c985b6c61f77f20117bc812c87dd8a1230d111453d82d0471a42102ca755edd03f9572c9e98729a31cb17f1aceac5a7fa38d81ab7fe5806f413dbd0210337b7a28ae11ac35fd27cb3ab0777544932cecc6d432ae1cdbb256e934bdf68f653aeffffffff01f0076859000000001976a91493887891808d513309b8eb86b8d7089d9e2df6d088ac00000000",
"complete": true
}

我们用了三个私钥中的两个,得到了一个签名后可以广播的交易。这两步过程可以在两个私钥的 所有者各自的钱包中完成,不需要将私钥暴露给其他人。

我们也可以验证一下,用三个公钥对应的私钥,能否对 sig_first["hex"] 成功签名(这 意味着使用不同的两个公钥完成 2-of-3 签名)。

priv2 = cli("dumpprivkey", addr2)
result = cli("signrawtransaction", sig_first["hex"], 
             json.dumps([{"txid":tx["txid"], "vout":1, 
                          "scriptPubKey":tx["vout"][1]["scriptPubKey"]["hex"],
                          "redeemScript":sig["redeemScript"]}]),
            json.dumps([priv2]))
sig_other = json.loads(result)
print(result)
{
"hex": "0200000001619bcedced4301bc2c436ae11b8af427603a06416013cb31558ac97cc762214e01000000fc0047304402205e24e336a66d8944c676ca3377dd9608fa0053f9680adc151ee0dffe33324c6c02200578533ae3a0771eb6a1881b20818deff896ceba264e7724d4d68ea8e0281ed30147304402205b6ecd76af1f7ff3359ff651a135b014d66207916c3e93af9ddb0d05e23dd69a022032833a9b9f14065d118635bfafd61b4ffdfb27dfd0f4cee31f6aa434330a65e9014c69522102ac14f0f7f6565c985b6c61f77f20117bc812c87dd8a1230d111453d82d0471a42102ca755edd03f9572c9e98729a31cb17f1aceac5a7fa38d81ab7fe5806f413dbd0210337b7a28ae11ac35fd27cb3ab0777544932cecc6d432ae1cdbb256e934bdf68f653aeffffffff01f0076859000000001976a91493887891808d513309b8eb86b8d7089d9e2df6d088ac00000000",
"complete": true
}

这里因为都是向同一个地址转账,我们可以观察到甚至用不同的私钥签名得到的结果都是一样的。实际 场景中我们不排除 m-of-n 中多个 m 组合的成员构造了向不同地址转账的交易这种极端情况,但是它们 至多有一个能够被确认——比特币的机制会抵抗那种试图多重花费的欺诈行为。

print(cli("sendrawtransaction", sig_second["hex"]))
19a7c361e5bef11f71ea8424b06e8abb422840ffee6886f739a31e1c5021ce84
result = cli("generate", "1")
print(result)
[
"5580f5a1275b94694794cb723d0394491182b2cd4f9900c91d1d98073178bf2a"
]

这个交易被确认后,我们再执行下面代码,会得到“bad-txns-inputs-spent”的错误提示。 Notebook 用户可以在执行 jupyter 的终端看到这个信息。

print(cli("sendrawtransaction", sig_other["hex"]))