From f9f0b517e271aaf654d86cc79b52ce20736ab46c Mon Sep 17 00:00:00 2001 From: Andrew Kozlik Date: Mon, 1 Nov 2021 16:12:23 +0100 Subject: [PATCH] feat(core): Support Taproot in TxWeightCalculator. --- core/src/apps/bitcoin/sign_tx/tx_weight.py | 14 ++- core/tests/test_apps.bitcoin.txweight.py | 99 +++++++++++++++++++++- 2 files changed, 109 insertions(+), 4 deletions(-) diff --git a/core/src/apps/bitcoin/sign_tx/tx_weight.py b/core/src/apps/bitcoin/sign_tx/tx_weight.py index a80a67c91..d7fc71037 100644 --- a/core/src/apps/bitcoin/sign_tx/tx_weight.py +++ b/core/src/apps/bitcoin/sign_tx/tx_weight.py @@ -7,6 +7,7 @@ from micropython import const +from trezor import wire from trezor.enums import InputScriptType from .. import common @@ -28,7 +29,9 @@ _TXSIZE_OUTPUT = const(8) # size of a pubkey _TXSIZE_PUBKEY = const(33) # maximum size of a DER signature (3 type bytes, 3 len bytes, 33 R, 32 S, 1 sighash) -_TXSIZE_SIGNATURE = const(72) +_TXSIZE_DER_SIGNATURE = const(72) +# size of a Schnorr signature (32 R, 32 S, no sighash) +_TXSIZE_SCHNORR_SIGNATURE = const(64) # size of a multiscript without pubkey (1 M, 1 N, 1 checksig) _TXSIZE_MULTISIGSCRIPT = const(3) # size of a p2wpkh script (1 version, 1 push, 20 hash) @@ -48,6 +51,9 @@ class TxWeightCalculator: self.inputs_count += 1 if i.multisig: + if i.script_type == InputScriptType.SPENDTAPROOT: + raise wire.ProcessError("Multisig not supported for taproot") + n = len(i.multisig.nodes) if i.multisig.nodes else len(i.multisig.pubkeys) multisig_script_size = _TXSIZE_MULTISIGSCRIPT + n * (1 + _TXSIZE_PUBKEY) if i.script_type in common.SEGWIT_INPUT_SCRIPT_TYPES: @@ -57,11 +63,13 @@ class TxWeightCalculator: input_script_size = ( 1 # the OP_FALSE bug in multisig - + i.multisig.m * (1 + _TXSIZE_SIGNATURE) + + i.multisig.m * (1 + _TXSIZE_DER_SIGNATURE) + multisig_script_size ) + elif i.script_type == InputScriptType.SPENDTAPROOT: + input_script_size = 1 + _TXSIZE_SCHNORR_SIGNATURE else: - input_script_size = 1 + _TXSIZE_SIGNATURE + 1 + _TXSIZE_PUBKEY + input_script_size = 1 + _TXSIZE_DER_SIGNATURE + 1 + _TXSIZE_PUBKEY self.counter += 4 * _TXSIZE_INPUT diff --git a/core/tests/test_apps.bitcoin.txweight.py b/core/tests/test_apps.bitcoin.txweight.py index 14e0ab2b3..5a635c674 100644 --- a/core/tests/test_apps.bitcoin.txweight.py +++ b/core/tests/test_apps.bitcoin.txweight.py @@ -11,7 +11,7 @@ from apps.common import coins from apps.common.keychain import Keychain from apps.common.paths import AlwaysMatchingSchema from apps.bitcoin.sign_tx.tx_weight import * -from apps.bitcoin.scripts import output_derive_script +from apps.bitcoin.scripts import output_derive_script, output_script_paytoopreturn class TestCalculateTxWeight(unittest.TestCase): @@ -122,6 +122,30 @@ class TestCalculateTxWeight(unittest.TestCase): # segwit: segwit header, witness stack item count, witness 1*(2+1+107) = 110 # total 566 + def test_taproot_txweight(self): + coin = coins.by_name('Testnet') + + inp1 = TxInput( + address_n=[86 | 0x80000000, 1 | 0x80000000, 0 | 0x80000000, 1, 0], + amount=4600, + prev_hash=unhexlify('7956f1de3e7362b04115b64a31f0b6822c50dd6c08d78398f392a0ac3f0e357b'), + prev_index=1, + script_type=InputScriptType.SPENDTAPROOT, + ) + + out1 = TxOutput( + address="tb1paxhjl357yzctuf3fe58fcdx6nul026hhh6kyldpfsf3tckj9a3wslqd7zd", + amount=4450, + script_type=OutputScriptType.PAYTOADDRESS, + ) + + calculator = TxWeightCalculator() + calculator.add_input(inp1) + calculator.add_output(output_derive_script(out1.address, coin)) + + # 010000000001017b350e3faca092f39883d7086cdd502c82b6f0314ab61541b062733edef156790100000000ffffffff016211000000000000225120e9af2fc69e20b0be2629cd0e9c34da9f3ef56af7beac4fb4298262bc5a45ec5d0140493145b992dacbd7ea579a415efc2cba20c3bf0f7827d1bcf999109c0d11783fe96f91ddb04a889faa17ad21ecc5c81a578009744e95c7e721aff2a5c442916600000000 + self.assertEqual(calculator.get_total(), 4*94 + 68) + def test_legacy_multisig_txweight(self): coin = coins.by_name('Bitcoin') seed = bip39.seed(' '.join(['all'] * 12), '') @@ -206,6 +230,79 @@ class TestCalculateTxWeight(unittest.TestCase): # 01000000000101be0210025c5be68a473f6a38bf53b53bc88d5c46567616026dc056e72b92319c01000000232200208d398cfb58a1d9cdb59ccbce81559c095e8c6f4a3e64966ca385078d9879f95effffffff01887d180000000000220020c5f4a0a4ea7c0392efe0a9670a73264cffa90b19107cd8a8e9750ff93c77fdfb0400483045022100dd6342c65197af27d7894d8b8b88b16b568ee3b5ebfdc55fdfb7caa9650e3b4c02200c7074a5bcb0068f63d9014c7cd2b0490aba75822d315d41aad444e9b86adf5201483045022100e7e6c2d21109512ba0609e93903e84bfb7731ac3962ee2c1cad54a7a30ff99a20220421497930226c39fc3834e8d6da3fc876516239518b0e82e2dc1e3c46271a17c01695221021630971f20fa349ba940a6ba3706884c41579cd760c89901374358db5dd545b92102f2ff4b353702d2bb03d4c494be19d77d0ab53d16161b53fbcaf1afeef4ad0cb52103e9b6b1c691a12ce448f1aedbbd588e064869c79fbd760eae3b8cd8a5f1a224db53ae00000000 self.assertEqual(calculator.get_total(), 4*129 + 256) + def test_mixed_txweight(self): + coin = coins.by_name('Testnet') + + inp1 = TxInput( + address_n=[49 | 0x80000000, 1 | 0x80000000, 1 | 0x80000000, 0, 0], + amount=20000, + prev_hash=unhexlify('8c3ea7a10ab6d289119b722ec8c27b70c17c722334ced31a0370d782e4b6775d'), + prev_index=0, + script_type=InputScriptType.SPENDP2SHWITNESS, + ) + inp2 = TxInput( + address_n=[84 | 0x80000000, 1 | 0x80000000, 1 | 0x80000000, 0, 0], + amount=15000, + prev_hash=unhexlify('7956f1de3e7362b04115b64a31f0b6822c50dd6c08d78398f392a0ac3f0e357b'), + prev_index=0, + script_type=InputScriptType.SPENDWITNESS, + ) + inp3 = TxInput( + address_n=[86 | 0x80000000, 1 | 0x80000000, 1 | 0x80000000, 0, 0], + amount=4450, + prev_hash=unhexlify('7956f1de3e7362b04115b64a31f0b6822c50dd6c08d78398f392a0ac3f0e357b'), + prev_index=0, + script_type=InputScriptType.SPENDTAPROOT, + ) + inp4 = TxInput( + address_n=[44 | 0x80000000, 1 | 0x80000000, 1 | 0x80000000, 0, 0], + amount=10000, + prev_hash=unhexlify('3ac32e90831d79385eee49d6030a2123cd9d009fe8ffc3d470af9a6a777a119b'), + prev_index=2, + script_type=InputScriptType.SPENDADDRESS, + ) + + out1 = TxOutput( + address="tb1q6xnnna3g7lk22h5tn8nlx2ezmndlvuk556w4w3", + amount=25000, + script_type=OutputScriptType.PAYTOWITNESS, + ) + out2 = TxOutput( + address="mfnMbVFC1rH4p9GNbjkMfrAjyKRLycFAzA", + script_type=OutputScriptType.PAYTOADDRESS, + amount=7000, + ) + out3 = TxOutput( + address="2MvAG8m2xSf83FgeR4ZpUtaubpLNjAMMoka", + amount=6900, + script_type=OutputScriptType.PAYTOP2SHWITNESS, + ) + out4 = TxOutput( + op_return_data=b"test of op_return data", + amount=0, + script_type=OutputScriptType.PAYTOOPRETURN, + ) + out5 = TxOutput( + address="tb1ptgp9w0mm89ms43flw0gkrhyx75gyc6qjhtpf0jmt5sv0dufpnsrsyv9nsz", + amount=10000, + script_type=OutputScriptType.PAYTOTAPROOT, + ) + + calculator = TxWeightCalculator() + calculator.add_input(inp1) + calculator.add_input(inp2) + calculator.add_input(inp3) + calculator.add_input(inp4) + calculator.add_output(output_derive_script(out1.address, coin)) + calculator.add_output(output_derive_script(out2.address, coin)) + calculator.add_output(output_derive_script(out3.address, coin)) + calculator.add_output(output_script_paytoopreturn(out4.op_return_data)) + calculator.add_output(output_derive_script(out5.address, coin)) + + # 010000000001045d77b6e482d770031ad3ce3423727cc1707bc2c82e729b1189d2b60aa1a73e8c0000000017160014a33c6e24c99e108b97bc411e7e9ef31e9d5d6164ffffffff7b350e3faca092f39883d7086cdd502c82b6f0314ab61541b062733edef156790000000000ffffffff852e125137abca2dd7a42837dccfc34edc358c72eefd62978d6747d3be9315900000000000ffffffff9b117a776a9aaf70d4c3ffe89f009dcd23210a03d649ee5e38791d83902ec33a020000006b483045022100f6bd64136839b49822cf7e2050bc5c91346fc18b5cf97a945d4fd6c502f712d002207d1859e66d218f705b704f3cfca0c75410349bb1f50623f4fc2d09d5d8df0a3f012103bae960983f83e28fcb8f0e5f3dc1f1297b9f9636612fd0835b768e1b7275fb9dffffffff05a861000000000000160014d1a739f628f7eca55e8b99e7f32b22dcdbf672d4581b0000000000001976a91402e9b094fd98e2a26e805894eb78f7ff3fef199b88acf41a00000000000017a9141ff816cbeb74817050de585ceb2c772ebf71147a870000000000000000186a1674657374206f66206f705f72657475726e206461746110270000000000002251205a02573f7b39770ac53f73d161dc86f5104c6812bac297cb6ba418f6f1219c070247304402205fae7fa2b5141548593d5623ce5bd82ee18dfc751c243526039c91848efd603702200febfbe3467a68c599245ff89055514f26e146c79b58d932ced2325e6dad1b1a0121021630971f20fa349ba940a6ba3706884c41579cd760c89901374358db5dd545b90247304402201b21212100c84207697cebb852374669c382ed97cbd08afbbdfe1b302802161602206b32b2140d094cf5b7e758135961c95478c8e82fea0df30f56ccee284b79eaea012103f6b2377d52960a6094ec158cf19dcf9e33b3da4798c2302aa5806483ed4187ae01404a81e4b7f55d6d4a26923c5e2daf3cc86ed6030f83ea6e7bb16d7b81b988b34585be21a64ab45ddcc2fb9f17be2dfeff6b22cf943bc3fc8f125a7f463af428ed0000000000 + # The witness data is 283 bytes, but two of the DER signatures are one byte below the + # average length, so the caculator should estimate 285 bytes of witness data. + self.assertEqual(calculator.get_total(), 4*477 + 285) if __name__ == '__main__': unittest.main()