From da4bc2dc8b92f434ef8a96c576fb41b6e8e95b63 Mon Sep 17 00:00:00 2001 From: Tomas Susanka Date: Fri, 3 Nov 2017 16:04:39 +0100 Subject: [PATCH] wallet/signing: P2WPKH in P2SH address generation based on BIP-49, PAYTOP2SHWITNESS output type --- src/apps/wallet/sign_tx/segwit_bip143.py | 2 +- src/apps/wallet/sign_tx/signing.py | 36 +++++++- tests/test_apps.wallet.segwit.address.py | 44 ++++++++++ tests/test_apps.wallet.segwit.signtx.py | 103 ++++++++++++++++++++++- 4 files changed, 178 insertions(+), 7 deletions(-) create mode 100644 tests/test_apps.wallet.segwit.address.py diff --git a/src/apps/wallet/sign_tx/segwit_bip143.py b/src/apps/wallet/sign_tx/segwit_bip143.py index eaa23dd3..4d459225 100644 --- a/src/apps/wallet/sign_tx/segwit_bip143.py +++ b/src/apps/wallet/sign_tx/segwit_bip143.py @@ -51,7 +51,7 @@ class Bip143: write_bytes(h_preimage, bytearray(self.get_outputs_hash())) # hashOutputs write_uint32(h_preimage, tx.lock_time) # nLockTime - write_uint32(h_preimage, 0x00000001) # nHashType todo + write_uint32(h_preimage, 0x00000001) # nHashType - only SIGHASH_ALL currently return get_tx_hash(h_preimage, True) diff --git a/src/apps/wallet/sign_tx/signing.py b/src/apps/wallet/sign_tx/signing.py index f7af261f..9d252fb6 100644 --- a/src/apps/wallet/sign_tx/signing.py +++ b/src/apps/wallet/sign_tx/signing.py @@ -205,7 +205,7 @@ async def sign_tx(tx: SignTx, root): write_varint(w_txo_bin, tx.outputs_count) write_tx_output(w_txo_bin, txo_bin) - tx_ser.signature_index = None # @todo delete? + tx_ser.signature_index = None tx_ser.signature = None tx_ser.serialized_tx = w_txo_bin @@ -216,8 +216,6 @@ async def sign_tx(tx: SignTx, root): # STAGE_REQUEST_SEGWIT_WITNESS txi = await request_tx_input(tx_req, i) # todo check amount? - # if hashType != ANYONE_CAN_PAY ? todo - # todo: what to do with other types? key_sign = node_derive(root, txi.address_n) key_sign_pub = key_sign.public_key() bip143_hash = bip143.preimage_hash(tx, txi, ecdsa_hash_pubkey(key_sign_pub)) @@ -299,6 +297,14 @@ def get_p2wpkh_witness(signature: bytes, pubkey: bytes): def output_derive_script(o: TxOutputType, coin: CoinType, root) -> bytes: + + # if PAYTOADDRESS check address prefix todo could be better? + if o.script_type == OutputScriptType.PAYTOADDRESS and o.address: + raw = base58.decode_check(o.address) + coin_type, address = address_type.split(coin, raw) + if int.from_bytes(coin_type, 'little') == coin.address_type_p2sh: + o.script_type = OutputScriptType.PAYTOSCRIPTHASH + if o.script_type == OutputScriptType.PAYTOADDRESS: ra = output_paytoaddress_extract_raw_address(o, coin, root) ra = address_type.strip(coin.address_type, ra) @@ -309,6 +315,13 @@ def output_derive_script(o: TxOutputType, coin: CoinType, root) -> bytes: ra = address_type.strip(coin.address_type_p2sh, ra) return script_paytoscripthash_new(ra) + elif o.script_type == OutputScriptType.PAYTOP2SHWITNESS: # todo ok? check if change? + node = node_derive(root, o.address_n) + address = get_p2wpkh_in_p2sh_address(node.public_key(), coin) + ra = base58.decode_check(address) + ra = address_type.strip(coin.address_type_p2sh, ra) + return script_paytoscripthash_new(ra) + elif o.script_type == OutputScriptType.PAYTOOPRETURN: if o.amount == 0: return script_paytoopreturn_new(o.op_return_data) @@ -323,7 +336,6 @@ def output_derive_script(o: TxOutputType, coin: CoinType, root) -> bytes: def output_paytoaddress_extract_raw_address( o: TxOutputType, coin: CoinType, root, p2sh: bool=False) -> bytes: - # todo if segwit then addr_type = p2sh ? addr_type = coin.address_type_p2sh if p2sh else coin.address_type # TODO: dont encode/decode more then necessary if o.address_n is not None: @@ -387,6 +399,22 @@ def ecdsa_sign(node, digest: bytes) -> bytes: return sigder +def get_p2wpkh_in_p2sh_address(pubkey: bytes, coin: CoinType) -> str: + pubkeyhash = ecdsa_hash_pubkey(pubkey) + s = bytearray(22) + s[0] = 0x00 # OP_0 + s[1] = 0x14 # pushing 20 bytes + s[2:22] = pubkeyhash + h = sha256(s).digest() + h = ripemd160(h).digest() + + s = bytearray(21) # todo better? + s[0] = coin.address_type_p2sh + s[1:21] = h + + return base58.encode_check(bytes(s)) + + # TX Scripts # === diff --git a/tests/test_apps.wallet.segwit.address.py b/tests/test_apps.wallet.segwit.address.py new file mode 100644 index 00000000..b9acbd85 --- /dev/null +++ b/tests/test_apps.wallet.segwit.address.py @@ -0,0 +1,44 @@ +from common import * + +from apps.wallet.sign_tx.signing import * +from apps.common import coins +from trezor.crypto import bip32, bip39 + + +class TestSegwitAddress(unittest.TestCase): + # pylint: disable=C0301 + + def test_p2wpkh_in_p2sh_address(self): + + coin = coins.by_name('Testnet') + + address = get_p2wpkh_in_p2sh_address( + unhexlify('03a1af804ac108a8a51782198c2d034b28bf90c8803f5a53f76276fa69a4eae77f'), + coin + ) + self.assertEqual(address, '2Mww8dCYPUpKHofjgcXcBCEGmniw9CoaiD2') + + def test_p2wpkh_in_p2sh_node_derive_address(self): + + coin = coins.by_name('Testnet') + seed = bip39.seed(' '.join(['all'] * 12), '') + root = bip32.from_seed(seed, 'secp256k1') + + node = node_derive(root, [49 | 0x80000000, 1 | 0x80000000, 0 | 0x80000000, 1, 0]) + address = get_p2wpkh_in_p2sh_address(node.public_key(), coin) + + self.assertEqual(address, '2N1LGaGg836mqSQqiuUBLfcyGBhyZbremDX') + + node = node_derive(root, [49 | 0x80000000, 1 | 0x80000000, 0 | 0x80000000, 1, 1]) + address = get_p2wpkh_in_p2sh_address(node.public_key(), coin) + + self.assertEqual(address, '2NFWLCJQBSpz1oUJwwLpX8ECifFWGznBVqs') + + node = node_derive(root, [49 | 0x80000000, 1 | 0x80000000, 0 | 0x80000000, 0, 0]) + address = get_p2wpkh_in_p2sh_address(node.public_key(), coin) + + self.assertEqual(address, '2N4Q5FhU2497BryFfUgbqkAJE87aKHUhXMp') + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_apps.wallet.segwit.signtx.py b/tests/test_apps.wallet.segwit.signtx.py index 5ba08a5d..c0c88494 100644 --- a/tests/test_apps.wallet.segwit.signtx.py +++ b/tests/test_apps.wallet.segwit.signtx.py @@ -20,7 +20,7 @@ from apps.wallet.sign_tx import signing class TestSignSegwitTx(unittest.TestCase): # pylint: disable=C0301 - def test_send_p2sh(self): + def test_send_p2wpkh_in_p2sh(self): coin = coins.by_name('Testnet') @@ -44,7 +44,7 @@ class TestSignSegwitTx(unittest.TestCase): ) out2 = TxOutputType( address='2N1LGaGg836mqSQqiuUBLfcyGBhyZbremDX', - script_type=OutputScriptType.PAYTOSCRIPTHASH, # todo! + script_type=OutputScriptType.PAYTOADDRESS, amount=123456789 - 11000 - 12300000, address_n=None, ) @@ -112,6 +112,105 @@ class TestSignSegwitTx(unittest.TestCase): with self.assertRaises(StopIteration): signer.send(None) + def test_send_p2wpkh_in_p2sh_change(self): + + coin = coins.by_name('Testnet') + + seed = bip39.seed(' '.join(['all'] * 12), '') + root = bip32.from_seed(seed, 'secp256k1') + + inp1 = TxInputType( + # 49'/1'/0'/1/0" - 2N1LGaGg836mqSQqiuUBLfcyGBhyZbremDX + address_n=[49 | 0x80000000, 1 | 0x80000000, 0 | 0x80000000, 1, 0], + amount=123456789, + prev_hash=unhexlify('20912f98ea3ed849042efed0fdac8cb4fc301961c5988cba56902d8ffb61c337'), + prev_index=0, + script_type=InputScriptType.SPENDP2SHWITNESS, + sequence=0xffffffff, + ) + out1 = TxOutputType( + address='mhRx1CeVfaayqRwq5zgRQmD7W5aWBfD5mC', + amount=12300000, + script_type=OutputScriptType.PAYTOADDRESS, + address_n=None, + ) + out2 = TxOutputType( + address_n = [49 | 0x80000000, 1 | 0x80000000, 0 | 0x80000000, 1, 0], + script_type = OutputScriptType.PAYTOP2SHWITNESS, + amount = 123456789 - 11000 - 12300000, + ) + tx = SignTx(coin_name='Testnet', version=None, lock_time=None, inputs_count=1, outputs_count=2) + + messages = [ + None, + + # check fee + TxRequest(request_type=TXINPUT, details=TxRequestDetailsType(request_index=0, tx_hash=None)), + TxAck(tx=TransactionType(inputs=[inp1])), + + TxRequest(request_type=TXOUTPUT, details=TxRequestDetailsType(request_index=0, tx_hash=None), + serialized=None), + TxAck(tx=TransactionType(outputs=[out1])), + + signing.UiConfirmOutput(out1, coin), + True, + + TxRequest(request_type=TXOUTPUT, details=TxRequestDetailsType(request_index=1, tx_hash=None), + serialized=None), + TxAck(tx=TransactionType(outputs=[out2])), + + signing.UiConfirmTotal(12300000, 11000, coin), + True, + + # sign tx + TxRequest(request_type=TXINPUT, details=TxRequestDetailsType(request_index=0, tx_hash=None), + serialized=None), + TxAck(tx=TransactionType(inputs=[inp1])), + + TxRequest(request_type=TXOUTPUT, details=TxRequestDetailsType(request_index=0, tx_hash=None), + serialized=TxRequestSerializedType( + # returned serialized inp1 + serialized_tx=unhexlify( + '0100000000010137c361fb8f2d9056ba8c98c5611930fcb48cacfdd0fe2e0449d83eea982f91200000000017160014d16b8c0680c61fc6ed2e407455715055e41052f5ffffffff'), + )), + TxAck(tx=TransactionType(outputs=[out1])), + # here + TxRequest(request_type=TXOUTPUT, details=TxRequestDetailsType(request_index=1, tx_hash=None), + serialized=TxRequestSerializedType( + # returned serialized out1 + serialized_tx=unhexlify( + '02e0aebb00000000001976a91414fdede0ddc3be652a0ce1afbc1b509a55b6b94888ac'), + signature_index=None, + signature=None, + )), + TxAck(tx=TransactionType(outputs=[out2])), + + # segwit + TxRequest(request_type=TXINPUT, details=TxRequestDetailsType(request_index=0, tx_hash=None), + serialized=TxRequestSerializedType( + # returned serialized out2 + serialized_tx=unhexlify( + '3df39f060000000017a91458b53ea7f832e8f096e896b8713a8c6df0e892ca87'), + signature_index=None, + signature=None, + )), + TxAck(tx=TransactionType(inputs=[inp1])), + + TxRequest(request_type=TXFINISHED, details=None, serialized=TxRequestSerializedType( + serialized_tx=unhexlify( + '02483045022100ccd253bfdf8a5593cd7b6701370c531199f0f05a418cd547dfc7da3f21515f0f02203fa08a0753688871c220648f9edadbdb98af42e5d8269364a326572cf703895b012103e7bfe10708f715e8538c92d46ca50db6f657bbc455b7494e6a0303ccdb868b7900000000'), + signature_index=None, + signature=None, + )), + ] + + signer = signing.sign_tx(tx, root) + for request, response in chunks(messages, 2): + self.assertEqualEx(signer.send(request), response) + with self.assertRaises(StopIteration): + signer.send(None) + + def assertEqualEx(self, a, b): # hack to avoid adding __eq__ to signing.Ui* classes if ((isinstance(a, signing.UiConfirmOutput) and isinstance(b, signing.UiConfirmOutput)) or