比特币离线签名

比特币开发技术学习笔记(三)

离线签名

离线签名不需要实际接触到 UTXO 集合,对于离线钱包是个有用的功能。离线签名本身是安全 的,不过我们接下来这个模拟场景其实用了链上不存在的 UTXO ——来自还没有被确认的交易的 output。使用未广播的交易中的 UTXO 可能导致延展性攻击,详情请阅读 transaction malleability

接下来的演示我们需要一个未广播的交易,如果上一章那个交易还没有广播出去,您可以直接拿来 用。否则问题也不大,在 notebook 里重新执行前面的步骤再生成一个也很容易。为了演示效果, 我们在下面用 python 生成一个新的交易,并对它签名:

result = json.loads(cli("listunspent"))
utxo0 = result[-6]
utxo1 = result[5]
addr0 = cli("getnewaddress")
addr1 = cli("getnewaddress")
priv0 = cli("dumpprivkey", utxo0["address"])
priv1 = cli("dumpprivkey", utxo1["address"])
vin = [{"txid":utxo0["txid"], "vout":utxo0["vout"]}, {"txid":utxo1["txid"], "vout":utxo1["vout"]}]
vout = {utxo0["address"]:89.9999, utxo1["address"]:9.9999}
old_tx_encoded = cli("createrawtransaction", json.dumps(vin), json.dumps(vout))

signed0 = json.loads(cli("signrawtransaction", old_tx_encoded, "[]", json.dumps([priv0])))
signed1 = json.loads(cli("signrawtransaction", signed0["hex"], "[]", json.dumps([priv1])))
print(signed1)


{'hex': '0200000002ea542d4d421df0a22396c5e2606a18c22830b24ab7af5e0a701de4478eb2ad4500000000484730440220318b1001990df37555dc337bbabf0afb40ea0dbf5884bf424fdb0407b2d53e450220678843f45de65951980a23d804afb9fa61d63804791dc48bb582af34fadc4d3a01ffffffff08897b4ecfc4e2edf70c743aab558a0e9e33876512e28a8f3e3c06eeb9f2785a000000006a47304402204a8cd566949ddbb061eb4c7ffd5a8fa5398835d06010d62a35dbee5553ebe5ec022058631cbc2a35507684b990b0b12e93c1e763b3685ed863fcab0524f74982abe6012102dfaabd6c406491fd337c7aedc923c984f8b53aedf016fc470dbe60f931b3ce11ffffffff02f0f27018020000001976a9148eb3bc2be78b4e8289ec39d356247993c14920f788acf0a29a3b000000001976a914f4bc1ba5d157600b5338bca440903b513be1f6a288ac00000000', 'complete': True}

我们解开这个交易,后面会用到它的内部数据,这里将我们需要的部分取出来:

old_raw_tx = json.loads(cli("decoderawtransaction", signed1["hex"]))
utxo_txid = old_raw_tx["txid"]
utxo = old_raw_tx["vout"][1]
print(utxo)


{'value': 9.9999, 'n': 1, 'scriptPubKey': {'asm': 'OP_DUP OP_HASH160 f4bc1ba5d157600b5338bca440903b513be1f6a2 OP_EQUALVERIFY OP_CHECKSIG', 'hex': '76a914f4bc1ba5d157600b5338bca440903b513be1f6a288ac', 'reqSigs': 1, 'type': 'pubkeyhash', 'addresses': ['n3pzWqJHW4y1BCdPKv4VnB3Qtnq5X745wa']}}

再构造一个接收地址:

addr2 = cli("getnewaddress")

现在我们构造一个新交易,向它转账:

vin = [{"txid":utxo_txid, "vout":1}]
vout = {addr2:9.9999}
raw_tx_encoded = cli("createrawtransaction", json.dumps(vin), json.dumps(vout))
print(raw_tx_encoded)


0200000001f2fbb7a4795e8f56be05a1a29a400607f3081f344a53a9e4cd51ca7eed6a584d0100000000ffffffff01f0a29a3b000000001976a914ae7e87d7da27a3c913f2a6deb229fe1b81e9183288ac00000000

需要注意的是此时我们再这个交易中花费的 utxo 还没有被确认,如果我们对它签名:

signed2 = json.loads(cli("signrawtransaction", raw_tx_encoded))
print(signed2)


{'hex': '0200000001fa1098be46a37d606601317f6e117d3f31c2f2058d9845ae715063aaa28623e20100000000ffffffff01f0a29a3b000000001976a914ae7e87d7da27a3c913f2a6deb229fe1b81e9183288ac00000000', 'complete': False, 'errors': [{'txid': 'e22386a2aa635071ae45988d05f2c2313f7d116e7f310166607da346be9810fa', 'vout': 1, 'scriptSig': '', 'sequence': 4294967295, 'error': 'Input not found or already spent'}]}

之前我们这样成功的签了简单交易,但是现在会提示我们input中有不存在的数据。 Offline Signing 一章图解了其中的机制。简单来说,节点签名需要引用 UTXO 的 pubkey script ,因为交易还没有确认,节点找不到它,这种情况 与离线签名要解决的问题一样,都要明确引入 pubkey script :

signed2 = json.loads(cli("signrawtransaction", raw_tx_encoded, 
                         json.dumps([{"txid":utxo_txid, "vout":1, "scriptPubKey":utxo["scriptPubKey"]["hex"]}])))
print(signed2)


{'hex': '0200000001f2fbb7a4795e8f56be05a1a29a400607f3081f344a53a9e4cd51ca7eed6a584d010000006a473044022065aa62f5b6c8a1839108b9b3585a4915abec8c073afdc63b08e5fef70a2b92f202207c2a06ec125bf1a75d21d8fe933e4592c5f837e7846000a6f48c8fa8c701eaf2012102dfaabd6c406491fd337c7aedc923c984f8b53aedf016fc470dbe60f931b3ce11ffffffff01f0a29a3b000000001976a914ae7e87d7da27a3c913f2a6deb229fe1b81e9183288ac00000000', 'complete': True}

实际做离线签名时,我们也要向离线钱包提供这些数据。

接下来,我们广播这个签名后的交易结果:

cli("sendrawtransaction", signed2["hex"])

这会造成错误,节点拒绝广播一条带有未知 utxo 作为 input 的交 易。如果您在 notebook 里运行这段演示代码,收不到任何反馈,但 是在后台终端可以看到错误信息。我们要先将前面那个交易广播出去, 就可以广播这个做了离线签名的交易:

print(cli("sendrawtransaction", signed1["hex"]))
result = cli("sendrawtransaction", signed2["hex"])
print(result)
4d586aed7eca51cde4a9534a341f08f30706409aa2a105be568f5e79a4b7fbf2

我们可以看到被广播的交易详情,也可以将这个离线签名得到的交易打包确认:

print(cli("getrawtransaction", result, "1"))


result = cli("generate", "1")
print(result)
[
"4d75953a5a5131ed0fa17360cc79653777fa3d6b6abddbadbd57a750d63f9710"
]

现在我们看到新产生的块确认了这次交易:

print(cli("getblock", json.loads(result)[0]))
{
"hash": "4d75953a5a5131ed0fa17360cc79653777fa3d6b6abddbadbd57a750d63f9710",
"confirmations": 1,
"strippedsize": 228,
"size": 228,
"weight": 912,
"height": 209,
"version": 805306371,
"versionHex": "30000003",
"merkleroot": "4c1a679ecffd1c7eaa7b1ca937c37230008d67dbdcae8de13b34d9cb9b33b35f",
"tx": [
"4c1a679ecffd1c7eaa7b1ca937c37230008d67dbdcae8de13b34d9cb9b33b35f"
],
"time": 1505413337,
"mediantime": 1505237405,
"nonce": 1,
"bits": "207fffff",
"difficulty": 4.656542373906925e-10,
"chainwork": "00000000000000000000000000000000000000000000000000000000000001a4",
"previousblockhash": "67c39d669acd9bb4f7c5002b6f31669702ac8dc7d50e94778e17c14008a45558"
}