From 039f6bad431ed6d59297a0d13f01934bb50765a2 Mon Sep 17 00:00:00 2001 From: Tomas Susanka Date: Fri, 20 Oct 2017 13:09:05 +0200 Subject: [PATCH 01/23] wallet/signing: fee checking seperated --- src/apps/wallet/sign_tx/signing.py | 40 +++++++++++++++++++----------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/src/apps/wallet/sign_tx/signing.py b/src/apps/wallet/sign_tx/signing.py index dfd05a24ae..6c955498a4 100644 --- a/src/apps/wallet/sign_tx/signing.py +++ b/src/apps/wallet/sign_tx/signing.py @@ -137,21 +137,10 @@ def sanitize_tx_binoutput(tx: TransactionType) -> TxOutputBinType: # Transaction signing # === +async def check_tx_fee(tx: SignTx, root, segwit=False): -async def sign_tx(tx: SignTx, root): - - tx = sanitize_sign_tx(tx) coin = coins.by_name(tx.coin_name) - # Phase 1 - # - check inputs, previous transactions, and outputs - # - ask for confirmations - # - check fee - - total_in = 0 # sum of input amounts - total_out = 0 # sum of output amounts - change_out = 0 # change output amount - # h_first is used to make sure the inputs and outputs streamed in Phase 1 # are the same as in Phase 2. it is thus not required to fully hash the # tx, as the SignTx info is streamed only once @@ -161,12 +150,19 @@ async def sign_tx(tx: SignTx, root): tx_req = TxRequest() tx_req.details = TxRequestDetailsType() + total_in = 0 # sum of input amounts + total_out = 0 # sum of output amounts + change_out = 0 # change output amount + for i in range(tx.inputs_count): # STAGE_REQUEST_1_INPUT txi = await request_tx_input(tx_req, i) write_tx_input_check(h_first, txi) - total_in += await get_prevtx_output_value( - tx_req, txi.prev_hash, txi.prev_index) + if segwit: + total_in += txi.amount + else: + total_in += await get_prevtx_output_value( + tx_req, txi.prev_hash, txi.prev_index) for o in range(tx.outputs_count): # STAGE_REQUEST_3_OUTPUT @@ -186,7 +182,6 @@ async def sign_tx(tx: SignTx, root): total_out += txo_bin.amount fee = total_in - total_out - if fee < 0: raise SigningError(FailureType.NotEnoughFunds, 'Not enough funds') @@ -200,10 +195,25 @@ async def sign_tx(tx: SignTx, root): raise SigningError(FailureType.ActionCancelled, 'Total cancelled') + return h_first, tx_req, txo_bin + + +async def sign_tx(tx: SignTx, root): + + tx = sanitize_sign_tx(tx) + + # Phase 1 + # - check inputs, previous transactions, and outputs + # - ask for confirmations + # - check fee + + h_first, tx_req, txo_bin = await check_tx_fee(tx, root) + # Phase 2 # - sign inputs # - check that nothing changed + coin = coins.by_name(tx.coin_name) tx_ser = TxRequestSerializedType() for i_sign in range(tx.inputs_count): From 81ec2f3c657ac03b579973f37791d2871da834e0 Mon Sep 17 00:00:00 2001 From: Tomas Susanka Date: Thu, 26 Oct 2017 13:23:30 +0200 Subject: [PATCH 02/23] wallet/signing: hash writers and serialization moved to seperate file --- src/apps/wallet/sign_tx/signing.py | 130 +--------------------------- src/apps/wallet/sign_tx/writers.py | 132 +++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 128 deletions(-) create mode 100644 src/apps/wallet/sign_tx/writers.py diff --git a/src/apps/wallet/sign_tx/signing.py b/src/apps/wallet/sign_tx/signing.py index 6c955498a4..ae5ab8fe6c 100644 --- a/src/apps/wallet/sign_tx/signing.py +++ b/src/apps/wallet/sign_tx/signing.py @@ -6,8 +6,6 @@ from trezor.utils import ensure from trezor.messages.CoinType import CoinType from trezor.messages.SignTx import SignTx from trezor.messages.TxOutputType import TxOutputType -from trezor.messages.TxOutputBinType import TxOutputBinType -from trezor.messages.TxInputType import TxInputType from trezor.messages.TxRequest import TxRequest from trezor.messages.TransactionType import TransactionType from trezor.messages.RequestType import TXINPUT, TXOUTPUT, TXMETA, TXFINISHED @@ -17,6 +15,7 @@ from trezor.messages import OutputScriptType, InputScriptType, FailureType from apps.common import address_type from apps.common import coins +from apps.wallet.sign_tx.writers import * # Machine instructions @@ -137,6 +136,7 @@ def sanitize_tx_binoutput(tx: TransactionType) -> TxOutputBinType: # Transaction signing # === +# Phase 1 async def check_tx_fee(tx: SignTx, root, segwit=False): coin = coins.by_name(tx.coin_name) @@ -337,15 +337,6 @@ async def get_prevtx_output_value(tx_req: TxRequest, prev_hash: bytes, prev_inde return total_out -def get_tx_hash(w, double: bool, reverse: bool=False) -> bytes: - d = w.getvalue() - if double: - d = sha256(d).digest() - if reverse: - d = bytes(reversed(d)) - return d - - def estimate_tx_size(inputs, outputs): return 10 + inputs * 149 + outputs * 35 @@ -479,120 +470,3 @@ def script_spendaddress_new(pubkey: bytes, signature: bytes) -> bytearray: write_op_push(w, len(pubkey)) write_bytes(w, pubkey) return w - - -# TX Serialization -# === - -_DEFAULT_SEQUENCE = 4294967295 - - -def write_tx_input(w, i: TxInputType): - i_sequence = i.sequence if i.sequence is not None else _DEFAULT_SEQUENCE - write_bytes_rev(w, i.prev_hash) - write_uint32(w, i.prev_index) - write_varint(w, len(i.script_sig)) - write_bytes(w, i.script_sig) - write_uint32(w, i_sequence) - - -def write_tx_input_check(w, i: TxInputType): - i_sequence = i.sequence if i.sequence is not None else _DEFAULT_SEQUENCE - write_bytes(w, i.prev_hash) - write_uint32(w, i.prev_index) - write_uint32(w, len(i.address_n)) - for n in i.address_n: - write_uint32(w, n) - write_uint32(w, i_sequence) - - -def write_tx_output(w, o: TxOutputBinType): - write_uint64(w, o.amount) - write_varint(w, len(o.script_pubkey)) - write_bytes(w, o.script_pubkey) - - -def write_op_push(w, n: int): - if n < 0x4C: - w.append(n & 0xFF) - elif n < 0xFF: - w.append(0x4C) - w.append(n & 0xFF) - elif n < 0xFFFF: - w.append(0x4D) - w.append(n & 0xFF) - w.append((n >> 8) & 0xFF) - else: - w.append(0x4E) - w.append(n & 0xFF) - w.append((n >> 8) & 0xFF) - w.append((n >> 16) & 0xFF) - w.append((n >> 24) & 0xFF) - - -# Buffer IO & Serialization -# === - - -def write_varint(w, n: int): - if n < 253: - w.append(n & 0xFF) - elif n < 65536: - w.append(253) - w.append(n & 0xFF) - w.append((n >> 8) & 0xFF) - else: - w.append(254) - w.append(n & 0xFF) - w.append((n >> 8) & 0xFF) - w.append((n >> 16) & 0xFF) - w.append((n >> 24) & 0xFF) - - -def write_uint32(w, n: int): - w.append(n & 0xFF) - w.append((n >> 8) & 0xFF) - w.append((n >> 16) & 0xFF) - w.append((n >> 24) & 0xFF) - - -def write_uint64(w, n: int): - w.append(n & 0xFF) - w.append((n >> 8) & 0xFF) - w.append((n >> 16) & 0xFF) - w.append((n >> 24) & 0xFF) - w.append((n >> 32) & 0xFF) - w.append((n >> 40) & 0xFF) - w.append((n >> 48) & 0xFF) - w.append((n >> 56) & 0xFF) - - -def write_bytes(w, buf: bytearray): - w.extend(buf) - - -def write_bytes_rev(w, buf: bytearray): - w.extend(bytearray(reversed(buf))) - - -def bytearray_with_cap(cap: int) -> bytearray: - b = bytearray(cap) - b[:] = bytes() - return b - - -class HashWriter: - - def __init__(self, hashfunc): - self.ctx = hashfunc() - self.buf = bytearray(1) # used in append() - - def extend(self, buf: bytearray): - self.ctx.update(buf) - - def append(self, b: int): - self.buf[0] = b - self.ctx.update(self.buf) - - def getvalue(self) -> bytes: - return self.ctx.digest() diff --git a/src/apps/wallet/sign_tx/writers.py b/src/apps/wallet/sign_tx/writers.py new file mode 100644 index 0000000000..c5eea29d06 --- /dev/null +++ b/src/apps/wallet/sign_tx/writers.py @@ -0,0 +1,132 @@ +from trezor.messages.TxOutputBinType import TxOutputBinType +from trezor.messages.TxInputType import TxInputType +from trezor.crypto.hashlib import sha256 + +# TX Serialization +# === + +_DEFAULT_SEQUENCE = 4294967295 + + +def write_tx_input(w, i: TxInputType): + i_sequence = i.sequence if i.sequence is not None else _DEFAULT_SEQUENCE + write_bytes_rev(w, i.prev_hash) + write_uint32(w, i.prev_index) + write_varint(w, len(i.script_sig)) + write_bytes(w, i.script_sig) + write_uint32(w, i_sequence) + + +def write_tx_input_check(w, i: TxInputType): + i_sequence = i.sequence if i.sequence is not None else _DEFAULT_SEQUENCE + write_bytes(w, i.prev_hash) + write_uint32(w, i.prev_index) + write_uint32(w, len(i.address_n)) + for n in i.address_n: + write_uint32(w, n) + write_uint32(w, i_sequence) + + +def write_tx_output(w, o: TxOutputBinType): + write_uint64(w, o.amount) + write_varint(w, len(o.script_pubkey)) + write_bytes(w, o.script_pubkey) + + +def write_op_push(w, n: int): + if n < 0x4C: + w.append(n & 0xFF) + elif n < 0xFF: + w.append(0x4C) + w.append(n & 0xFF) + elif n < 0xFFFF: + w.append(0x4D) + w.append(n & 0xFF) + w.append((n >> 8) & 0xFF) + else: + w.append(0x4E) + w.append(n & 0xFF) + w.append((n >> 8) & 0xFF) + w.append((n >> 16) & 0xFF) + w.append((n >> 24) & 0xFF) + + +# Buffer IO & Serialization +# === + + +def write_varint(w, n: int): + if n < 253: + w.append(n & 0xFF) + elif n < 65536: + w.append(253) + w.append(n & 0xFF) + w.append((n >> 8) & 0xFF) + else: + w.append(254) + w.append(n & 0xFF) + w.append((n >> 8) & 0xFF) + w.append((n >> 16) & 0xFF) + w.append((n >> 24) & 0xFF) + + +def write_uint32(w, n: int): + w.append(n & 0xFF) + w.append((n >> 8) & 0xFF) + w.append((n >> 16) & 0xFF) + w.append((n >> 24) & 0xFF) + + +def write_uint64(w, n: int): + w.append(n & 0xFF) + w.append((n >> 8) & 0xFF) + w.append((n >> 16) & 0xFF) + w.append((n >> 24) & 0xFF) + w.append((n >> 32) & 0xFF) + w.append((n >> 40) & 0xFF) + w.append((n >> 48) & 0xFF) + w.append((n >> 56) & 0xFF) + + +def write_bytes(w, buf: bytearray): + w.extend(buf) + + +def write_bytes_rev(w, buf: bytearray): + w.extend(bytearray(reversed(buf))) + + +def bytearray_with_cap(cap: int) -> bytearray: + b = bytearray(cap) + b[:] = bytes() + return b + + +# Hashes +# === + + +def get_tx_hash(w, double: bool, reverse: bool=False) -> bytes: + d = w.getvalue() + if double: + d = sha256(d).digest() + if reverse: + d = bytes(reversed(d)) + return d + + +class HashWriter: + + def __init__(self, hashfunc): + self.ctx = hashfunc() + self.buf = bytearray(1) # used in append() + + def extend(self, buf: bytearray): + self.ctx.update(buf) + + def append(self, b: int): + self.buf[0] = b + self.ctx.update(self.buf) + + def getvalue(self) -> bytes: + return self.ctx.digest() From bcef96105912fb689d4e3a42d0e3b21e59e7f7f0 Mon Sep 17 00:00:00 2001 From: Tomas Susanka Date: Tue, 24 Oct 2017 16:48:07 +0200 Subject: [PATCH 03/23] wallet/signing: bip143 transactions hashing --- src/apps/wallet/sign_tx/segwit_bip143.py | 50 ++++++++++ src/apps/wallet/sign_tx/signing.py | 115 +++++++++++++---------- tests/test_apps.wallet.segwit.bip143.py | 84 +++++++++++++++++ 3 files changed, 200 insertions(+), 49 deletions(-) create mode 100644 src/apps/wallet/sign_tx/segwit_bip143.py create mode 100644 tests/test_apps.wallet.segwit.bip143.py diff --git a/src/apps/wallet/sign_tx/segwit_bip143.py b/src/apps/wallet/sign_tx/segwit_bip143.py new file mode 100644 index 0000000000..846d37b25b --- /dev/null +++ b/src/apps/wallet/sign_tx/segwit_bip143.py @@ -0,0 +1,50 @@ +from trezor.crypto.hashlib import sha256 +from trezor.messages.SignTx import SignTx + + +class Bip143: + + def __init__(self): + self.h_prevouts = HashWriter(sha256) + self.h_sequence = HashWriter(sha256) + self.h_outputs = HashWriter(sha256) + + def add_prevouts(self, txi: TxInputType): + write_bytes(self.h_prevouts, txi.prev_hash) + write_uint32(self.h_prevouts, txi.prev_index) + + def get_prevouts_hash(self) -> bytes: + return get_tx_hash(self.h_prevouts, True) + + def add_sequence(self, txi: TxInputType): + write_uint32(self.h_sequence, txi.sequence) + + def get_sequence_hash(self) -> bytes: + return get_tx_hash(self.h_sequence, True) + + def add_output(self, txo_bin: TxOutputBinType): + write_tx_output(self.h_outputs, txo_bin) + + def get_outputs_hash(self) -> bytes: + return get_tx_hash(self.h_outputs, True) + + def preimage(self, tx: SignTx, txi: TxInputType, script_code) -> bytes: + h_preimage = HashWriter(sha256) + + write_uint32(h_preimage, tx.version) # nVersion + write_bytes(h_preimage, bytearray(self.get_prevouts_hash())) # hashPrevouts + write_bytes(h_preimage, bytearray(self.get_sequence_hash())) # hashSequence + write_bytes(h_preimage, txi.prev_hash) # outpoint + write_uint32(h_preimage, txi.prev_index) # outpoint + + write_varint(h_preimage, len(script_code)) # scriptCode length + write_bytes(h_preimage, bytearray(script_code)) # scriptCode + + write_uint64(h_preimage, txi.amount) # amount + write_uint32(h_preimage, txi.sequence) # nSequence + + 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 + + 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 ae5ab8fe6c..24f34a3ba7 100644 --- a/src/apps/wallet/sign_tx/signing.py +++ b/src/apps/wallet/sign_tx/signing.py @@ -15,9 +15,9 @@ from trezor.messages import OutputScriptType, InputScriptType, FailureType from apps.common import address_type from apps.common import coins +from apps.wallet.sign_tx.segwit_bip143 import * from apps.wallet.sign_tx.writers import * - # Machine instructions # === @@ -137,7 +137,10 @@ def sanitize_tx_binoutput(tx: TransactionType) -> TxOutputBinType: # === # Phase 1 -async def check_tx_fee(tx: SignTx, root, segwit=False): +# - check inputs, previous transactions, and outputs +# - ask for confirmations +# - check fee +async def check_tx_fee(tx: SignTx, root, segwit): coin = coins.by_name(tx.coin_name) @@ -145,6 +148,7 @@ async def check_tx_fee(tx: SignTx, root, segwit=False): # are the same as in Phase 2. it is thus not required to fully hash the # tx, as the SignTx info is streamed only once h_first = HashWriter(sha256) # not a real tx hash + bip143 = Bip143() txo_bin = TxOutputBinType() tx_req = TxRequest() @@ -159,6 +163,9 @@ async def check_tx_fee(tx: SignTx, root, segwit=False): txi = await request_tx_input(tx_req, i) write_tx_input_check(h_first, txi) if segwit: + # Add I to segwit hash_prevouts, hash_sequence + bip143.add_prevouts(txi) + bip143.add_sequence(txi) total_in += txi.amount else: total_in += await get_prevtx_output_value( @@ -179,6 +186,7 @@ async def check_tx_fee(tx: SignTx, root, segwit=False): txo_bin.amount = txo.amount txo_bin.script_pubkey = output_derive_script(txo, coin, root) write_tx_output(h_first, txo_bin) + bip143.add_output(txo_bin) total_out += txo_bin.amount fee = total_in - total_out @@ -195,19 +203,16 @@ async def check_tx_fee(tx: SignTx, root, segwit=False): raise SigningError(FailureType.ActionCancelled, 'Total cancelled') - return h_first, tx_req, txo_bin + return h_first, tx_req, txo_bin, bip143 -async def sign_tx(tx: SignTx, root): +async def sign_tx(tx: SignTx, root, segwit=False): tx = sanitize_sign_tx(tx) # Phase 1 - # - check inputs, previous transactions, and outputs - # - ask for confirmations - # - check fee - h_first, tx_req, txo_bin = await check_tx_fee(tx, root) + h_first, tx_req, txo_bin, bip143 = await check_tx_fee(tx, root, segwit) # Phase 2 # - sign inputs @@ -230,55 +235,64 @@ async def sign_tx(tx: SignTx, root): write_varint(h_sign, tx.inputs_count) - for i in range(tx.inputs_count): - # STAGE_REQUEST_4_INPUT - txi = await request_tx_input(tx_req, i) - write_tx_input_check(h_second, txi) - if i == i_sign: - txi_sign = txi - key_sign = node_derive(root, txi.address_n) - key_sign_pub = key_sign.public_key() - txi.script_sig = input_derive_script(txi, key_sign_pub) - else: - txi.script_sig = bytes() - write_tx_input(h_sign, txi) + if segwit: + txi = await request_tx_input(tx_req, i_sign) + # if hashType != ANYONE_CAN_PAY ? todo - write_varint(h_sign, tx.outputs_count) + # todo: what to do with other types? + script_code = input_derive_script(txi, coin, root) + bip143.preimage(tx, txi, script_code) + # Return serialized input chunk ? todo + else: + for i in range(tx.inputs_count): + # STAGE_REQUEST_4_INPUT + txi = await request_tx_input(tx_req, i) + write_tx_input_check(h_second, txi) + if i == i_sign: + txi_sign = txi + key_sign = node_derive(root, txi.address_n) + key_sign_pub = key_sign.public_key() + txi.script_sig = input_derive_script(txi, key_sign_pub) + else: + txi.script_sig = bytes() + write_tx_input(h_sign, txi) - for o in range(tx.outputs_count): - # STAGE_REQUEST_4_OUTPUT - txo = await request_tx_output(tx_req, o) - txo_bin.amount = txo.amount - txo_bin.script_pubkey = output_derive_script(txo, coin, root) - write_tx_output(h_second, txo_bin) - write_tx_output(h_sign, txo_bin) + write_varint(h_sign, tx.outputs_count) - write_uint32(h_sign, tx.lock_time) + for o in range(tx.outputs_count): + # STAGE_REQUEST_4_OUTPUT + txo = await request_tx_output(tx_req, o) + txo_bin.amount = txo.amount + txo_bin.script_pubkey = output_derive_script(txo, coin, root) + write_tx_output(h_second, txo_bin) + write_tx_output(h_sign, txo_bin) - write_uint32(h_sign, 0x00000001) # SIGHASH_ALL hash_type + write_uint32(h_sign, tx.lock_time) - # check the control digests - if get_tx_hash(h_first, False) != get_tx_hash(h_second, False): - raise SigningError(FailureType.ProcessError, - 'Transaction has changed during signing') + write_uint32(h_sign, 0x00000001) # SIGHASH_ALL hash_type - # compute the signature from the tx digest - signature = ecdsa_sign(key_sign, get_tx_hash(h_sign, True)) - tx_ser.signature_index = i_sign - tx_ser.signature = signature + # check the control digests + if get_tx_hash(h_first, False) != get_tx_hash(h_second, False): + raise SigningError(FailureType.ProcessError, + 'Transaction has changed during signing') - # serialize input with correct signature - txi_sign.script_sig = input_derive_script( - txi_sign, key_sign_pub, signature) - w_txi_sign = bytearray_with_cap( - len(txi_sign.prev_hash) + 4 + 5 + len(txi_sign.script_sig) + 4) - if i_sign == 0: # serializing first input => prepend tx version and inputs count - write_uint32(w_txi_sign, tx.version) - write_varint(w_txi_sign, tx.inputs_count) - write_tx_input(w_txi_sign, txi_sign) - tx_ser.serialized_tx = w_txi_sign + # compute the signature from the tx digest + signature = ecdsa_sign(key_sign, get_tx_hash(h_sign, True)) + tx_ser.signature_index = i_sign + tx_ser.signature = signature - tx_req.serialized = tx_ser + # serialize input with correct signature + txi_sign.script_sig = input_derive_script( + txi_sign, key_sign_pub, signature) + w_txi_sign = bytearray_with_cap( + len(txi_sign.prev_hash) + 4 + 5 + len(txi_sign.script_sig) + 4) + if i_sign == 0: # serializing first input => prepend tx version and inputs count + write_uint32(w_txi_sign, tx.version) + write_varint(w_txi_sign, tx.inputs_count) + write_tx_input(w_txi_sign, txi_sign) + tx_ser.serialized_tx = w_txi_sign + + tx_req.serialized = tx_ser for o in range(tx.outputs_count): # STAGE_REQUEST_5_OUTPUT @@ -401,6 +415,9 @@ def input_derive_script(i: TxInputType, pubkey: bytes, signature: bytes=None) -> else: return script_spendaddress_new(pubkey, signature) + if i.script_type == InputScriptType.SPENDP2SHWITNESS: # todo + return script_paytoaddress_new(ecdsa_hash_pubkey(pubkey)) + else: raise SigningError(FailureType.SyntaxError, 'Unknown input script type') diff --git a/tests/test_apps.wallet.segwit.bip143.py b/tests/test_apps.wallet.segwit.bip143.py new file mode 100644 index 0000000000..514b4db8e9 --- /dev/null +++ b/tests/test_apps.wallet.segwit.bip143.py @@ -0,0 +1,84 @@ +from common import * + +from apps.wallet.sign_tx.signing import * +from apps.common import coins +from trezor.messages.SignTx import SignTx +from trezor.messages.TxInputType import TxInputType +from trezor.messages.TxOutputType import TxOutputType +from trezor.messages import OutputScriptType +from trezor.crypto import bip32, bip39 + + +class TestSegwitBip143(unittest.TestCase): + # pylint: disable=C0301 + + tx = SignTx(coin_name='Bitcoin', version=1, lock_time=0x00000492, inputs_count=1, outputs_count=2) + inp1 = TxInputType(address_n=[0], + prev_hash=unhexlify('db6b1b20aa0fd7b23880be2ecbd4a98130974cf4748fb66092ac4d3ceb1a5477'), + prev_index=1, + amount=1000000000, # 10 btc + script_type=InputScriptType.SPENDP2SHWITNESS, # todo is this correct? + sequence=0xfffffffe) + out1 = TxOutputType(address='1Fyxts6r24DpEieygQiNnWxUdb18ANa5p7', + amount=0x000000000bebb4b8, + script_type=OutputScriptType.PAYTOWITNESS, + address_n=None) + out2 = TxOutputType(address='1Q5YjKVj5yQWHBBsyEBamkfph3cA6G9KK8', + amount=0x000000002faf0800, + script_type=OutputScriptType.PAYTOWITNESS, + address_n=None) + + def test_bip143_prevouts(self): + + bip143 = Bip143() + bip143.add_prevouts(self.inp1) + self.assertEqual(hexlify(bip143.get_prevouts_hash()), b'b0287b4a252ac05af83d2dcef00ba313af78a3e9c329afa216eb3aa2a7b4613a') + + def test_bip143_sequence(self): + + bip143 = Bip143() + bip143.add_sequence(self.inp1) + self.assertEqual(hexlify(bip143.get_sequence_hash()), b'18606b350cd8bf565266bc352f0caddcf01e8fa789dd8a15386327cf8cabe198') + + def test_bip143_outputs(self): + + seed = bip39.seed('alcohol woman abuse must during monitor noble actual mixed trade anger aisle', '') + root = bip32.from_seed(seed, 'secp256k1') + coin = coins.by_name(self.tx.coin_name) + + bip143 = Bip143() + + for txo in [self.out1, self.out2]: + txo_bin = TxOutputBinType() + txo_bin.amount = txo.amount + txo_bin.script_pubkey = output_derive_script(txo, coin, root) + bip143.add_output(txo_bin) + + self.assertEqual(hexlify(bip143.get_outputs_hash()), + b'de984f44532e2173ca0d64314fcefe6d30da6f8cf27bafa706da61df8a226c83') + + def test_bip143_preimage_testdata(self): + + seed = bip39.seed('alcohol woman abuse must during monitor noble actual mixed trade anger aisle', '') + root = bip32.from_seed(seed, 'secp256k1') + coin = coins.by_name(self.tx.coin_name) + + bip143 = Bip143() + bip143.add_prevouts(self.inp1) + bip143.add_sequence(self.inp1) + for txo in [self.out1, self.out2]: + txo_bin = TxOutputBinType() + txo_bin.amount = txo.amount + txo_bin.script_pubkey = output_derive_script(txo, coin, root) + bip143.add_output(txo_bin) + + # test data public key + script_code = input_derive_script(self.inp1, unhexlify('03ad1d8e89212f0b92c74d23bb710c00662ad1470198ac48c43f7d6f93a2a26873')) + self.assertEqual(hexlify(script_code), b'76a91479091972186c449eb1ded22b78e40d009bdf008988ac') + result = bip143.preimage(self.tx, self.inp1, script_code) + self.assertEqual(hexlify(result), b'64f3b0f4dd2bb3aa1ce8566d220cc74dda9df97d8490cc81d89d735c92e59fb6') + + + +if __name__ == '__main__': + unittest.main() From e63d0adc23c78e0ae3001b65b144e49ce7806e47 Mon Sep 17 00:00:00 2001 From: Tomas Susanka Date: Mon, 30 Oct 2017 11:42:22 +0100 Subject: [PATCH 04/23] wallet/signing: segwit first test passing --- src/apps/wallet/sign_tx/segwit_bip143.py | 30 +++++- src/apps/wallet/sign_tx/signing.py | 101 ++++++++++++++---- tests/test_apps.wallet.segwit.bip143.py | 13 ++- tests/test_apps.wallet.segwit.signtx.py | 125 +++++++++++++++++++++++ 4 files changed, 237 insertions(+), 32 deletions(-) create mode 100644 tests/test_apps.wallet.segwit.signtx.py diff --git a/src/apps/wallet/sign_tx/segwit_bip143.py b/src/apps/wallet/sign_tx/segwit_bip143.py index 846d37b25b..eaa23dd345 100644 --- a/src/apps/wallet/sign_tx/segwit_bip143.py +++ b/src/apps/wallet/sign_tx/segwit_bip143.py @@ -1,5 +1,10 @@ from trezor.crypto.hashlib import sha256 from trezor.messages.SignTx import SignTx +from trezor.messages import InputScriptType, FailureType + + +class Bip143Error(ValueError): + pass class Bip143: @@ -10,7 +15,7 @@ class Bip143: self.h_outputs = HashWriter(sha256) def add_prevouts(self, txi: TxInputType): - write_bytes(self.h_prevouts, txi.prev_hash) + write_bytes_rev(self.h_prevouts, txi.prev_hash) write_uint32(self.h_prevouts, txi.prev_index) def get_prevouts_hash(self) -> bytes: @@ -28,17 +33,18 @@ class Bip143: def get_outputs_hash(self) -> bytes: return get_tx_hash(self.h_outputs, True) - def preimage(self, tx: SignTx, txi: TxInputType, script_code) -> bytes: + def preimage_hash(self, tx: SignTx, txi: TxInputType, pubkeyhash) -> bytes: h_preimage = HashWriter(sha256) write_uint32(h_preimage, tx.version) # nVersion write_bytes(h_preimage, bytearray(self.get_prevouts_hash())) # hashPrevouts write_bytes(h_preimage, bytearray(self.get_sequence_hash())) # hashSequence - write_bytes(h_preimage, txi.prev_hash) # outpoint + write_bytes_rev(h_preimage, txi.prev_hash) # outpoint write_uint32(h_preimage, txi.prev_index) # outpoint + script_code = self.derive_script_code(txi, pubkeyhash) write_varint(h_preimage, len(script_code)) # scriptCode length - write_bytes(h_preimage, bytearray(script_code)) # scriptCode + write_bytes(h_preimage, script_code) # scriptCode write_uint64(h_preimage, txi.amount) # amount write_uint32(h_preimage, txi.sequence) # nSequence @@ -48,3 +54,19 @@ class Bip143: write_uint32(h_preimage, 0x00000001) # nHashType todo return get_tx_hash(h_preimage, True) + + # this not redeemScript nor scriptPubKey + # for P2WPKH this is always 0x1976a914{20-byte-pubkey-hash}88ac + def derive_script_code(self, txi: TxInputType, pubkeyhash: bytes) -> bytearray: + if txi.script_type == InputScriptType.SPENDP2SHWITNESS: + s = bytearray(25) + s[0] = 0x76 # OP_DUP + s[1] = 0xA9 # OP_HASH_160 + s[2] = 0x14 # pushing 20 bytes + s[3:23] = pubkeyhash + s[23] = 0x88 # OP_EQUALVERIFY + s[24] = 0xAC # OP_CHECKSIG + return s + else: + raise Bip143Error(FailureType.SyntaxError, + 'Unknown input script type for bip143 script code') diff --git a/src/apps/wallet/sign_tx/signing.py b/src/apps/wallet/sign_tx/signing.py index 24f34a3ba7..9125b00c47 100644 --- a/src/apps/wallet/sign_tx/signing.py +++ b/src/apps/wallet/sign_tx/signing.py @@ -4,14 +4,13 @@ from trezor.crypto import base58, der from trezor.utils import ensure from trezor.messages.CoinType import CoinType -from trezor.messages.SignTx import SignTx from trezor.messages.TxOutputType import TxOutputType from trezor.messages.TxRequest import TxRequest from trezor.messages.TransactionType import TransactionType from trezor.messages.RequestType import TXINPUT, TXOUTPUT, TXMETA, TXFINISHED from trezor.messages.TxRequestSerializedType import TxRequestSerializedType from trezor.messages.TxRequestDetailsType import TxRequestDetailsType -from trezor.messages import OutputScriptType, InputScriptType, FailureType +from trezor.messages import OutputScriptType from apps.common import address_type from apps.common import coins @@ -140,7 +139,7 @@ def sanitize_tx_binoutput(tx: TransactionType) -> TxOutputBinType: # - check inputs, previous transactions, and outputs # - ask for confirmations # - check fee -async def check_tx_fee(tx: SignTx, root, segwit): +async def check_tx_fee(tx: SignTx, root): coin = coins.by_name(tx.coin_name) @@ -157,17 +156,20 @@ async def check_tx_fee(tx: SignTx, root, segwit): total_in = 0 # sum of input amounts total_out = 0 # sum of output amounts change_out = 0 # change output amount + segwit = {} # dict of booleans stating if input is segwit for i in range(tx.inputs_count): # STAGE_REQUEST_1_INPUT txi = await request_tx_input(tx_req, i) write_tx_input_check(h_first, txi) - if segwit: + if txi.script_type == InputScriptType.SPENDP2SHWITNESS: + segwit[i] = True # Add I to segwit hash_prevouts, hash_sequence bip143.add_prevouts(txi) bip143.add_sequence(txi) total_in += txi.amount else: + segwit[i] = False total_in += await get_prevtx_output_value( tx_req, txi.prev_hash, txi.prev_index) @@ -203,16 +205,16 @@ async def check_tx_fee(tx: SignTx, root, segwit): raise SigningError(FailureType.ActionCancelled, 'Total cancelled') - return h_first, tx_req, txo_bin, bip143 + return h_first, tx_req, txo_bin, bip143, segwit -async def sign_tx(tx: SignTx, root, segwit=False): +async def sign_tx(tx: SignTx, root): tx = sanitize_sign_tx(tx) # Phase 1 - h_first, tx_req, txo_bin, bip143 = await check_tx_fee(tx, root, segwit) + h_first, tx_req, txo_bin, bip143, segwit = await check_tx_fee(tx, root) # Phase 2 # - sign inputs @@ -235,14 +237,25 @@ async def sign_tx(tx: SignTx, root, segwit=False): write_varint(h_sign, tx.inputs_count) - if segwit: - txi = await request_tx_input(tx_req, i_sign) - # if hashType != ANYONE_CAN_PAY ? todo + if segwit[i_sign]: + # STAGE_REQUEST_SEGWIT_INPUT + txi_sign = await request_tx_input(tx_req, i_sign) + if txi_sign.script_type == InputScriptType.SPENDP2SHWITNESS: + key_sign = node_derive(root, txi_sign.address_n) + key_sign_pub = key_sign.public_key() + txi_sign.script_sig = input_derive_script(txi_sign, key_sign_pub) + w_txi = bytearray_with_cap( + 7 + len(txi_sign.prev_hash) + 4 + len(txi_sign.script_sig) + 4) + if i_sign == 0: # serializing first input => prepend meta + write_uint32(w_txi, tx.version) + write_varint(w_txi, 0x00) # segwit witness marker + write_varint(w_txi, 0x01) # segwit witness flag + write_varint(w_txi, tx.inputs_count) + write_tx_input(w_txi, txi_sign) + tx_ser.serialized_tx = w_txi + + tx_req.serialized = tx_ser - # todo: what to do with other types? - script_code = input_derive_script(txi, coin, root) - bip143.preimage(tx, txi, script_code) - # Return serialized input chunk ? todo else: for i in range(tx.inputs_count): # STAGE_REQUEST_4_INPUT @@ -285,7 +298,7 @@ async def sign_tx(tx: SignTx, root, segwit=False): txi_sign.script_sig = input_derive_script( txi_sign, key_sign_pub, signature) w_txi_sign = bytearray_with_cap( - len(txi_sign.prev_hash) + 4 + 5 + len(txi_sign.script_sig) + 4) + 5 + len(txi_sign.prev_hash) + 4 + len(txi_sign.script_sig) + 4) if i_sign == 0: # serializing first input => prepend tx version and inputs count write_uint32(w_txi_sign, tx.version) write_varint(w_txi_sign, tx.inputs_count) @@ -306,14 +319,35 @@ async def sign_tx(tx: SignTx, root, segwit=False): if o == 0: # serializing first output => prepend outputs count write_varint(w_txo_bin, tx.outputs_count) write_tx_output(w_txo_bin, txo_bin) - if o == tx.outputs_count - 1: # serializing last output => append tx lock_time - write_uint32(w_txo_bin, tx.lock_time) - tx_ser.signature_index = None + + tx_ser.signature_index = None # @todo delete? tx_ser.signature = None tx_ser.serialized_tx = w_txo_bin tx_req.serialized = tx_ser + for i in range(tx.inputs_count): + if segwit[i]: + # 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)) + + signature = ecdsa_sign(key_sign, bip143_hash) + + witness = get_p2wpkh_witness(signature, key_sign_pub) + + tx_ser.serialized_tx = witness + tx_req.serialized = tx_ser + # else + # witness is 0x00 + + write_uint32(tx_ser.serialized_tx, tx.lock_time) + await request_tx_finish(tx_req) @@ -384,6 +418,7 @@ 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: @@ -415,8 +450,8 @@ def input_derive_script(i: TxInputType, pubkey: bytes, signature: bytes=None) -> else: return script_spendaddress_new(pubkey, signature) - if i.script_type == InputScriptType.SPENDP2SHWITNESS: # todo - return script_paytoaddress_new(ecdsa_hash_pubkey(pubkey)) + if i.script_type == InputScriptType.SPENDP2SHWITNESS: # p2wpkh using p2sh + return script_p2wpkh_in_p2sh(ecdsa_hash_pubkey(pubkey)) else: raise SigningError(FailureType.SyntaxError, @@ -471,6 +506,18 @@ def script_paytoscripthash_new(scripthash: bytes) -> bytearray: return s +# P2WPKH is nested in P2SH to be backwards compatible +# see https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#witness-program +# this pushes 16 00 14 +def script_p2wpkh_in_p2sh(pubkeyhash: bytes) -> bytearray: + w = bytearray_with_cap(3 + len(pubkeyhash)) + write_op_push(w, len(pubkeyhash) + 2) # 0x16 - length of the redeemScript + w.append(0x00) # witness version byte + w.append(0x14) # P2WPKH witness program (pub key hash length + pub key hash) + write_bytes(w, pubkeyhash) + return w + + def script_paytoopreturn_new(data: bytes) -> bytearray: w = bytearray_with_cap(1 + 5 + len(data)) w.append(0x6A) # OP_RETURN @@ -481,9 +528,21 @@ def script_paytoopreturn_new(data: bytes) -> bytearray: def script_spendaddress_new(pubkey: bytes, signature: bytes) -> bytearray: w = bytearray_with_cap(5 + len(signature) + 1 + 5 + len(pubkey)) + append_signature_and_pubkey(w, pubkey, signature) + return w + + +def get_p2wpkh_witness(signature: bytes, pubkey: bytes): + w = bytearray_with_cap(1 + 5 + len(signature) + 1 + 5 + len(pubkey)) + write_varint(w, 0x02) # num of segwit items, in P2WPKH it's always 2 + append_signature_and_pubkey(w, pubkey, signature) + return w + + +def append_signature_and_pubkey(w: bytearray, pubkey: bytes, signature: bytes) -> bytearray: write_op_push(w, len(signature) + 1) write_bytes(w, signature) - w.append(0x01) + w.append(0x01) # SIGHASH_ALL write_op_push(w, len(pubkey)) write_bytes(w, pubkey) return w diff --git a/tests/test_apps.wallet.segwit.bip143.py b/tests/test_apps.wallet.segwit.bip143.py index 514b4db8e9..22f2413c18 100644 --- a/tests/test_apps.wallet.segwit.bip143.py +++ b/tests/test_apps.wallet.segwit.bip143.py @@ -14,18 +14,19 @@ class TestSegwitBip143(unittest.TestCase): tx = SignTx(coin_name='Bitcoin', version=1, lock_time=0x00000492, inputs_count=1, outputs_count=2) inp1 = TxInputType(address_n=[0], - prev_hash=unhexlify('db6b1b20aa0fd7b23880be2ecbd4a98130974cf4748fb66092ac4d3ceb1a5477'), + # Trezor expects hash in reversed format + prev_hash=unhexlify('77541aeb3c4dac9260b68f74f44c973081a9d4cb2ebe8038b2d70faa201b6bdb'), prev_index=1, amount=1000000000, # 10 btc script_type=InputScriptType.SPENDP2SHWITNESS, # todo is this correct? sequence=0xfffffffe) out1 = TxOutputType(address='1Fyxts6r24DpEieygQiNnWxUdb18ANa5p7', amount=0x000000000bebb4b8, - script_type=OutputScriptType.PAYTOWITNESS, + script_type=OutputScriptType.PAYTOADDRESS, address_n=None) out2 = TxOutputType(address='1Q5YjKVj5yQWHBBsyEBamkfph3cA6G9KK8', amount=0x000000002faf0800, - script_type=OutputScriptType.PAYTOWITNESS, + script_type=OutputScriptType.PAYTOADDRESS, address_n=None) def test_bip143_prevouts(self): @@ -72,10 +73,8 @@ class TestSegwitBip143(unittest.TestCase): txo_bin.script_pubkey = output_derive_script(txo, coin, root) bip143.add_output(txo_bin) - # test data public key - script_code = input_derive_script(self.inp1, unhexlify('03ad1d8e89212f0b92c74d23bb710c00662ad1470198ac48c43f7d6f93a2a26873')) - self.assertEqual(hexlify(script_code), b'76a91479091972186c449eb1ded22b78e40d009bdf008988ac') - result = bip143.preimage(self.tx, self.inp1, script_code) + # test data public key hash + result = bip143.preimage_hash(self.tx, self.inp1, unhexlify('79091972186c449eb1ded22b78e40d009bdf0089')) self.assertEqual(hexlify(result), b'64f3b0f4dd2bb3aa1ce8566d220cc74dda9df97d8490cc81d89d735c92e59fb6') diff --git a/tests/test_apps.wallet.segwit.signtx.py b/tests/test_apps.wallet.segwit.signtx.py new file mode 100644 index 0000000000..70da122201 --- /dev/null +++ b/tests/test_apps.wallet.segwit.signtx.py @@ -0,0 +1,125 @@ +from common import * + +from trezor.utils import chunks +from trezor.crypto import bip32, bip39 +from trezor.messages.SignTx import SignTx +from trezor.messages.TxInputType import TxInputType +from trezor.messages.TxOutputType import TxOutputType +from trezor.messages.TxRequest import TxRequest +from trezor.messages.TxAck import TxAck +from trezor.messages.TransactionType import TransactionType +from trezor.messages.RequestType import TXINPUT, TXOUTPUT, TXMETA, TXFINISHED +from trezor.messages.TxRequestDetailsType import TxRequestDetailsType +from trezor.messages.TxRequestSerializedType import TxRequestSerializedType +from trezor.messages import InputScriptType +from trezor.messages import OutputScriptType + +from apps.common import coins +from apps.wallet.sign_tx import signing + +class TestSignSegwitTx(unittest.TestCase): + # pylint: disable=C0301 + + def test_send_p2sh(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='2N1LGaGg836mqSQqiuUBLfcyGBhyZbremDX', + script_type=OutputScriptType.PAYTOSCRIPTHASH, # todo + amount=123456789 - 11000 - 12300000, + address_n=None, + ) + 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.UiConfirmOutput(out2, coin), + True, + + signing.UiConfirmTotal(123445789, 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])), + + 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 + (isinstance(a, signing.UiConfirmTotal) and isinstance(b, signing.UiConfirmTotal))): + return self.assertEqual(a.__dict__, b.__dict__) + else: + return self.assertEqual(a, b) + + +if __name__ == '__main__': + unittest.main() From b7f01baf99185e9e4a09739acd9a2d4ffad9c19f Mon Sep 17 00:00:00 2001 From: Tomas Susanka Date: Fri, 3 Nov 2017 11:49:08 +0100 Subject: [PATCH 05/23] wallet/signing: refactoring --- src/apps/wallet/sign_tx/helpers.py | 121 ++++++++++++++++++ src/apps/wallet/sign_tx/signing.py | 158 +++++------------------- tests/test_apps.wallet.segwit.signtx.py | 2 +- 3 files changed, 150 insertions(+), 131 deletions(-) create mode 100644 src/apps/wallet/sign_tx/helpers.py diff --git a/src/apps/wallet/sign_tx/helpers.py b/src/apps/wallet/sign_tx/helpers.py new file mode 100644 index 0000000000..78d62c3f85 --- /dev/null +++ b/src/apps/wallet/sign_tx/helpers.py @@ -0,0 +1,121 @@ + +from trezor.messages.CoinType import CoinType +from trezor.messages.TxOutputType import TxOutputType +from trezor.messages.TxOutputBinType import TxOutputBinType +from trezor.messages.TxInputType import TxInputType +from trezor.messages.SignTx import SignTx +from trezor.messages.TxRequest import TxRequest +from trezor.messages.TransactionType import TransactionType +from trezor.messages.RequestType import TXINPUT, TXOUTPUT, TXMETA, TXFINISHED +from trezor.messages import InputScriptType + +# Machine instructions +# === + + +class UiConfirmOutput: + + def __init__(self, output: TxOutputType, coin: CoinType): + self.output = output + self.coin = coin + + +class UiConfirmTotal: + + def __init__(self, spending: int, fee: int, coin: CoinType): + self.spending = spending + self.fee = fee + self.coin = coin + + +class UiConfirmFeeOverThreshold: + + def __init__(self, fee: int, coin: CoinType): + self.fee = fee + self.coin = coin + + +def confirm_output(output: TxOutputType, coin: CoinType): + return (yield UiConfirmOutput(output, coin)) + + +def confirm_total(spending: int, fee: int, coin: CoinType): + return (yield UiConfirmTotal(spending, fee, coin)) + + +def confirm_feeoverthreshold(fee: int, coin: CoinType): + return (yield UiConfirmFeeOverThreshold(fee, coin)) + + +def request_tx_meta(tx_req: TxRequest, tx_hash: bytes=None): + tx_req.request_type = TXMETA + tx_req.details.tx_hash = tx_hash + tx_req.details.request_index = None + ack = yield tx_req + tx_req.serialized = None + return sanitize_tx_meta(ack.tx) + + +def request_tx_input(tx_req: TxRequest, i: int, tx_hash: bytes=None): + tx_req.request_type = TXINPUT + tx_req.details.request_index = i + tx_req.details.tx_hash = tx_hash + ack = yield tx_req + tx_req.serialized = None + return sanitize_tx_input(ack.tx) + + +def request_tx_output(tx_req: TxRequest, i: int, tx_hash: bytes=None): + tx_req.request_type = TXOUTPUT + tx_req.details.request_index = i + tx_req.details.tx_hash = tx_hash + ack = yield tx_req + tx_req.serialized = None + if tx_hash is None: + return sanitize_tx_output(ack.tx) + else: + return sanitize_tx_binoutput(ack.tx) + + +def request_tx_finish(tx_req: TxRequest): + tx_req.request_type = TXFINISHED + tx_req.details = None + yield tx_req + tx_req.serialized = None + + +# Data sanitizers +# === + + +def sanitize_sign_tx(tx: SignTx) -> SignTx: + tx.version = tx.version if tx.version is not None else 1 + tx.lock_time = tx.lock_time if tx.lock_time is not None else 0 + tx.inputs_count = tx.inputs_count if tx.inputs_count is not None else 0 + tx.outputs_count = tx.outputs_count if tx.outputs_count is not None else 0 + tx.coin_name = tx.coin_name if tx.coin_name is not None else 'Bitcoin' + return tx + + +def sanitize_tx_meta(tx: TransactionType) -> TransactionType: + tx.version = tx.version if tx.version is not None else 1 + tx.lock_time = tx.lock_time if tx.lock_time is not None else 0 + tx.inputs_cnt = tx.inputs_cnt if tx.inputs_cnt is not None else 0 + tx.outputs_cnt = tx.outputs_cnt if tx.outputs_cnt is not None else 0 + return tx + + +def sanitize_tx_input(tx: TransactionType) -> TxInputType: + txi = tx.inputs[0] + txi.script_type = ( + txi.script_type if txi.script_type is not None else InputScriptType.SPENDADDRESS) + return txi + + +def sanitize_tx_output(tx: TransactionType) -> TxOutputType: + return tx.outputs[0] + + +def sanitize_tx_binoutput(tx: TransactionType) -> TxOutputBinType: + return tx.bin_outputs[0] + diff --git a/src/apps/wallet/sign_tx/signing.py b/src/apps/wallet/sign_tx/signing.py index 9125b00c47..f7af261f24 100644 --- a/src/apps/wallet/sign_tx/signing.py +++ b/src/apps/wallet/sign_tx/signing.py @@ -3,11 +3,6 @@ from trezor.crypto.curve import secp256k1 from trezor.crypto import base58, der from trezor.utils import ensure -from trezor.messages.CoinType import CoinType -from trezor.messages.TxOutputType import TxOutputType -from trezor.messages.TxRequest import TxRequest -from trezor.messages.TransactionType import TransactionType -from trezor.messages.RequestType import TXINPUT, TXOUTPUT, TXMETA, TXFINISHED from trezor.messages.TxRequestSerializedType import TxRequestSerializedType from trezor.messages.TxRequestDetailsType import TxRequestDetailsType from trezor.messages import OutputScriptType @@ -16,124 +11,18 @@ from apps.common import address_type from apps.common import coins from apps.wallet.sign_tx.segwit_bip143 import * from apps.wallet.sign_tx.writers import * - -# Machine instructions -# === +from apps.wallet.sign_tx.helpers import * class SigningError(ValueError): pass -class UiConfirmOutput: - - def __init__(self, output: TxOutputType, coin: CoinType): - self.output = output - self.coin = coin - - -class UiConfirmTotal: - - def __init__(self, spending: int, fee: int, coin: CoinType): - self.spending = spending - self.fee = fee - self.coin = coin - - -class UiConfirmFeeOverThreshold: - - def __init__(self, fee: int, coin: CoinType): - self.fee = fee - self.coin = coin - - -def confirm_output(output: TxOutputType, coin: CoinType): - return (yield UiConfirmOutput(output, coin)) - - -def confirm_total(spending: int, fee: int, coin: CoinType): - return (yield UiConfirmTotal(spending, fee, coin)) - - -def confirm_feeoverthreshold(fee: int, coin: CoinType): - return (yield UiConfirmFeeOverThreshold(fee, coin)) - - -def request_tx_meta(tx_req: TxRequest, tx_hash: bytes=None): - tx_req.request_type = TXMETA - tx_req.details.tx_hash = tx_hash - tx_req.details.request_index = None - ack = yield tx_req - tx_req.serialized = None - return sanitize_tx_meta(ack.tx) - - -def request_tx_input(tx_req: TxRequest, i: int, tx_hash: bytes=None): - tx_req.request_type = TXINPUT - tx_req.details.request_index = i - tx_req.details.tx_hash = tx_hash - ack = yield tx_req - tx_req.serialized = None - return sanitize_tx_input(ack.tx) - - -def request_tx_output(tx_req: TxRequest, i: int, tx_hash: bytes=None): - tx_req.request_type = TXOUTPUT - tx_req.details.request_index = i - tx_req.details.tx_hash = tx_hash - ack = yield tx_req - tx_req.serialized = None - if tx_hash is None: - return sanitize_tx_output(ack.tx) - else: - return sanitize_tx_binoutput(ack.tx) - - -def request_tx_finish(tx_req: TxRequest): - tx_req.request_type = TXFINISHED - tx_req.details = None - yield tx_req - tx_req.serialized = None - - -# Data sanitizers -# === - - -def sanitize_sign_tx(tx: SignTx) -> SignTx: - tx.version = tx.version if tx.version is not None else 1 - tx.lock_time = tx.lock_time if tx.lock_time is not None else 0 - tx.inputs_count = tx.inputs_count if tx.inputs_count is not None else 0 - tx.outputs_count = tx.outputs_count if tx.outputs_count is not None else 0 - tx.coin_name = tx.coin_name if tx.coin_name is not None else 'Bitcoin' - return tx - - -def sanitize_tx_meta(tx: TransactionType) -> TransactionType: - tx.version = tx.version if tx.version is not None else 1 - tx.lock_time = tx.lock_time if tx.lock_time is not None else 0 - tx.inputs_cnt = tx.inputs_cnt if tx.inputs_cnt is not None else 0 - tx.outputs_cnt = tx.outputs_cnt if tx.outputs_cnt is not None else 0 - return tx - - -def sanitize_tx_input(tx: TransactionType) -> TxInputType: - txi = tx.inputs[0] - txi.script_type = ( - txi.script_type if txi.script_type is not None else InputScriptType.SPENDADDRESS) - return txi - - -def sanitize_tx_output(tx: TransactionType) -> TxOutputType: - return tx.outputs[0] - - -def sanitize_tx_binoutput(tx: TransactionType) -> TxOutputBinType: - return tx.bin_outputs[0] - - # Transaction signing # === +# see https://github.com/trezor/trezor-mcu/blob/master/firmware/signing.c#L84 +# for pseudo code overview +# === # Phase 1 # - check inputs, previous transactions, and outputs @@ -246,11 +135,8 @@ async def sign_tx(tx: SignTx, root): txi_sign.script_sig = input_derive_script(txi_sign, key_sign_pub) w_txi = bytearray_with_cap( 7 + len(txi_sign.prev_hash) + 4 + len(txi_sign.script_sig) + 4) - if i_sign == 0: # serializing first input => prepend meta - write_uint32(w_txi, tx.version) - write_varint(w_txi, 0x00) # segwit witness marker - write_varint(w_txi, 0x01) # segwit witness flag - write_varint(w_txi, tx.inputs_count) + if i_sign == 0: # serializing first input => prepend headers + write_bytes(w_txi, get_tx_header(tx, True)) write_tx_input(w_txi, txi_sign) tx_ser.serialized_tx = w_txi @@ -299,9 +185,8 @@ async def sign_tx(tx: SignTx, root): txi_sign, key_sign_pub, signature) w_txi_sign = bytearray_with_cap( 5 + len(txi_sign.prev_hash) + 4 + len(txi_sign.script_sig) + 4) - if i_sign == 0: # serializing first input => prepend tx version and inputs count - write_uint32(w_txi_sign, tx.version) - write_varint(w_txi_sign, tx.inputs_count) + if i_sign == 0: # serializing first input => prepend headers + write_bytes(w_txi_sign, get_tx_header(tx)) write_tx_input(w_txi_sign, txi_sign) tx_ser.serialized_tx = w_txi_sign @@ -389,6 +274,26 @@ def estimate_tx_size(inputs, outputs): return 10 + inputs * 149 + outputs * 35 +# TX Helpers +# === + +def get_tx_header(tx: SignTx, segwit=False): + w_txi = bytearray() + write_uint32(w_txi, tx.version) + if segwit: + write_varint(w_txi, 0x00) # segwit witness marker + write_varint(w_txi, 0x01) # segwit witness flag + write_varint(w_txi, tx.inputs_count) + return w_txi + + +def get_p2wpkh_witness(signature: bytes, pubkey: bytes): + w = bytearray_with_cap(1 + 5 + len(signature) + 1 + 5 + len(pubkey)) + write_varint(w, 0x02) # num of segwit items, in P2WPKH it's always 2 + append_signature_and_pubkey(w, pubkey, signature) + return w + + # TX Outputs # === @@ -532,13 +437,6 @@ def script_spendaddress_new(pubkey: bytes, signature: bytes) -> bytearray: return w -def get_p2wpkh_witness(signature: bytes, pubkey: bytes): - w = bytearray_with_cap(1 + 5 + len(signature) + 1 + 5 + len(pubkey)) - write_varint(w, 0x02) # num of segwit items, in P2WPKH it's always 2 - append_signature_and_pubkey(w, pubkey, signature) - return w - - def append_signature_and_pubkey(w: bytearray, pubkey: bytes, signature: bytes) -> bytearray: write_op_push(w, len(signature) + 1) write_bytes(w, signature) diff --git a/tests/test_apps.wallet.segwit.signtx.py b/tests/test_apps.wallet.segwit.signtx.py index 70da122201..5ba08a5d12 100644 --- a/tests/test_apps.wallet.segwit.signtx.py +++ b/tests/test_apps.wallet.segwit.signtx.py @@ -44,7 +44,7 @@ class TestSignSegwitTx(unittest.TestCase): ) out2 = TxOutputType( address='2N1LGaGg836mqSQqiuUBLfcyGBhyZbremDX', - script_type=OutputScriptType.PAYTOSCRIPTHASH, # todo + script_type=OutputScriptType.PAYTOSCRIPTHASH, # todo! amount=123456789 - 11000 - 12300000, address_n=None, ) From da4bc2dc8b92f434ef8a96c576fb41b6e8e95b63 Mon Sep 17 00:00:00 2001 From: Tomas Susanka Date: Fri, 3 Nov 2017 16:04:39 +0100 Subject: [PATCH 06/23] 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 eaa23dd345..4d45922522 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 f7af261f24..9d252fb6e7 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 0000000000..b9acbd85a6 --- /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 5ba08a5d12..c0c88494f9 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 From b7b46b6980f4c9538aa1fe46c04e5712630c1893 Mon Sep 17 00:00:00 2001 From: Tomas Susanka Date: Thu, 9 Nov 2017 12:45:24 +0100 Subject: [PATCH 07/23] wallet/signing: amount is checked during witness signature with test --- src/apps/wallet/sign_tx/signing.py | 12 ++- tests/test_apps.wallet.segwit.signtx.py | 109 ++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 3 deletions(-) diff --git a/src/apps/wallet/sign_tx/signing.py b/src/apps/wallet/sign_tx/signing.py index 9d252fb6e7..d262576b25 100644 --- a/src/apps/wallet/sign_tx/signing.py +++ b/src/apps/wallet/sign_tx/signing.py @@ -94,7 +94,7 @@ async def check_tx_fee(tx: SignTx, root): raise SigningError(FailureType.ActionCancelled, 'Total cancelled') - return h_first, tx_req, txo_bin, bip143, segwit + return h_first, tx_req, txo_bin, bip143, segwit, total_in async def sign_tx(tx: SignTx, root): @@ -103,7 +103,7 @@ async def sign_tx(tx: SignTx, root): # Phase 1 - h_first, tx_req, txo_bin, bip143, segwit = await check_tx_fee(tx, root) + h_first, tx_req, txo_bin, bip143, segwit, authorized_in = await check_tx_fee(tx, root) # Phase 2 # - sign inputs @@ -215,7 +215,13 @@ async def sign_tx(tx: SignTx, root): if segwit[i]: # STAGE_REQUEST_SEGWIT_WITNESS txi = await request_tx_input(tx_req, i) - # todo check amount? + + # Check amount + if txi.amount > authorized_in: + raise SigningError(FailureType.ProcessError, + 'Transaction has changed during signing') + authorized_in -= txi.amount + 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)) diff --git a/tests/test_apps.wallet.segwit.signtx.py b/tests/test_apps.wallet.segwit.signtx.py index c0c88494f9..d83bee796b 100644 --- a/tests/test_apps.wallet.segwit.signtx.py +++ b/tests/test_apps.wallet.segwit.signtx.py @@ -17,6 +17,7 @@ from trezor.messages import OutputScriptType from apps.common import coins from apps.wallet.sign_tx import signing + class TestSignSegwitTx(unittest.TestCase): # pylint: disable=C0301 @@ -210,6 +211,114 @@ class TestSignSegwitTx(unittest.TestCase): with self.assertRaises(StopIteration): signer.send(None) + # see https://github.com/trezor/trezor-mcu/commit/6b615ce40567cc0da0b3b38ff668916aaae9dd4b#commitcomment-25505919 + # for the rational behind this attack + def test_send_p2wpkh_in_p2sh_attack_amount(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=10, + prev_hash=unhexlify('20912f98ea3ed849042efed0fdac8cb4fc301961c5988cba56902d8ffb61c337'), + prev_index=0, + script_type=InputScriptType.SPENDP2SHWITNESS, + sequence=0xffffffff, + ) + inpattack = TxInputType( + # 49'/1'/0'/1/0" - 2N1LGaGg836mqSQqiuUBLfcyGBhyZbremDX + address_n=[49 | 0x80000000, 1 | 0x80000000, 0 | 0x80000000, 1, 0], + amount=9, # modified! + prev_hash=unhexlify('20912f98ea3ed849042efed0fdac8cb4fc301961c5988cba56902d8ffb61c337'), + prev_index=0, + script_type=InputScriptType.SPENDP2SHWITNESS, + sequence=0xffffffff, + ) + out1 = TxOutputType( + address='mhRx1CeVfaayqRwq5zgRQmD7W5aWBfD5mC', + amount=8, + script_type=OutputScriptType.PAYTOADDRESS, + address_n=None, + ) + out2 = TxOutputType( + address_n = [49 | 0x80000000, 1 | 0x80000000, 0 | 0x80000000, 1, 0], + script_type = OutputScriptType.PAYTOP2SHWITNESS, + amount = 1, + ) + 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=[inpattack])), + + 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(8, 0, coin), + True, + + 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 inpattack + serialized_tx=unhexlify( + '0100000000010137c361fb8f2d9056ba8c98c5611930fcb48cacfdd0fe2e0449d83eea982f91200000000017160014d16b8c0680c61fc6ed2e407455715055e41052f5ffffffff'), + )), + TxAck(tx=TransactionType(outputs=[out1])), + + TxRequest(request_type=TXOUTPUT, details=TxRequestDetailsType(request_index=1, tx_hash=None), + serialized=TxRequestSerializedType( + # returned serialized out1 + serialized_tx=unhexlify( + '0208000000000000001976a91414fdede0ddc3be652a0ce1afbc1b509a55b6b94888ac'), + 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( + '010000000000000017a91458b53ea7f832e8f096e896b8713a8c6df0e892ca87'), + signature_index=None, + signature=None, + )), + TxAck(tx=TransactionType(inputs=[inp1])), + + TxRequest(request_type=TXFINISHED, details=None) + ] + + signer = signing.sign_tx(tx, root) + i = 0 + messages_count = int(len(messages) / 2) + for request, response in chunks(messages, 2): + if i == messages_count - 1: # last message should throw SigningError + self.assertRaises(signing.SigningError, signer.send, request) + else: + self.assertEqualEx(signer.send(request), response) + i += 1 + with self.assertRaises(StopIteration): + signer.send(None) def assertEqualEx(self, a, b): # hack to avoid adding __eq__ to signing.Ui* classes From dc6701af9056e7181b3708b9b55bc271e7a6a8ee Mon Sep 17 00:00:00 2001 From: Tomas Susanka Date: Fri, 10 Nov 2017 14:10:19 +0100 Subject: [PATCH 08/23] wallet/signing: native P2WPKH, P2WPKH in P2SH, scripts --- src/apps/common/address_type.py | 17 +- src/apps/wallet/sign_tx/scripts.py | 118 ++++++++ src/apps/wallet/sign_tx/signing.py | 257 ++++++++---------- src/apps/wallet/sign_tx/writers.py | 2 + tests/test_apps.wallet.segwit.address.py | 18 +- ...apps.wallet.segwit.signtx.native_p2wpkh.py | 215 +++++++++++++++ ...ps.wallet.segwit.signtx.p2wpkh_in_p2sh.py} | 16 +- tests/test_apps.wallet.signtx.py | 1 + 8 files changed, 482 insertions(+), 162 deletions(-) create mode 100644 src/apps/wallet/sign_tx/scripts.py create mode 100644 tests/test_apps.wallet.segwit.signtx.native_p2wpkh.py rename tests/{test_apps.wallet.segwit.signtx.py => test_apps.wallet.segwit.signtx.p2wpkh_in_p2sh.py} (96%) diff --git a/src/apps/common/address_type.py b/src/apps/common/address_type.py index 3bc7807fd1..cc54c34d2c 100644 --- a/src/apps/common/address_type.py +++ b/src/apps/common/address_type.py @@ -28,13 +28,12 @@ def strip(address_type, raw_address): def split(coin, raw_address): - l = None - for f in ['', '_p2sh', '_p2wpkh', '_p2wsh']: - at = getattr(coin, 'address_type' + f) + for f in ('address_type', + 'address_type_p2sh', + 'address_type_p2wpkh', + 'address_type_p2wsh'): + at = getattr(coin, f) if at is not None and check(at, raw_address): - l = length(coin.address_type) - break - if l is not None: - return raw_address[:l], raw_address[l:] - else: - raise ValueError('Invalid addressXXX') + l = length(at) + return raw_address[:l], raw_address[l:] + raise ValueError('Invalid address') diff --git a/src/apps/wallet/sign_tx/scripts.py b/src/apps/wallet/sign_tx/scripts.py new file mode 100644 index 0000000000..5c7a794c98 --- /dev/null +++ b/src/apps/wallet/sign_tx/scripts.py @@ -0,0 +1,118 @@ +from apps.wallet.sign_tx.writers import * + + +# TX Scripts +# === + +# -------------------------- First gen -------------------------- + +# =============== P2PK =============== +# obsolete + + +# =============== P2PKH =============== + +def input_script_p2pkh_or_p2sh(pubkey: bytes, signature: bytes) -> bytearray: + w = bytearray_with_cap(5 + len(signature) + 1 + 5 + len(pubkey)) + append_signature_and_pubkey(w, pubkey, signature) + return w + + +def output_script_p2pkh(pubkeyhash: bytes) -> bytearray: + s = bytearray(25) + s[0] = 0x76 # OP_DUP + s[1] = 0xA9 # OP_HASH_160 + s[2] = 0x14 # pushing 20 bytes + s[3:23] = pubkeyhash + s[23] = 0x88 # OP_EQUALVERIFY + s[24] = 0xAC # OP_CHECKSIG + return s + + +# =============== P2SH =============== +# see https://github.com/bitcoin/bips/blob/master/bip-0016.mediawiki + +# input script (scriptSig) is the same as input_script_p2pkh_or_p2sh + +# output script (scriptPubKey) is A9 14 87 +def output_script_p2sh(scripthash: bytes) -> bytearray: + s = bytearray(23) + s[0] = 0xA9 # OP_HASH_160 + s[1] = 0x14 # pushing 20 bytes + s[2:22] = scripthash + s[22] = 0x87 # OP_EQUAL + return s + + +# -------------------------- SegWit -------------------------- + +# =============== Native P2WPKH =============== +# see https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#p2wpkh +# P2WPKH (Pay-to-Witness-Public-Key-Hash) is the segwit native P2PKH +# not backwards compatible + +# input script is completely replaced by the witness and therefore empty +def input_script_native_p2wpkh_or_p2wsh() -> bytearray: + return bytearray(0) + + +# output script consists of 00 14 <20-byte-key-hash> +def output_script_native_p2wpkh_or_p2wsh(pubkeyhash: bytes) -> bytearray: + w = bytearray_with_cap(3 + len(pubkeyhash)) + w.append(0x00) # witness version byte + w.append(len(pubkeyhash)) # pub key hash length is 20 (P2WPKH) or 32 (P2WSH) bytes + write_bytes(w, pubkeyhash) # pub key hash + return w + + +# =============== Native P2WPKH nested in P2SH =============== +# P2WPKH is nested in P2SH to be backwards compatible +# see https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#witness-program + +# input script (scriptSig) is 16 00 14 +# signature is moved to the witness +def input_script_p2wpkh_in_p2sh(pubkeyhash: bytes) -> bytearray: + w = bytearray_with_cap(3 + len(pubkeyhash)) + w.append(0x16) # 0x16 - length of the redeemScript + w.append(0x00) # witness version byte + w.append(0x14) # P2WPKH witness program (pub key hash length) + write_bytes(w, pubkeyhash) # pub key hash + return w + +# output script (scriptPubKey) is A9 14 87 +# which is same as the output_script_p2sh + + +# =============== Native P2WSH =============== +# see https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#p2wsh +# P2WSH (Pay-to-Witness-Script-Hash) is segwit native P2SH +# not backwards compatible + +# input script is completely replaced by the witness and therefore empty +# same as input_script_native_p2wpkh_or_p2wsh + +# output script consists of 00 20 <32-byte-key-hash> +# same as output_script_native_p2wpkh_or_p2wsh (only different length) + + +# -------------------------- Others -------------------------- + +# === OP_RETURN script + +def output_script_paytoopreturn(data: bytes) -> bytearray: + w = bytearray_with_cap(1 + 5 + len(data)) + w.append(0x6A) # OP_RETURN + write_op_push(w, len(data)) + w.extend(data) + return w + + +# === helpers + +def append_signature_and_pubkey(w: bytearray, pubkey: bytes, signature: bytes) -> bytearray: + write_op_push(w, len(signature) + 1) + write_bytes(w, signature) + w.append(0x01) # SIGHASH_ALL + write_op_push(w, len(pubkey)) + write_bytes(w, pubkey) + return w diff --git a/src/apps/wallet/sign_tx/signing.py b/src/apps/wallet/sign_tx/signing.py index d262576b25..30d385cc6d 100644 --- a/src/apps/wallet/sign_tx/signing.py +++ b/src/apps/wallet/sign_tx/signing.py @@ -1,6 +1,6 @@ from trezor.crypto.hashlib import sha256, ripemd160 from trezor.crypto.curve import secp256k1 -from trezor.crypto import base58, der +from trezor.crypto import base58, der, bech32 from trezor.utils import ensure from trezor.messages.TxRequestSerializedType import TxRequestSerializedType @@ -10,8 +10,8 @@ from trezor.messages import OutputScriptType from apps.common import address_type from apps.common import coins from apps.wallet.sign_tx.segwit_bip143 import * -from apps.wallet.sign_tx.writers import * from apps.wallet.sign_tx.helpers import * +from apps.wallet.sign_tx.scripts import * class SigningError(ValueError): @@ -51,7 +51,7 @@ async def check_tx_fee(tx: SignTx, root): # STAGE_REQUEST_1_INPUT txi = await request_tx_input(tx_req, i) write_tx_input_check(h_first, txi) - if txi.script_type == InputScriptType.SPENDP2SHWITNESS: + if txi.script_type in (InputScriptType.SPENDP2SHWITNESS, InputScriptType.SPENDWITNESS): segwit[i] = True # Add I to segwit hash_prevouts, hash_sequence bip143.add_prevouts(txi) @@ -129,7 +129,8 @@ async def sign_tx(tx: SignTx, root): if segwit[i_sign]: # STAGE_REQUEST_SEGWIT_INPUT txi_sign = await request_tx_input(tx_req, i_sign) - if txi_sign.script_type == InputScriptType.SPENDP2SHWITNESS: + write_tx_input_check(h_second, txi_sign) + if txi_sign.script_type in (InputScriptType.SPENDP2SHWITNESS, InputScriptType.SPENDWITNESS): key_sign = node_derive(root, txi_sign.address_n) key_sign_pub = key_sign.public_key() txi_sign.script_sig = input_derive_script(txi_sign, key_sign_pub) @@ -151,7 +152,9 @@ async def sign_tx(tx: SignTx, root): txi_sign = txi key_sign = node_derive(root, txi.address_n) key_sign_pub = key_sign.public_key() - txi.script_sig = input_derive_script(txi, key_sign_pub) + # the signature has to be also over the output script to prevent modification + # todo this should fail for p2sh + txi_sign.script_sig = output_script_p2pkh(ecdsa_hash_pubkey(key_sign_pub)) else: txi.script_sig = bytes() write_tx_input(h_sign, txi) @@ -197,6 +200,7 @@ async def sign_tx(tx: SignTx, root): txo = await request_tx_output(tx_req, o) txo_bin.amount = txo.amount txo_bin.script_pubkey = output_derive_script(txo, coin, root) + write_tx_output(h_second, txo_bin) # for segwit (not yet checked) # serialize output w_txo_bin = bytearray_with_cap( @@ -216,8 +220,8 @@ async def sign_tx(tx: SignTx, root): # STAGE_REQUEST_SEGWIT_WITNESS txi = await request_tx_input(tx_req, i) - # Check amount - if txi.amount > authorized_in: + # Check amount and the control digests + if txi.amount > authorized_in or (get_tx_hash(h_first, False) != get_tx_hash(h_second, False)): raise SigningError(FailureType.ProcessError, 'Transaction has changed during signing') authorized_in -= txi.amount @@ -274,13 +278,14 @@ async def get_prevtx_output_value(tx_req: TxRequest, prev_hash: bytes, prev_inde return total_out -def estimate_tx_size(inputs, outputs): +def estimate_tx_size(inputs: int, outputs: int) -> int: return 10 + inputs * 149 + outputs * 35 # TX Helpers # === + def get_tx_header(tx: SignTx, segwit=False): w_txi = bytearray() write_uint32(w_txi, tx.version) @@ -298,64 +303,114 @@ def get_p2wpkh_witness(signature: bytes, pubkey: bytes): return w +def get_address(script_type: InputScriptType, coin: CoinType, node) -> bytes: + + if script_type == InputScriptType.SPENDADDRESS: # p2pkh + return node.address(coin.address_type) + + elif script_type == InputScriptType.SPENDWITNESS: # native p2wpkh + if not coin.segwit or not coin.bech32_prefix: + raise SigningError(FailureType.ProcessError, + 'Coin does not support segwit') + return address_p2wpkh(node.public_key(), coin.bech32_prefix) + + elif script_type == InputScriptType.SPENDP2SHWITNESS: # p2wpkh using p2sh + if not coin.segwit or not coin.address_type_p2sh: + raise SigningError(FailureType.ProcessError, + 'Coin does not support segwit') + return address_p2wpkh_in_p2sh(node.public_key(), coin.address_type_p2sh) + + else: + raise SigningError(FailureType.ProcessError, 'Invalid script type') + + +def address_p2wpkh_in_p2sh(pubkey: bytes, addrtype: int) -> str: + s = bytearray(21) + s[0] = addrtype + s[1:21] = address_p2wpkh_in_p2sh_raw(pubkey) + return base58.encode_check(bytes(s)) + + +def address_p2wpkh_in_p2sh_raw(pubkey: bytes) -> bytes: + s = bytearray(22) + s[0] = 0x00 # OP_0 + s[1] = 0x14 # pushing 20 bytes + s[2:22] = ecdsa_hash_pubkey(pubkey) + h = sha256(s).digest() + h = ripemd160(h).digest() + return h + + +def address_p2wpkh(pubkey: bytes, hrp: str) -> str: + pubkeyhash = ecdsa_hash_pubkey(pubkey) + address = bech32.encode(hrp, 0, pubkeyhash) # TODO: constant? + if address is None: + raise SigningError(FailureType.ProcessError, + 'Invalid address') + return address + + +def decode_bech32_address(prefix: str, address: str) -> bytes: + witver, raw = bech32.decode(prefix, address) + if witver != 0: # TODO: constant? + raise SigningError(FailureType.ProcessError, + 'Invalid address witness program') + return bytes(raw) + + # TX Outputs # === 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.PAYTOOPRETURN: + if o.amount != 0: + raise SigningError(FailureType.ProcessError, + 'OP_RETURN output with non-zero amount') + return output_script_paytoopreturn(o.op_return_data) + + if o.address_n: # change output + if o.address: + raise SigningError(FailureType.ProcessError, + 'Both address_n and address provided') + address = get_address_for_change(o, coin, root) + else: + if not o.address: + raise SigningError(FailureType.ProcessError, 'Missing address') + address = o.address + + if coin.bech32_prefix and address.startswith(coin.bech32_prefix): # p2wpkh or p2wsh + # todo check if p2wsh works + pubkeyhash = decode_bech32_address(coin.bech32_prefix, address) + return output_script_native_p2wpkh_or_p2wsh(pubkeyhash) + + raw_address = base58.decode_check(address) + + if address_type.check(coin.address_type, raw_address): # p2pkh + pubkeyhash = address_type.strip(coin.address_type, raw_address) + return output_script_p2pkh(pubkeyhash) + + elif address_type.check(coin.address_type_p2sh, raw_address): # p2sh + scripthash = address_type.strip(coin.address_type_p2sh, raw_address) + return output_script_p2sh(scripthash) + + raise SigningError(FailureType.ProcessError, 'Invalid address type') + + +def get_address_for_change(o: TxOutputType, coin: CoinType, root): if o.script_type == OutputScriptType.PAYTOADDRESS: - ra = output_paytoaddress_extract_raw_address(o, coin, root) - ra = address_type.strip(coin.address_type, ra) - return script_paytoaddress_new(ra) - - elif o.script_type == OutputScriptType.PAYTOSCRIPTHASH: - ra = output_paytoaddress_extract_raw_address(o, coin, root, p2sh=True) - 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) - else: - raise SigningError(FailureType.SyntaxError, - 'OP_RETURN output with non-zero amount') - + input_script_type = InputScriptType.SPENDADDRESS + elif o.script_type == OutputScriptType.PAYTOMULTISIG: + input_script_type = InputScriptType.SPENDMULTISIG + elif o.script_type == OutputScriptType.PAYTOWITNESS: + input_script_type = InputScriptType.SPENDWITNESS + elif o.script_type == OutputScriptType.PAYTOP2SHWITNESS: + input_script_type = InputScriptType.SPENDP2SHWITNESS else: - raise SigningError(FailureType.SyntaxError, - 'Invalid output script type') - - -def output_paytoaddress_extract_raw_address( - o: TxOutputType, coin: CoinType, root, p2sh: bool=False) -> bytes: - 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: - node = node_derive(root, o.address_n) - address = node.address(addr_type) - return base58.decode_check(address) - if o.address: - raw = base58.decode_check(o.address) - if not address_type.check(addr_type, raw): - raise SigningError(FailureType.SyntaxError, - 'Invalid address type') - return raw - raise SigningError(FailureType.SyntaxError, - 'Missing address') + raise SigningError(FailureType.ProcessError, 'Invalid script type') + return get_address(input_script_type, coin, node_derive(root, o.address_n)) def output_is_change(o: TxOutputType) -> bool: @@ -368,17 +423,16 @@ def output_is_change(o: TxOutputType) -> bool: def input_derive_script(i: TxInputType, pubkey: bytes, signature: bytes=None) -> bytes: if i.script_type == InputScriptType.SPENDADDRESS: - if signature is None: - return script_paytoaddress_new(ecdsa_hash_pubkey(pubkey)) - else: - return script_spendaddress_new(pubkey, signature) + return input_script_p2pkh_or_p2sh(pubkey, signature) # p2pkh or p2sh if i.script_type == InputScriptType.SPENDP2SHWITNESS: # p2wpkh using p2sh - return script_p2wpkh_in_p2sh(ecdsa_hash_pubkey(pubkey)) + return input_script_p2wpkh_in_p2sh(ecdsa_hash_pubkey(pubkey)) + + elif i.script_type == InputScriptType.SPENDWITNESS: # native p2wpkh or p2wsh + return input_script_native_p2wpkh_or_p2wsh() else: - raise SigningError(FailureType.SyntaxError, - 'Unknown input script type') + raise SigningError(FailureType.ProcessError, 'Invalid script type') def node_derive(root, address_n: list): @@ -403,78 +457,3 @@ def ecdsa_sign(node, digest: bytes) -> bytes: sig = secp256k1.sign(node.private_key(), digest) sigder = der.encode_seq((sig[1:33], sig[33:65])) 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 -# === - - -def script_paytoaddress_new(pubkeyhash: bytes) -> bytearray: - s = bytearray(25) - s[0] = 0x76 # OP_DUP - s[1] = 0xA9 # OP_HASH_160 - s[2] = 0x14 # pushing 20 bytes - s[3:23] = pubkeyhash - s[23] = 0x88 # OP_EQUALVERIFY - s[24] = 0xAC # OP_CHECKSIG - return s - - -def script_paytoscripthash_new(scripthash: bytes) -> bytearray: - s = bytearray(23) - s[0] = 0xA9 # OP_HASH_160 - s[1] = 0x14 # pushing 20 bytes - s[2:22] = scripthash - s[22] = 0x87 # OP_EQUAL - return s - - -# P2WPKH is nested in P2SH to be backwards compatible -# see https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#witness-program -# this pushes 16 00 14 -def script_p2wpkh_in_p2sh(pubkeyhash: bytes) -> bytearray: - w = bytearray_with_cap(3 + len(pubkeyhash)) - write_op_push(w, len(pubkeyhash) + 2) # 0x16 - length of the redeemScript - w.append(0x00) # witness version byte - w.append(0x14) # P2WPKH witness program (pub key hash length + pub key hash) - write_bytes(w, pubkeyhash) - return w - - -def script_paytoopreturn_new(data: bytes) -> bytearray: - w = bytearray_with_cap(1 + 5 + len(data)) - w.append(0x6A) # OP_RETURN - write_op_push(w, len(data)) - w.extend(data) - return w - - -def script_spendaddress_new(pubkey: bytes, signature: bytes) -> bytearray: - w = bytearray_with_cap(5 + len(signature) + 1 + 5 + len(pubkey)) - append_signature_and_pubkey(w, pubkey, signature) - return w - - -def append_signature_and_pubkey(w: bytearray, pubkey: bytes, signature: bytes) -> bytearray: - write_op_push(w, len(signature) + 1) - write_bytes(w, signature) - w.append(0x01) # SIGHASH_ALL - write_op_push(w, len(pubkey)) - write_bytes(w, pubkey) - return w diff --git a/src/apps/wallet/sign_tx/writers.py b/src/apps/wallet/sign_tx/writers.py index c5eea29d06..9564a6fab2 100644 --- a/src/apps/wallet/sign_tx/writers.py +++ b/src/apps/wallet/sign_tx/writers.py @@ -25,6 +25,8 @@ def write_tx_input_check(w, i: TxInputType): for n in i.address_n: write_uint32(w, n) write_uint32(w, i_sequence) + i_amount = i.amount if i.amount is not None else 0 + write_uint32(w, i_amount) # this is probably redundant, but better safe than sorry def write_tx_output(w, o: TxOutputBinType): diff --git a/tests/test_apps.wallet.segwit.address.py b/tests/test_apps.wallet.segwit.address.py index b9acbd85a6..8bfb45e2ab 100644 --- a/tests/test_apps.wallet.segwit.address.py +++ b/tests/test_apps.wallet.segwit.address.py @@ -9,33 +9,37 @@ 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( + address = address_p2wpkh_in_p2sh( unhexlify('03a1af804ac108a8a51782198c2d034b28bf90c8803f5a53f76276fa69a4eae77f'), - coin + coin.address_type_p2sh ) self.assertEqual(address, '2Mww8dCYPUpKHofjgcXcBCEGmniw9CoaiD2') - def test_p2wpkh_in_p2sh_node_derive_address(self): + def test_p2wpkh_in_p2sh_raw_address(self): + raw = address_p2wpkh_in_p2sh_raw( + unhexlify('03a1af804ac108a8a51782198c2d034b28bf90c8803f5a53f76276fa69a4eae77f') + ) + self.assertEqual(raw, unhexlify('336caa13e08b96080a32b5d818d59b4ab3b36742')) + 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) + address = address_p2wpkh_in_p2sh(node.public_key(), coin.address_type_p2sh) 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) + address = address_p2wpkh_in_p2sh(node.public_key(), coin.address_type_p2sh) 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) + address = address_p2wpkh_in_p2sh(node.public_key(), coin.address_type_p2sh) self.assertEqual(address, '2N4Q5FhU2497BryFfUgbqkAJE87aKHUhXMp') diff --git a/tests/test_apps.wallet.segwit.signtx.native_p2wpkh.py b/tests/test_apps.wallet.segwit.signtx.native_p2wpkh.py new file mode 100644 index 0000000000..562809782d --- /dev/null +++ b/tests/test_apps.wallet.segwit.signtx.native_p2wpkh.py @@ -0,0 +1,215 @@ +from common import * + +from trezor.utils import chunks +from trezor.crypto import bip32, bip39 +from trezor.messages.SignTx import SignTx +from trezor.messages.TxInputType import TxInputType +from trezor.messages.TxOutputType import TxOutputType +from trezor.messages.TxRequest import TxRequest +from trezor.messages.TxAck import TxAck +from trezor.messages.TransactionType import TransactionType +from trezor.messages.RequestType import TXINPUT, TXOUTPUT, TXMETA, TXFINISHED +from trezor.messages.TxRequestDetailsType import TxRequestDetailsType +from trezor.messages.TxRequestSerializedType import TxRequestSerializedType +from trezor.messages import InputScriptType +from trezor.messages import OutputScriptType + +from apps.common import coins +from apps.wallet.sign_tx import signing + + +class TestSignSegwitTxNativeP2WPKH(unittest.TestCase): + # pylint: disable=C0301 + + def test_send_native_p2wpkh(self): + + coin = coins.by_name('Testnet') + + seed = bip39.seed(' '.join(['all'] * 12), '') + root = bip32.from_seed(seed, 'secp256k1') + + inp1 = TxInputType( + # 49'/1'/0'/0/0" - tb1qqzv60m9ajw8drqulta4ld4gfx0rdh82un5s65s + address_n=[49 | 0x80000000, 1 | 0x80000000, 0 | 0x80000000, 0, 0], + amount=12300000, + prev_hash=unhexlify('09144602765ce3dd8f4329445b20e3684e948709c5cdcaf12da3bb079c99448a'), + prev_index=0, + script_type=InputScriptType.SPENDWITNESS, + sequence=0xffffffff, + ) + out1 = TxOutputType( + address='2N4Q5FhU2497BryFfUgbqkAJE87aKHUhXMp', + amount=5000000, + script_type=OutputScriptType.PAYTOADDRESS, + address_n=None, # @todo ask honza about sanitizing + ) + out2 = TxOutputType( + address='tb1q694ccp5qcc0udmfwgp692u2s2hjpq5h407urtu', + script_type=OutputScriptType.PAYTOADDRESS, + amount=12300000 - 11000 - 5000000, + address_n=None, + ) + 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.UiConfirmOutput(out2, coin), + True, + + signing.UiConfirmTotal(12300000 - 11000, 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('010000000001018a44999c07bba32df1cacdc50987944e68e3205b4429438fdde35c76024614090000000000ffffffff'), + )), + TxAck(tx=TransactionType(outputs=[out1])), + + TxRequest(request_type=TXOUTPUT, details=TxRequestDetailsType(request_index=1, tx_hash=None), serialized=TxRequestSerializedType( + # returned serialized out1 + serialized_tx=unhexlify('02404b4c000000000017a9147a55d61848e77ca266e79a39bfc85c580a6426c987'), + 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('a8386f0000000000160014d16b8c0680c61fc6ed2e407455715055e41052f5'), + signature_index=None, + signature=None, + )), + TxAck(tx=TransactionType(inputs=[inp1])), + + TxRequest(request_type=TXFINISHED, details=None, serialized=TxRequestSerializedType( + serialized_tx=unhexlify('02483045022100a7ca8f097525f9044e64376dc0a0f5d4aeb8d15d66808ba97979a0475b06b66502200597c8ebcef63e047f9aeef1a8001d3560470cf896c12f6990eec4faec599b950121033add1f0e8e3c3136f7428dd4a4de1057380bd311f5b0856e2269170b4ffa65bf00000000'), + 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 test_send_native_p2wpkh_change(self): + + coin = coins.by_name('Testnet') + + seed = bip39.seed(' '.join(['all'] * 12), '') + root = bip32.from_seed(seed, 'secp256k1') + + inp1 = TxInputType( + # 49'/1'/0'/0/0" - tb1qqzv60m9ajw8drqulta4ld4gfx0rdh82un5s65s + address_n=[49 | 0x80000000, 1 | 0x80000000, 0 | 0x80000000, 0, 0], + amount=12300000, + prev_hash=unhexlify('09144602765ce3dd8f4329445b20e3684e948709c5cdcaf12da3bb079c99448a'), + prev_index=0, + script_type=InputScriptType.SPENDWITNESS, + sequence=0xffffffff, + ) + out1 = TxOutputType( + address='2N4Q5FhU2497BryFfUgbqkAJE87aKHUhXMp', + amount=5000000, + script_type=OutputScriptType.PAYTOADDRESS, + address_n=None, # @todo ask honza about sanitizing + ) + out2 = TxOutputType( + address=None, + address_n=[49 | 0x80000000, 1 | 0x80000000, 0 | 0x80000000, 1, 0], + script_type=OutputScriptType.PAYTOWITNESS, + amount=12300000 - 11000 - 5000000, + ) + 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(5000000, 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('010000000001018a44999c07bba32df1cacdc50987944e68e3205b4429438fdde35c76024614090000000000ffffffff'), + )), + TxAck(tx=TransactionType(outputs=[out1])), + + TxRequest(request_type=TXOUTPUT, details=TxRequestDetailsType(request_index=1, tx_hash=None), serialized=TxRequestSerializedType( + # returned serialized out1 + serialized_tx=unhexlify('02404b4c000000000017a9147a55d61848e77ca266e79a39bfc85c580a6426c987'), + 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('a8386f0000000000160014d16b8c0680c61fc6ed2e407455715055e41052f5'), + signature_index=None, + signature=None, + )), + TxAck(tx=TransactionType(inputs=[inp1])), + + TxRequest(request_type=TXFINISHED, details=None, serialized=TxRequestSerializedType( + serialized_tx=unhexlify('02483045022100a7ca8f097525f9044e64376dc0a0f5d4aeb8d15d66808ba97979a0475b06b66502200597c8ebcef63e047f9aeef1a8001d3560470cf896c12f6990eec4faec599b950121033add1f0e8e3c3136f7428dd4a4de1057380bd311f5b0856e2269170b4ffa65bf00000000'), + 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 + (isinstance(a, signing.UiConfirmTotal) and isinstance(b, signing.UiConfirmTotal))): + return self.assertEqual(a.__dict__, b.__dict__) + else: + return self.assertEqual(a, b) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_apps.wallet.segwit.signtx.py b/tests/test_apps.wallet.segwit.signtx.p2wpkh_in_p2sh.py similarity index 96% rename from tests/test_apps.wallet.segwit.signtx.py rename to tests/test_apps.wallet.segwit.signtx.p2wpkh_in_p2sh.py index d83bee796b..294a7be403 100644 --- a/tests/test_apps.wallet.segwit.signtx.py +++ b/tests/test_apps.wallet.segwit.signtx.p2wpkh_in_p2sh.py @@ -18,7 +18,7 @@ from apps.common import coins from apps.wallet.sign_tx import signing -class TestSignSegwitTx(unittest.TestCase): +class TestSignSegwitTxP2WPKHInP2SH(unittest.TestCase): # pylint: disable=C0301 def test_send_p2wpkh_in_p2sh(self): @@ -136,9 +136,10 @@ class TestSignSegwitTx(unittest.TestCase): address_n=None, ) out2 = TxOutputType( - address_n = [49 | 0x80000000, 1 | 0x80000000, 0 | 0x80000000, 1, 0], - script_type = OutputScriptType.PAYTOP2SHWITNESS, - amount = 123456789 - 11000 - 12300000, + address_n=[49 | 0x80000000, 1 | 0x80000000, 0 | 0x80000000, 1, 0], + script_type=OutputScriptType.PAYTOP2SHWITNESS, + amount=123456789 - 11000 - 12300000, + address=None, # todo ask about sanitizing ) tx = SignTx(coin_name='Testnet', version=None, lock_time=None, inputs_count=1, outputs_count=2) @@ -245,9 +246,10 @@ class TestSignSegwitTx(unittest.TestCase): address_n=None, ) out2 = TxOutputType( - address_n = [49 | 0x80000000, 1 | 0x80000000, 0 | 0x80000000, 1, 0], - script_type = OutputScriptType.PAYTOP2SHWITNESS, - amount = 1, + address_n=[49 | 0x80000000, 1 | 0x80000000, 0 | 0x80000000, 1, 0], + script_type=OutputScriptType.PAYTOP2SHWITNESS, + amount=1, + address=None, # todo ask about sanitizing ) tx = SignTx(coin_name='Testnet', version=None, lock_time=None, inputs_count=1, outputs_count=2) diff --git a/tests/test_apps.wallet.signtx.py b/tests/test_apps.wallet.signtx.py index 2e395ed4bc..b5957f9da1 100644 --- a/tests/test_apps.wallet.signtx.py +++ b/tests/test_apps.wallet.signtx.py @@ -45,6 +45,7 @@ class TestSignTx(unittest.TestCase): # amount=390000, prev_hash=unhexlify('d5f65ee80147b4bcc70b75e4bbf2d7382021b871bd8867ef8fa525ef50864882'), prev_index=0, + amount=None, script_type=None, sequence=None) out1 = TxOutputType(address='1MJ2tj2ThBE62zXbBYA5ZaN3fdve5CPAz1', From 2ebf1cd9f44be7775b36e01d8d533a88fdcef0f2 Mon Sep 17 00:00:00 2001 From: Tomas Susanka Date: Thu, 9 Nov 2017 14:40:55 +0100 Subject: [PATCH 09/23] trezor/crypto: bech32 taken from https://github.com/sipa/bech32/tree/master/ref/python --- src/trezor/crypto/bech32.py | 123 ++++++++++++++++++++++++++ tests/test_trezor.crypto.bech32.py | 133 +++++++++++++++++++++++++++++ 2 files changed, 256 insertions(+) create mode 100644 src/trezor/crypto/bech32.py create mode 100644 tests/test_trezor.crypto.bech32.py diff --git a/src/trezor/crypto/bech32.py b/src/trezor/crypto/bech32.py new file mode 100644 index 0000000000..e0df3b6a67 --- /dev/null +++ b/src/trezor/crypto/bech32.py @@ -0,0 +1,123 @@ +# Copyright (c) 2017 Pieter Wuille +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +"""Reference implementation for Bech32 and segwit addresses.""" + + +CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" + + +def bech32_polymod(values): + """Internal function that computes the Bech32 checksum.""" + generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3] + chk = 1 + for value in values: + top = chk >> 25 + chk = (chk & 0x1ffffff) << 5 ^ value + for i in range(5): + chk ^= generator[i] if ((top >> i) & 1) else 0 + return chk + + +def bech32_hrp_expand(hrp): + """Expand the HRP into values for checksum computation.""" + return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp] + + +def bech32_verify_checksum(hrp, data): + """Verify a checksum given HRP and converted data characters.""" + return bech32_polymod(bech32_hrp_expand(hrp) + data) == 1 + + +def bech32_create_checksum(hrp, data): + """Compute the checksum values given HRP and data.""" + values = bech32_hrp_expand(hrp) + data + polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ 1 + return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)] + + +def bech32_encode(hrp, data): + """Compute a Bech32 string given HRP and data values.""" + combined = data + bech32_create_checksum(hrp, data) + return hrp + '1' + ''.join([CHARSET[d] for d in combined]) + + +def bech32_decode(bech): + """Validate a Bech32 string, and determine HRP and data.""" + if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or + (bech.lower() != bech and bech.upper() != bech)): + return (None, None) + bech = bech.lower() + pos = bech.rfind('1') + if pos < 1 or pos + 7 > len(bech) or len(bech) > 90: + return (None, None) + if not all(x in CHARSET for x in bech[pos+1:]): + return (None, None) + hrp = bech[:pos] + data = [CHARSET.find(x) for x in bech[pos+1:]] + if not bech32_verify_checksum(hrp, data): + return (None, None) + return (hrp, data[:-6]) + + +def convertbits(data, frombits, tobits, pad=True): + """General power-of-2 base conversion.""" + acc = 0 + bits = 0 + ret = [] + maxv = (1 << tobits) - 1 + max_acc = (1 << (frombits + tobits - 1)) - 1 + for value in data: + if value < 0 or (value >> frombits): + return None + acc = ((acc << frombits) | value) & max_acc + bits += frombits + while bits >= tobits: + bits -= tobits + ret.append((acc >> bits) & maxv) + if pad: + if bits: + ret.append((acc << (tobits - bits)) & maxv) + elif bits >= frombits or ((acc << (tobits - bits)) & maxv): + return None + return ret + + +def decode(hrp, addr): + """Decode a segwit address.""" + hrpgot, data = bech32_decode(addr) + if hrpgot != hrp: + return (None, None) + decoded = convertbits(data[1:], 5, 8, False) + if decoded is None or len(decoded) < 2 or len(decoded) > 40: + return (None, None) + if data[0] > 16: + return (None, None) + if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32: + return (None, None) + return (data[0], decoded) + + +def encode(hrp, witver, witprog): + """Encode a segwit address.""" + ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5)) + if decode(hrp, ret) == (None, None): + return None + return ret \ No newline at end of file diff --git a/tests/test_trezor.crypto.bech32.py b/tests/test_trezor.crypto.bech32.py new file mode 100644 index 0000000000..946de0fe3c --- /dev/null +++ b/tests/test_trezor.crypto.bech32.py @@ -0,0 +1,133 @@ +# Copyright (c) 2017 Pieter Wuille +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + + +"""Reference tests for segwit adresses""" + +from common import * +from trezor.crypto import bech32 + +def segwit_scriptpubkey(witver, witprog): + """Construct a Segwit scriptPubKey for a given witness program.""" + return bytes([witver + 0x50 if witver else 0, len(witprog)] + witprog) + +VALID_CHECKSUM = [ + "A12UEL5L", + "an83characterlonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1tt5tgs", + "abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw", + "11qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqc8247j", + "split1checkupstagehandshakeupstreamerranterredcaperred2y9e3w", +] + +INVALID_CHECKSUM = [ + " 1nwldj5", + "\x7F" + "1axkwrx", + "an84characterslonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1569pvx", + "pzry9x0s0muk", + "1pzry9x0s0muk", + "x1b4n0q5v", + "li1dgmt3", + "de1lg7wt\xff", +] + +VALID_ADDRESS = [ + ["BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4", "0014751e76e8199196d454941c45d1b3a323f1433bd6"], + ["tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7", + "00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262"], + ["bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx", + "5128751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6"], + ["BC1SW50QA3JX3S", "6002751e"], + ["bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj", "5210751e76e8199196d454941c45d1b3a323"], + ["tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy", + "0020000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433"], +] + +INVALID_ADDRESS = [ + "tc1qw508d6qejxtdg4y5r3zarvary0c5xw7kg3g4ty", + "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5", + "BC13W508D6QEJXTDG4Y5R3ZARVARY0C5XW7KN40WF2", + "bc1rw5uspcuh", + "bc10w508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kw5rljs90", + "BC1QR508D6QEJXTDG4Y5R3ZARVARYV98GJ9P", + "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sL5k7", + "bc1zw508d6qejxtdg4y5r3zarvaryvqyzf3du", + "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3pjxtptv", + "bc1gmk9yu", + +] + +INVALID_ADDRESS_ENC = [ + ("BC", 0, 20), + ("bc", 0, 21), + ("bc", 17, 32), + ("bc", 1, 1), + ("bc", 16, 41), +] + + +class TestCryptoBech32(unittest.TestCase): + """Unit test class for segwit addressess.""" + + def test_valid_checksum(self): + """Test checksum creation and validation.""" + for test in VALID_CHECKSUM: + hrp, _ = bech32.bech32_decode(test) + self.assertIsNotNone(hrp) + pos = test.rfind('1') + test = test[:pos+1] + chr(ord(test[pos + 1]) ^ 1) + test[pos+2:] + hrp, _ = bech32.bech32_decode(test) + self.assertIsNone(hrp) + + def test_invalid_checksum(self): + """Test validation of invalid checksums.""" + for test in INVALID_CHECKSUM: + hrp, _ = bech32.bech32_decode(test) + self.assertIsNone(hrp) + + def test_valid_address(self): + """Test whether valid addresses decode to the correct output.""" + for (address, hexscript) in VALID_ADDRESS: + hrp = "bc" + witver, witprog = bech32.decode(hrp, address) + if witver is None: + hrp = "tb" + witver, witprog = bech32.decode(hrp, address) + self.assertIsNotNone(witver) + scriptpubkey = segwit_scriptpubkey(witver, witprog) + self.assertEqual(scriptpubkey, unhexlify(hexscript)) + addr = bech32.encode(hrp, witver, witprog) + self.assertEqual(address.lower(), addr) + + def test_invalid_address(self): + """Test whether invalid addresses fail to decode.""" + for test in INVALID_ADDRESS: + witver, _ = bech32.decode("bc", test) + self.assertIsNone(witver) + witver, _ = bech32.decode("tb", test) + self.assertIsNone(witver) + + def test_invalid_address_enc(self): + """Test whether address encoding fails on invalid input.""" + for hrp, version, length in INVALID_ADDRESS_ENC: + code = bech32.encode(hrp, version, [0] * length) + self.assertIsNone(code) + +if __name__ == "__main__": + unittest.main() From a79a71b74a173d28029e7c06f7584d0f38b6ccea Mon Sep 17 00:00:00 2001 From: Tomas Susanka Date: Thu, 9 Nov 2017 15:09:02 +0100 Subject: [PATCH 10/23] submodules: trezor-common updated to 9c1e61e --- vendor/trezor-common | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/trezor-common b/vendor/trezor-common index 0001cb18c0..9c1e61e460 160000 --- a/vendor/trezor-common +++ b/vendor/trezor-common @@ -1 +1 @@ -Subproject commit 0001cb18c065d7f3aacd6f8d7c5be42cd7477a94 +Subproject commit 9c1e61e4608e1d49043fb668eb9c7621fcc20648 From aa2966705900b36271472114364396f2bf92cdac Mon Sep 17 00:00:00 2001 From: Tomas Susanka Date: Thu, 9 Nov 2017 15:37:22 +0100 Subject: [PATCH 11/23] common: coins updated --- src/apps/common/coins.py | 154 ++++++++++++++++-- src/apps/wallet/sign_tx/segwit_bip143.py | 3 +- ...apps.wallet.segwit.bip143.native_p2wpkh.py | 96 +++++++++++ ...ps.wallet.segwit.bip143.p2wpkh_in_p2sh.py} | 0 tools/codegen/gen_coins.py | 17 +- 5 files changed, 255 insertions(+), 15 deletions(-) create mode 100644 tests/test_apps.wallet.segwit.bip143.native_p2wpkh.py rename tests/{test_apps.wallet.segwit.bip143.py => test_apps.wallet.segwit.bip143.p2wpkh_in_p2sh.py} (100%) diff --git a/src/apps/common/coins.py b/src/apps/common/coins.py index 5954ce1565..be35029011 100644 --- a/src/apps/common/coins.py +++ b/src/apps/common/coins.py @@ -1,127 +1,257 @@ from trezor.messages.CoinType import CoinType -# the following list is generated using tools/coins-gen.py +# the following list is generated using tools/codegen/gen_coins.py # do not edit manually! COINS = [ CoinType( coin_name='Bitcoin', coin_shortcut='BTC', + coin_label='Bitcoin', address_type=0, address_type_p2sh=5, maxfee_kb=500000, + minfee_kb=1000, signed_message_header='Bitcoin Signed Message:\n', - xpub_magic=0x0488b21e, + hash_genesis_block='000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f', xprv_magic=0x0488ade4, + xpub_magic=0x0488b21e, + bech32_prefix='bc', bip44=0, - segwit=False, + segwit=True, + forkid=None, + default_fee_b={'Low': 10, 'Economy': 70, 'Normal': 140, 'High': 200}, + dust_limit=546, + blocktime_minutes=10, + firmware='stable', + address_prefix='bitcoin:', + min_address_length=27, + max_address_length=34, + bitcore=['https://btc-bitcore3.trezor.io', 'https://btc-bitcore1.trezor.io'], ), CoinType( coin_name='Testnet', coin_shortcut='TEST', + coin_label='Testnet', address_type=111, address_type_p2sh=196, maxfee_kb=10000000, + minfee_kb=1000, signed_message_header='Bitcoin Signed Message:\n', - xpub_magic=0x043587cf, + hash_genesis_block='000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943', xprv_magic=0x04358394, + xpub_magic=0x043587cf, + bech32_prefix='tb', bip44=1, segwit=True, + forkid=None, + default_fee_b={'Normal': 10}, + dust_limit=546, + blocktime_minutes=10, + firmware='stable', + address_prefix='bitcoin:', + min_address_length=27, + max_address_length=34, + bitcore=['https://testnet-bitcore3.trezor.io', 'https://testnet-bitcore4.trezor.io'], ), CoinType( coin_name='Bcash', coin_shortcut='BCH', + coin_label='Bitcoin Cash', address_type=0, address_type_p2sh=5, maxfee_kb=500000, + minfee_kb=1000, signed_message_header='Bitcoin Signed Message:\n', - xpub_magic=0x0488b21e, + hash_genesis_block='000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f', xprv_magic=0x0488ade4, + xpub_magic=0x0488b21e, + bech32_prefix=None, bip44=145, segwit=False, + forkid=0, + default_fee_b={'Low': 10, 'Economy': 70, 'Normal': 140, 'High': 200}, + dust_limit=546, + blocktime_minutes=10, + firmware='stable', + address_prefix='bitcoincash:', + min_address_length=27, + max_address_length=34, + bitcore=['https://bch-bitcore2.trezor.io'], ), CoinType( coin_name='Bcash Testnet', coin_shortcut='TBCH', + coin_label='Bitcoin Cash Testnet', address_type=111, address_type_p2sh=196, maxfee_kb=10000000, + minfee_kb=1000, signed_message_header='Bitcoin Signed Message:\n', - xpub_magic=0x043587cf, + hash_genesis_block='000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943', xprv_magic=0x04358394, + xpub_magic=0x043587cf, + bech32_prefix=None, bip44=1, segwit=False, + forkid=0, + default_fee_b={'Normal': 10}, + dust_limit=546, + blocktime_minutes=10, + firmware='debug', + address_prefix='bitcoincash:', + min_address_length=27, + max_address_length=34, + bitcore=[], ), CoinType( coin_name='Namecoin', coin_shortcut='NMC', + coin_label='Namecoin', address_type=52, address_type_p2sh=5, maxfee_kb=10000000, + minfee_kb=1000, signed_message_header='Namecoin Signed Message:\n', - xpub_magic=0x019da462, + hash_genesis_block='000000000062b72c5e2ceb45fbc8587e807c155b0da735e6483dfba2f0a9c770', xprv_magic=0x019d9cfe, + xpub_magic=0x019da462, + bech32_prefix=None, bip44=7, segwit=False, + forkid=None, + default_fee_b={'Normal': 10}, + dust_limit=2940, + blocktime_minutes=10, + firmware='stable', + address_prefix='namecoin:', + min_address_length=27, + max_address_length=34, + bitcore=[], ), CoinType( coin_name='Litecoin', coin_shortcut='LTC', + coin_label='Litecoin', address_type=48, address_type_p2sh=50, maxfee_kb=40000000, + minfee_kb=100000, signed_message_header='Litecoin Signed Message:\n', - xpub_magic=0x019da462, + hash_genesis_block='12a765e31ffd4059bada1e25190f6e98c99d9714d334efa41a195a7e7e04bfe2', xprv_magic=0x019d9cfe, + xpub_magic=0x019da462, + bech32_prefix='ltc', bip44=2, segwit=True, + forkid=None, + default_fee_b={'Normal': 1000}, + dust_limit=54600, + blocktime_minutes=2.5, + firmware='stable', + address_prefix='litecoin:', + min_address_length=27, + max_address_length=34, + bitcore=['https://ltc-bitcore3.trezor.io'], ), CoinType( coin_name='Dogecoin', coin_shortcut='DOGE', + coin_label='Dogecoin', address_type=30, address_type_p2sh=22, maxfee_kb=1000000000, + minfee_kb=1000, signed_message_header='Dogecoin Signed Message:\n', - xpub_magic=0x02facafd, + hash_genesis_block='1a91e3dace36e2be3bf030a65679fe821aa1d6ef92e7c9902eb318182c355691', xprv_magic=0x02fac398, + xpub_magic=0x02facafd, + bech32_prefix=None, bip44=3, segwit=False, + forkid=None, + default_fee_b={'Normal': 10}, + dust_limit=10000000, + blocktime_minutes=1, + firmware='stable', + address_prefix='dogecoin:', + min_address_length=27, + max_address_length=34, + bitcore=[], ), CoinType( coin_name='Dash', coin_shortcut='DASH', + coin_label='Dash', address_type=76, address_type_p2sh=16, maxfee_kb=100000, + minfee_kb=10000, signed_message_header='DarkCoin Signed Message:\n', - xpub_magic=0x02fe52cc, + hash_genesis_block='00000ffd590b1485b3caadc19b22e6379c733355108f107a430458cdf3407ab6', xprv_magic=0x02fe52f8, + xpub_magic=0x02fe52cc, + bech32_prefix=None, bip44=5, segwit=False, + forkid=None, + default_fee_b={'Normal': 10}, + dust_limit=5460, + blocktime_minutes=2.5, + firmware='stable', + address_prefix='dash:', + min_address_length=27, + max_address_length=34, + bitcore=['https://dash-bitcore1.trezor.io', 'https://dash-bitcore3.trezor.io'], ), CoinType( coin_name='Zcash', coin_shortcut='ZEC', + coin_label='Zcash', address_type=7352, address_type_p2sh=7357, maxfee_kb=1000000, + minfee_kb=1000, signed_message_header='Zcash Signed Message:\n', - xpub_magic=0x0488b21e, + hash_genesis_block='00040fe8ec8471911baa1db1266ea15dd06b4a8a5c453883c000b031973dce08', xprv_magic=0x0488ade4, + xpub_magic=0x0488b21e, + bech32_prefix=None, bip44=133, segwit=False, + forkid=None, + default_fee_b={'Normal': 10}, + dust_limit=546, + blocktime_minutes=2.5, + firmware='stable', + address_prefix='zcash:', + min_address_length=35, + max_address_length=95, + bitcore=['https://zec-bitcore1.trezor.io/'], ), CoinType( coin_name='Zcash Testnet', coin_shortcut='TAZ', + coin_label='Zcash Testnet', address_type=7461, address_type_p2sh=7354, maxfee_kb=10000000, + minfee_kb=1000, signed_message_header='Zcash Signed Message:\n', - xpub_magic=0x043587cf, + hash_genesis_block='05a60a92d99d85997cce3b87616c089f6124d7342af37106edc76126334a2c38', xprv_magic=0x04358394, + xpub_magic=0x043587cf, + bech32_prefix=None, bip44=1, segwit=False, + forkid=None, + default_fee_b={'Normal': 10}, + dust_limit=546, + blocktime_minutes=2.5, + firmware='debug', + address_prefix='zcash:', + min_address_length=35, + max_address_length=95, + bitcore=[], ), ] diff --git a/src/apps/wallet/sign_tx/segwit_bip143.py b/src/apps/wallet/sign_tx/segwit_bip143.py index 4d45922522..4a97c52336 100644 --- a/src/apps/wallet/sign_tx/segwit_bip143.py +++ b/src/apps/wallet/sign_tx/segwit_bip143.py @@ -58,7 +58,8 @@ class Bip143: # this not redeemScript nor scriptPubKey # for P2WPKH this is always 0x1976a914{20-byte-pubkey-hash}88ac def derive_script_code(self, txi: TxInputType, pubkeyhash: bytes) -> bytearray: - if txi.script_type == InputScriptType.SPENDP2SHWITNESS: + # p2wpkh in p2sh or native p2wpkh + if txi.script_type in (InputScriptType.SPENDP2SHWITNESS, InputScriptType.SPENDWITNESS): s = bytearray(25) s[0] = 0x76 # OP_DUP s[1] = 0xA9 # OP_HASH_160 diff --git a/tests/test_apps.wallet.segwit.bip143.native_p2wpkh.py b/tests/test_apps.wallet.segwit.bip143.native_p2wpkh.py new file mode 100644 index 0000000000..1a390f4cfa --- /dev/null +++ b/tests/test_apps.wallet.segwit.bip143.native_p2wpkh.py @@ -0,0 +1,96 @@ +from common import * + +from apps.wallet.sign_tx.signing import * +from apps.common import coins +from trezor.messages.SignTx import SignTx +from trezor.messages.TxInputType import TxInputType +from trezor.messages.TxOutputType import TxOutputType +from trezor.messages import OutputScriptType +from trezor.crypto import bip32, bip39 + + +class TestSegwitBip143NativeP2WPKH(unittest.TestCase): + # pylint: disable=C0301 + + tx = SignTx(coin_name='Bitcoin', version=1, lock_time=0x00000011, inputs_count=2, outputs_count=2) + inp1 = TxInputType(address_n=[0], + # Trezor expects hash in reversed format + prev_hash=unhexlify('9f96ade4b41d5433f4eda31e1738ec2b36f6e7d1420d94a6af99801a88f7f7ff'), + prev_index=0, + amount=625000000, # 6.25 btc + script_type=InputScriptType.SPENDWITNESS, + sequence=0xffffffee) + inp2 = TxInputType(address_n=[1], + # Trezor expects hash in reversed format + prev_hash=unhexlify('8ac60eb9575db5b2d987e29f301b5b819ea83a5c6579d282d189cc04b8e151ef'), + prev_index=1, + amount=600000000, # 6 btc + script_type=InputScriptType.SPENDWITNESS, + sequence=0xffffffff) + out1 = TxOutputType(address='1Cu32FVupVCgHkMMRJdYJugxwo2Aprgk7H', # derived + amount=0x0000000006b22c20, + script_type=OutputScriptType.PAYTOADDRESS, + address_n=None) + out2 = TxOutputType(address='16TZ8J6Q5iZKBWizWzFAYnrsaox5Z5aBRV', # derived + amount=0x000000000d519390, + script_type=OutputScriptType.PAYTOADDRESS, + address_n=None) + + def test_prevouts(self): + + bip143 = Bip143() + bip143.add_prevouts(self.inp1) + bip143.add_prevouts(self.inp2) + self.assertEqual(hexlify(bip143.get_prevouts_hash()), b'96b827c8483d4e9b96712b6713a7b68d6e8003a781feba36c31143470b4efd37') + + def test_sequence(self): + + bip143 = Bip143() + bip143.add_sequence(self.inp1) + bip143.add_sequence(self.inp2) + self.assertEqual(hexlify(bip143.get_sequence_hash()), b'52b0a642eea2fb7ae638c36f6252b6750293dbe574a806984b8e4d8548339a3b') + + def test_outputs(self): + + seed = bip39.seed('alcohol woman abuse must during monitor noble actual mixed trade anger aisle', '') + root = bip32.from_seed(seed, 'secp256k1') + coin = coins.by_name(self.tx.coin_name) + + bip143 = Bip143() + + for txo in [self.out1, self.out2]: + txo_bin = TxOutputBinType() + txo_bin.amount = txo.amount + txo_bin.script_pubkey = output_derive_script(txo, coin, root) + bip143.add_output(txo_bin) + + self.assertEqual(hexlify(bip143.get_outputs_hash()), + b'863ef3e1a92afbfdb97f31ad0fc7683ee943e9abcf2501590ff8f6551f47e5e5') + + def test_preimage_testdata(self): + + seed = bip39.seed('alcohol woman abuse must during monitor noble actual mixed trade anger aisle', '') + root = bip32.from_seed(seed, 'secp256k1') + coin = coins.by_name(self.tx.coin_name) + + bip143 = Bip143() + bip143.add_prevouts(self.inp1) + bip143.add_prevouts(self.inp2) + bip143.add_sequence(self.inp1) + bip143.add_sequence(self.inp2) + + for txo in [self.out1, self.out2]: + txo_bin = TxOutputBinType() + txo_bin.amount = txo.amount + txo_bin.script_pubkey = output_derive_script(txo, coin, root) + bip143.add_output(txo_bin) + + # test data public key hash + # only for input 2 - input 1 is not segwit + result = bip143.preimage_hash(self.tx, self.inp2, unhexlify('1d0f172a0ecb48aee1be1f2687d2963ae33f71a1')) + self.assertEqual(hexlify(result), b'c37af31116d1b27caf68aae9e3ac82f1477929014d5b917657d0eb49478cb670') + + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_apps.wallet.segwit.bip143.py b/tests/test_apps.wallet.segwit.bip143.p2wpkh_in_p2sh.py similarity index 100% rename from tests/test_apps.wallet.segwit.bip143.py rename to tests/test_apps.wallet.segwit.bip143.p2wpkh_in_p2sh.py diff --git a/tools/codegen/gen_coins.py b/tools/codegen/gen_coins.py index ebfe6c2cd7..dd7728ddcd 100755 --- a/tools/codegen/gen_coins.py +++ b/tools/codegen/gen_coins.py @@ -4,17 +4,30 @@ import json fields = [ 'coin_name', 'coin_shortcut', + 'coin_label', 'address_type', 'address_type_p2sh', 'maxfee_kb', + 'minfee_kb', 'signed_message_header', - 'xpub_magic', + 'hash_genesis_block', 'xprv_magic', + 'xpub_magic', + 'bech32_prefix', 'bip44', 'segwit', + 'forkid', + 'default_fee_b', + 'dust_limit', + 'blocktime_minutes', + 'firmware', + 'address_prefix', + 'min_address_length', + 'max_address_length', + 'bitcore', ] -coins = json.load(open('../../../trezor-common/coins.json', 'r')) +coins = json.load(open('../../vendor/trezor-common/coins.json', 'r')) print('COINS = [') for c in coins: From 5a6b2a5a97f7b5a0cb35907288de39e604bd9e95 Mon Sep 17 00:00:00 2001 From: Jan Pochyla Date: Wed, 15 Nov 2017 14:03:24 +0100 Subject: [PATCH 12/23] wallet/signing: move default input sequence to sanitizer --- src/apps/wallet/sign_tx/helpers.py | 6 ++++-- src/apps/wallet/sign_tx/writers.py | 10 ++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/apps/wallet/sign_tx/helpers.py b/src/apps/wallet/sign_tx/helpers.py index 78d62c3f85..d6ffb5ab5f 100644 --- a/src/apps/wallet/sign_tx/helpers.py +++ b/src/apps/wallet/sign_tx/helpers.py @@ -107,8 +107,10 @@ def sanitize_tx_meta(tx: TransactionType) -> TransactionType: def sanitize_tx_input(tx: TransactionType) -> TxInputType: txi = tx.inputs[0] - txi.script_type = ( - txi.script_type if txi.script_type is not None else InputScriptType.SPENDADDRESS) + if txi.script_type is None: + txi.script_type = InputScriptType.SPENDADDRESS + if txi.sequence is None: + txi.sequence = 4294967295 return txi diff --git a/src/apps/wallet/sign_tx/writers.py b/src/apps/wallet/sign_tx/writers.py index 9564a6fab2..a9e8423bfe 100644 --- a/src/apps/wallet/sign_tx/writers.py +++ b/src/apps/wallet/sign_tx/writers.py @@ -1,3 +1,5 @@ +from micropython import const + from trezor.messages.TxOutputBinType import TxOutputBinType from trezor.messages.TxInputType import TxInputType from trezor.crypto.hashlib import sha256 @@ -5,26 +7,22 @@ from trezor.crypto.hashlib import sha256 # TX Serialization # === -_DEFAULT_SEQUENCE = 4294967295 - def write_tx_input(w, i: TxInputType): - i_sequence = i.sequence if i.sequence is not None else _DEFAULT_SEQUENCE write_bytes_rev(w, i.prev_hash) write_uint32(w, i.prev_index) write_varint(w, len(i.script_sig)) write_bytes(w, i.script_sig) - write_uint32(w, i_sequence) + write_uint32(w, i.sequence) def write_tx_input_check(w, i: TxInputType): - i_sequence = i.sequence if i.sequence is not None else _DEFAULT_SEQUENCE write_bytes(w, i.prev_hash) write_uint32(w, i.prev_index) write_uint32(w, len(i.address_n)) for n in i.address_n: write_uint32(w, n) - write_uint32(w, i_sequence) + write_uint32(w, i.sequence) i_amount = i.amount if i.amount is not None else 0 write_uint32(w, i_amount) # this is probably redundant, but better safe than sorry From f56d39a447cfed61bdb1ac19cd0f5c0f75e61a75 Mon Sep 17 00:00:00 2001 From: Jan Pochyla Date: Wed, 15 Nov 2017 14:34:11 +0100 Subject: [PATCH 13/23] wallet/signing: stream signatures for segwit inputs --- src/apps/wallet/sign_tx/signing.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/apps/wallet/sign_tx/signing.py b/src/apps/wallet/sign_tx/signing.py index 30d385cc6d..50f0eae544 100644 --- a/src/apps/wallet/sign_tx/signing.py +++ b/src/apps/wallet/sign_tx/signing.py @@ -231,13 +231,12 @@ async def sign_tx(tx: SignTx, root): bip143_hash = bip143.preimage_hash(tx, txi, ecdsa_hash_pubkey(key_sign_pub)) signature = ecdsa_sign(key_sign, bip143_hash) - witness = get_p2wpkh_witness(signature, key_sign_pub) + tx_ser.signature_index = i + tx_ser.signature = signature tx_ser.serialized_tx = witness tx_req.serialized = tx_ser - # else - # witness is 0x00 write_uint32(tx_ser.serialized_tx, tx.lock_time) From 1d9707734319cde3aa1a5e83b60e8cd125f87e66 Mon Sep 17 00:00:00 2001 From: Tomas Susanka Date: Mon, 20 Nov 2017 12:44:47 +0100 Subject: [PATCH 14/23] wallet/signing: typo - hex instead of magic number --- src/apps/wallet/sign_tx/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apps/wallet/sign_tx/helpers.py b/src/apps/wallet/sign_tx/helpers.py index d6ffb5ab5f..91b0034484 100644 --- a/src/apps/wallet/sign_tx/helpers.py +++ b/src/apps/wallet/sign_tx/helpers.py @@ -110,7 +110,7 @@ def sanitize_tx_input(tx: TransactionType) -> TxInputType: if txi.script_type is None: txi.script_type = InputScriptType.SPENDADDRESS if txi.sequence is None: - txi.sequence = 4294967295 + txi.sequence = 0xffffffff return txi From b1164077e915d5b37a3c90e426ef4822f32409db Mon Sep 17 00:00:00 2001 From: Jan Pochyla Date: Mon, 20 Nov 2017 12:47:39 +0100 Subject: [PATCH 15/23] wallet/signing: add change output restrictions --- src/apps/wallet/sign_tx/scripts.py | 12 ++- src/apps/wallet/sign_tx/signing.py | 155 +++++++++++++++++++---------- 2 files changed, 112 insertions(+), 55 deletions(-) diff --git a/src/apps/wallet/sign_tx/scripts.py b/src/apps/wallet/sign_tx/scripts.py index 5c7a794c98..7dc630b0de 100644 --- a/src/apps/wallet/sign_tx/scripts.py +++ b/src/apps/wallet/sign_tx/scripts.py @@ -56,12 +56,14 @@ def input_script_native_p2wpkh_or_p2wsh() -> bytearray: return bytearray(0) -# output script consists of 00 14 <20-byte-key-hash> -def output_script_native_p2wpkh_or_p2wsh(pubkeyhash: bytes) -> bytearray: - w = bytearray_with_cap(3 + len(pubkeyhash)) +# output script is either: +# 00 14 <20-byte-key-hash> +# 00 20 <32-byte-script-hash> +def output_script_native_p2wpkh_or_p2wsh(witprog: bytes) -> bytearray: + w = bytearray_with_cap(3 + len(witprog)) w.append(0x00) # witness version byte - w.append(len(pubkeyhash)) # pub key hash length is 20 (P2WPKH) or 32 (P2WSH) bytes - write_bytes(w, pubkeyhash) # pub key hash + w.append(len(witprog)) # pub key hash length is 20 (P2WPKH) or 32 (P2WSH) bytes + write_bytes(w, witprog) # pub key hash return w diff --git a/src/apps/wallet/sign_tx/signing.py b/src/apps/wallet/sign_tx/signing.py index 50f0eae544..8ecebd130d 100644 --- a/src/apps/wallet/sign_tx/signing.py +++ b/src/apps/wallet/sign_tx/signing.py @@ -13,6 +13,16 @@ from apps.wallet.sign_tx.segwit_bip143 import * from apps.wallet.sign_tx.helpers import * from apps.wallet.sign_tx.scripts import * +# the number of bip32 levels used in a wallet (chain and address) +_BIP32_WALLET_DEPTH = const(2) + +# the chain id used for change +_BIP32_CHANGE_CHAIN = const(1) + +# the maximum allowed change address. this should be large enough for normal +# use and still allow to quickly brute-force the correct bip32 path +_BIP32_MAX_LAST_ELEMENT = const(1000000) + class SigningError(ValueError): pass @@ -24,6 +34,7 @@ class SigningError(ValueError): # for pseudo code overview # === + # Phase 1 # - check inputs, previous transactions, and outputs # - ask for confirmations @@ -45,37 +56,47 @@ async def check_tx_fee(tx: SignTx, root): total_in = 0 # sum of input amounts total_out = 0 # sum of output amounts change_out = 0 # change output amount + wallet_path = [] # common prefix of input paths segwit = {} # dict of booleans stating if input is segwit for i in range(tx.inputs_count): # STAGE_REQUEST_1_INPUT txi = await request_tx_input(tx_req, i) + wallet_path = input_extract_wallet_path(txi, wallet_path) write_tx_input_check(h_first, txi) - if txi.script_type in (InputScriptType.SPENDP2SHWITNESS, InputScriptType.SPENDWITNESS): + if (txi.script_type == InputScriptType.SPENDWITNESS or + txi.script_type == InputScriptType.SPENDP2SHWITNESS): + if not coin.segwit: + raise SigningError(FailureType.DataError, + 'Segwit not enabled on this coin') + if not txi.amount: + raise SigningError(FailureType.DataError, + 'Segwit input without amount') segwit[i] = True - # Add I to segwit hash_prevouts, hash_sequence bip143.add_prevouts(txi) bip143.add_sequence(txi) total_in += txi.amount - else: + elif txi.script_type == InputScriptType.SPENDADDRESS: segwit[i] = False total_in += await get_prevtx_output_value( tx_req, txi.prev_hash, txi.prev_index) + else: + raise SigningError(FailureType.DataError, + 'Wrong input script type') for o in range(tx.outputs_count): # STAGE_REQUEST_3_OUTPUT txo = await request_tx_output(tx_req, o) - if output_is_change(txo): + txo_bin.amount = txo.amount + txo_bin.script_pubkey = output_derive_script(txo, coin, root) + if output_is_change(txo, wallet_path): if change_out != 0: raise SigningError(FailureType.ProcessError, 'Only one change output is valid') change_out = txo.amount - else: - if not await confirm_output(txo, coin): - raise SigningError(FailureType.ActionCancelled, - 'Output cancelled') - txo_bin.amount = txo.amount - txo_bin.script_pubkey = output_derive_script(txo, coin, root) + elif not await confirm_output(txo, coin): + raise SigningError(FailureType.ActionCancelled, + 'Output cancelled') write_tx_output(h_first, txo_bin) bip143.add_output(txo_bin) total_out += txo_bin.amount @@ -85,7 +106,8 @@ async def check_tx_fee(tx: SignTx, root): raise SigningError(FailureType.NotEnoughFunds, 'Not enough funds') - if fee > coin.maxfee_kb * ((estimate_tx_size(tx.inputs_count, tx.outputs_count) + 999) // 1000): + tx_size_b = estimate_tx_size(tx.inputs_count, tx.outputs_count) + if fee > coin.maxfee_kb * ((tx_size_b + 999) // 1000): if not await confirm_feeoverthreshold(fee, coin): raise SigningError(FailureType.ActionCancelled, 'Signing cancelled') @@ -94,7 +116,7 @@ async def check_tx_fee(tx: SignTx, root): raise SigningError(FailureType.ActionCancelled, 'Total cancelled') - return h_first, tx_req, txo_bin, bip143, segwit, total_in + return h_first, tx_req, txo_bin, bip143, segwit, total_in, wallet_path async def sign_tx(tx: SignTx, root): @@ -103,7 +125,8 @@ async def sign_tx(tx: SignTx, root): # Phase 1 - h_first, tx_req, txo_bin, bip143, segwit, authorized_in = await check_tx_fee(tx, root) + h_first, tx_req, txo_bin, bip143, segwit, authorized_in, wallet_path = \ + await check_tx_fee(tx, root) # Phase 2 # - sign inputs @@ -129,32 +152,37 @@ async def sign_tx(tx: SignTx, root): if segwit[i_sign]: # STAGE_REQUEST_SEGWIT_INPUT txi_sign = await request_tx_input(tx_req, i_sign) - write_tx_input_check(h_second, txi_sign) - if txi_sign.script_type in (InputScriptType.SPENDP2SHWITNESS, InputScriptType.SPENDWITNESS): - key_sign = node_derive(root, txi_sign.address_n) - key_sign_pub = key_sign.public_key() - txi_sign.script_sig = input_derive_script(txi_sign, key_sign_pub) - w_txi = bytearray_with_cap( - 7 + len(txi_sign.prev_hash) + 4 + len(txi_sign.script_sig) + 4) - if i_sign == 0: # serializing first input => prepend headers - write_bytes(w_txi, get_tx_header(tx, True)) - write_tx_input(w_txi, txi_sign) - tx_ser.serialized_tx = w_txi + if (txi_sign.script_type != InputScriptType.SPENDWITNESS and + txi_sign.script_type != InputScriptType.SPENDP2SHWITNESS): + raise SigningError(FailureType.ProcessError, + 'Transaction has changed during signing') + input_check_wallet_path(txi_sign, wallet_path) + write_tx_input_check(h_second, txi_sign) + + key_sign = node_derive(root, txi_sign.address_n) + key_sign_pub = key_sign.public_key() + txi_sign.script_sig = input_derive_script(txi_sign, key_sign_pub) + w_txi = bytearray_with_cap( + 7 + len(txi_sign.prev_hash) + 4 + len(txi_sign.script_sig) + 4) + if i_sign == 0: # serializing first input => prepend headers + write_bytes(w_txi, get_tx_header(tx, True)) + write_tx_input(w_txi, txi_sign) + tx_ser.serialized_tx = w_txi tx_req.serialized = tx_ser else: for i in range(tx.inputs_count): # STAGE_REQUEST_4_INPUT txi = await request_tx_input(tx_req, i) + input_check_wallet_path(txi, wallet_path) write_tx_input_check(h_second, txi) if i == i_sign: txi_sign = txi key_sign = node_derive(root, txi.address_n) key_sign_pub = key_sign.public_key() - # the signature has to be also over the output script to prevent modification - # todo this should fail for p2sh - txi_sign.script_sig = output_script_p2pkh(ecdsa_hash_pubkey(key_sign_pub)) + txi_sign.script_sig = output_script_p2pkh( + ecdsa_hash_pubkey(key_sign_pub)) else: txi.script_sig = bytes() write_tx_input(h_sign, txi) @@ -219,9 +247,9 @@ async def sign_tx(tx: SignTx, root): if segwit[i]: # STAGE_REQUEST_SEGWIT_WITNESS txi = await request_tx_input(tx_req, i) + input_check_wallet_path(txi, wallet_path) - # Check amount and the control digests - if txi.amount > authorized_in or (get_tx_hash(h_first, False) != get_tx_hash(h_second, False)): + if txi.amount > authorized_in: raise SigningError(FailureType.ProcessError, 'Transaction has changed during signing') authorized_in -= txi.amount @@ -237,6 +265,8 @@ async def sign_tx(tx: SignTx, root): tx_ser.signature = signature tx_ser.serialized_tx = witness tx_req.serialized = tx_ser + else: + pass # TODO: empty witness write_uint32(tx_ser.serialized_tx, tx.lock_time) @@ -310,13 +340,13 @@ def get_address(script_type: InputScriptType, coin: CoinType, node) -> bytes: elif script_type == InputScriptType.SPENDWITNESS: # native p2wpkh if not coin.segwit or not coin.bech32_prefix: raise SigningError(FailureType.ProcessError, - 'Coin does not support segwit') + 'Segwit not enabled on this coin') return address_p2wpkh(node.public_key(), coin.bech32_prefix) elif script_type == InputScriptType.SPENDP2SHWITNESS: # p2wpkh using p2sh if not coin.segwit or not coin.address_type_p2sh: raise SigningError(FailureType.ProcessError, - 'Coin does not support segwit') + 'Segwit not enabled on this coin') return address_p2wpkh_in_p2sh(node.public_key(), coin.address_type_p2sh) else: @@ -340,9 +370,12 @@ def address_p2wpkh_in_p2sh_raw(pubkey: bytes) -> bytes: return h +_BECH32_WITVER = const(0x00) + + def address_p2wpkh(pubkey: bytes, hrp: str) -> str: pubkeyhash = ecdsa_hash_pubkey(pubkey) - address = bech32.encode(hrp, 0, pubkeyhash) # TODO: constant? + address = bech32.encode(hrp, _BECH32_WITVER, pubkeyhash) if address is None: raise SigningError(FailureType.ProcessError, 'Invalid address') @@ -351,8 +384,8 @@ def address_p2wpkh(pubkey: bytes, hrp: str) -> str: def decode_bech32_address(prefix: str, address: str) -> bytes: witver, raw = bech32.decode(prefix, address) - if witver != 0: # TODO: constant? - raise SigningError(FailureType.ProcessError, + if witver != _BECH32_WITVER: + raise SigningError(FailureType.DataError, 'Invalid address witness program') return bytes(raw) @@ -365,26 +398,23 @@ def output_derive_script(o: TxOutputType, coin: CoinType, root) -> bytes: if o.script_type == OutputScriptType.PAYTOOPRETURN: if o.amount != 0: - raise SigningError(FailureType.ProcessError, + raise SigningError(FailureType.DataError, 'OP_RETURN output with non-zero amount') return output_script_paytoopreturn(o.op_return_data) if o.address_n: # change output if o.address: - raise SigningError(FailureType.ProcessError, - 'Both address_n and address provided') - address = get_address_for_change(o, coin, root) + raise SigningError(FailureType.DataError, 'Address in change output') + o.address = get_address_for_change(o, coin, root) else: if not o.address: - raise SigningError(FailureType.ProcessError, 'Missing address') - address = o.address + raise SigningError(FailureType.DataError, 'Missing address') - if coin.bech32_prefix and address.startswith(coin.bech32_prefix): # p2wpkh or p2wsh - # todo check if p2wsh works - pubkeyhash = decode_bech32_address(coin.bech32_prefix, address) - return output_script_native_p2wpkh_or_p2wsh(pubkeyhash) + if coin.bech32_prefix and o.address.startswith(coin.bech32_prefix): # p2wpkh or p2wsh + witprog = decode_bech32_address(coin.bech32_prefix, o.address) + return output_script_native_p2wpkh_or_p2wsh(witprog) - raw_address = base58.decode_check(address) + raw_address = base58.decode_check(o.address) if address_type.check(coin.address_type, raw_address): # p2pkh pubkeyhash = address_type.strip(coin.address_type, raw_address) @@ -394,11 +424,10 @@ def output_derive_script(o: TxOutputType, coin: CoinType, root) -> bytes: scripthash = address_type.strip(coin.address_type_p2sh, raw_address) return output_script_p2sh(scripthash) - raise SigningError(FailureType.ProcessError, 'Invalid address type') + raise SigningError(FailureType.DataError, 'Invalid address type') def get_address_for_change(o: TxOutputType, coin: CoinType, root): - if o.script_type == OutputScriptType.PAYTOADDRESS: input_script_type = InputScriptType.SPENDADDRESS elif o.script_type == OutputScriptType.PAYTOMULTISIG: @@ -408,12 +437,16 @@ def get_address_for_change(o: TxOutputType, coin: CoinType, root): elif o.script_type == OutputScriptType.PAYTOP2SHWITNESS: input_script_type = InputScriptType.SPENDP2SHWITNESS else: - raise SigningError(FailureType.ProcessError, 'Invalid script type') + raise SigningError(FailureType.DataError, 'Invalid script type') return get_address(input_script_type, coin, node_derive(root, o.address_n)) -def output_is_change(o: TxOutputType) -> bool: - return bool(o.address_n) +def output_is_change(o: TxOutputType, wallet_path: list) -> bool: + address_n = o.address_n + return (address_n is not None and wallet_path is not None + and wallet_path == address_n[:-_BIP32_WALLET_DEPTH] + and address_n[-2] == _BIP32_CHANGE_CHAIN + and address_n[-1] <= _BIP32_MAX_LAST_ELEMENT) # Tx Inputs @@ -434,6 +467,28 @@ def input_derive_script(i: TxInputType, pubkey: bytes, signature: bytes=None) -> raise SigningError(FailureType.ProcessError, 'Invalid script type') +def input_extract_wallet_path(txi: TxInputType, wallet_path: list) -> list: + if wallet_path is None: + return None # there was a mismatch in previous inputs + address_n = txi.address_n[:-_BIP32_WALLET_DEPTH] + if not address_n: + return None # input path is too short + if not wallet_path: + return address_n # this is the first input + if wallet_path == address_n: + return address_n # paths match + return None # paths don't match + + +def input_check_wallet_path(txi: TxInputType, wallet_path: list) -> list: + if wallet_path is None: + return # there was a mismatch in Phase 1, ignore it now + address_n = txi.address_n[:-_BIP32_WALLET_DEPTH] + if wallet_path != address_n: + raise SigningError(FailureType.ProcessError, + 'Transaction has changed during signing') + + def node_derive(root, address_n: list): node = root.clone() node.derive_path(address_n) From 26265ca51176b12265e8e4eeae1bfa2882616eeb Mon Sep 17 00:00:00 2001 From: Jan Pochyla Date: Mon, 20 Nov 2017 12:47:54 +0100 Subject: [PATCH 16/23] tests: enable signtx segwit tests --- tests/run_tests_device.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/run_tests_device.sh b/tests/run_tests_device.sh index 9fb20594ea..5289f94151 100755 --- a/tests/run_tests_device.sh +++ b/tests/run_tests_device.sh @@ -30,8 +30,6 @@ pytest \ --ignore test_msg_signmessage_segwit_native.py \ --ignore test_msg_signmessage_segwit.py \ --ignore test_msg_signtx_bch.py \ - --ignore test_msg_signtx_segwit_native.py \ - --ignore test_msg_signtx_segwit.py \ --ignore test_msg_signtx_zcash.py \ --ignore test_msg_verifymessage_segwit_native.py \ --ignore test_msg_verifymessage_segwit.py \ From 4fdc389a22818d3406ec9b284c4672c9947351e4 Mon Sep 17 00:00:00 2001 From: Tomas Susanka Date: Mon, 20 Nov 2017 17:18:55 +0100 Subject: [PATCH 17/23] bip143: all inputs (not only segwit ones) are added to bip143 --- src/apps/wallet/sign_tx/signing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/apps/wallet/sign_tx/signing.py b/src/apps/wallet/sign_tx/signing.py index 8ecebd130d..47f8621762 100644 --- a/src/apps/wallet/sign_tx/signing.py +++ b/src/apps/wallet/sign_tx/signing.py @@ -64,6 +64,8 @@ async def check_tx_fee(tx: SignTx, root): txi = await request_tx_input(tx_req, i) wallet_path = input_extract_wallet_path(txi, wallet_path) write_tx_input_check(h_first, txi) + bip143.add_prevouts(txi) + bip143.add_sequence(txi) if (txi.script_type == InputScriptType.SPENDWITNESS or txi.script_type == InputScriptType.SPENDP2SHWITNESS): if not coin.segwit: @@ -73,8 +75,6 @@ async def check_tx_fee(tx: SignTx, root): raise SigningError(FailureType.DataError, 'Segwit input without amount') segwit[i] = True - bip143.add_prevouts(txi) - bip143.add_sequence(txi) total_in += txi.amount elif txi.script_type == InputScriptType.SPENDADDRESS: segwit[i] = False From d8a884bd87d0198bcf105e7ccac981b0830f53e5 Mon Sep 17 00:00:00 2001 From: Tomas Susanka Date: Mon, 20 Nov 2017 17:41:23 +0100 Subject: [PATCH 18/23] tests: unit tests signature fixtures for segwit this fixes broken unit tests introduced by a3d1bdbb862eb2db978115e9e533da81d46a350d --- tests/test_apps.wallet.segwit.signtx.native_p2wpkh.py | 8 ++++---- tests/test_apps.wallet.segwit.signtx.p2wpkh_in_p2sh.py | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/test_apps.wallet.segwit.signtx.native_p2wpkh.py b/tests/test_apps.wallet.segwit.signtx.native_p2wpkh.py index 562809782d..d56c24c099 100644 --- a/tests/test_apps.wallet.segwit.signtx.native_p2wpkh.py +++ b/tests/test_apps.wallet.segwit.signtx.native_p2wpkh.py @@ -102,8 +102,8 @@ class TestSignSegwitTxNativeP2WPKH(unittest.TestCase): TxRequest(request_type=TXFINISHED, details=None, serialized=TxRequestSerializedType( serialized_tx=unhexlify('02483045022100a7ca8f097525f9044e64376dc0a0f5d4aeb8d15d66808ba97979a0475b06b66502200597c8ebcef63e047f9aeef1a8001d3560470cf896c12f6990eec4faec599b950121033add1f0e8e3c3136f7428dd4a4de1057380bd311f5b0856e2269170b4ffa65bf00000000'), - signature_index=None, - signature=None, + signature_index=0, + signature=unhexlify('3045022100a7ca8f097525f9044e64376dc0a0f5d4aeb8d15d66808ba97979a0475b06b66502200597c8ebcef63e047f9aeef1a8001d3560470cf896c12f6990eec4faec599b95'), )), ] @@ -191,8 +191,8 @@ class TestSignSegwitTxNativeP2WPKH(unittest.TestCase): TxRequest(request_type=TXFINISHED, details=None, serialized=TxRequestSerializedType( serialized_tx=unhexlify('02483045022100a7ca8f097525f9044e64376dc0a0f5d4aeb8d15d66808ba97979a0475b06b66502200597c8ebcef63e047f9aeef1a8001d3560470cf896c12f6990eec4faec599b950121033add1f0e8e3c3136f7428dd4a4de1057380bd311f5b0856e2269170b4ffa65bf00000000'), - signature_index=None, - signature=None, + signature_index=0, + signature=unhexlify('3045022100a7ca8f097525f9044e64376dc0a0f5d4aeb8d15d66808ba97979a0475b06b66502200597c8ebcef63e047f9aeef1a8001d3560470cf896c12f6990eec4faec599b95'), )), ] diff --git a/tests/test_apps.wallet.segwit.signtx.p2wpkh_in_p2sh.py b/tests/test_apps.wallet.segwit.signtx.p2wpkh_in_p2sh.py index 294a7be403..25ae38c445 100644 --- a/tests/test_apps.wallet.segwit.signtx.p2wpkh_in_p2sh.py +++ b/tests/test_apps.wallet.segwit.signtx.p2wpkh_in_p2sh.py @@ -102,8 +102,8 @@ class TestSignSegwitTxP2WPKHInP2SH(unittest.TestCase): TxRequest(request_type=TXFINISHED, details=None, serialized=TxRequestSerializedType( serialized_tx=unhexlify('02483045022100ccd253bfdf8a5593cd7b6701370c531199f0f05a418cd547dfc7da3f21515f0f02203fa08a0753688871c220648f9edadbdb98af42e5d8269364a326572cf703895b012103e7bfe10708f715e8538c92d46ca50db6f657bbc455b7494e6a0303ccdb868b7900000000'), - signature_index=None, - signature=None, + signature_index=0, + signature=unhexlify('3045022100ccd253bfdf8a5593cd7b6701370c531199f0f05a418cd547dfc7da3f21515f0f02203fa08a0753688871c220648f9edadbdb98af42e5d8269364a326572cf703895b'), )), ] @@ -201,8 +201,8 @@ class TestSignSegwitTxP2WPKHInP2SH(unittest.TestCase): TxRequest(request_type=TXFINISHED, details=None, serialized=TxRequestSerializedType( serialized_tx=unhexlify( '02483045022100ccd253bfdf8a5593cd7b6701370c531199f0f05a418cd547dfc7da3f21515f0f02203fa08a0753688871c220648f9edadbdb98af42e5d8269364a326572cf703895b012103e7bfe10708f715e8538c92d46ca50db6f657bbc455b7494e6a0303ccdb868b7900000000'), - signature_index=None, - signature=None, + signature_index=0, + signature=unhexlify('3045022100ccd253bfdf8a5593cd7b6701370c531199f0f05a418cd547dfc7da3f21515f0f02203fa08a0753688871c220648f9edadbdb98af42e5d8269364a326572cf703895b'), )), ] @@ -249,7 +249,7 @@ class TestSignSegwitTxP2WPKHInP2SH(unittest.TestCase): address_n=[49 | 0x80000000, 1 | 0x80000000, 0 | 0x80000000, 1, 0], script_type=OutputScriptType.PAYTOP2SHWITNESS, amount=1, - address=None, # todo ask about sanitizing + address=None, ) tx = SignTx(coin_name='Testnet', version=None, lock_time=None, inputs_count=1, outputs_count=2) From 0e2bcd64e8dfe239b6afd6f3363e5f38f2895fac Mon Sep 17 00:00:00 2001 From: Tomas Susanka Date: Mon, 20 Nov 2017 17:41:23 +0100 Subject: [PATCH 19/23] tests: o.address fix --- src/apps/wallet/sign_tx/signing.py | 10 +++++++--- .../test_apps.wallet.segwit.signtx.native_p2wpkh.py | 7 ++++--- .../test_apps.wallet.segwit.signtx.p2wpkh_in_p2sh.py | 12 +++++++----- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/apps/wallet/sign_tx/signing.py b/src/apps/wallet/sign_tx/signing.py index 47f8621762..f0598e9449 100644 --- a/src/apps/wallet/sign_tx/signing.py +++ b/src/apps/wallet/sign_tx/signing.py @@ -116,7 +116,7 @@ async def check_tx_fee(tx: SignTx, root): raise SigningError(FailureType.ActionCancelled, 'Total cancelled') - return h_first, tx_req, txo_bin, bip143, segwit, total_in, wallet_path + return h_first, bip143, segwit, total_in, wallet_path async def sign_tx(tx: SignTx, root): @@ -125,8 +125,7 @@ async def sign_tx(tx: SignTx, root): # Phase 1 - h_first, tx_req, txo_bin, bip143, segwit, authorized_in, wallet_path = \ - await check_tx_fee(tx, root) + h_first, bip143, segwit, authorized_in, wallet_path = await check_tx_fee(tx, root) # Phase 2 # - sign inputs @@ -135,6 +134,11 @@ async def sign_tx(tx: SignTx, root): coin = coins.by_name(tx.coin_name) tx_ser = TxRequestSerializedType() + txo_bin = TxOutputBinType() + tx_req = TxRequest() + tx_req.details = TxRequestDetailsType() + tx_req.serialized = None + for i_sign in range(tx.inputs_count): # hash of what we are signing with this input h_sign = HashWriter(sha256) diff --git a/tests/test_apps.wallet.segwit.signtx.native_p2wpkh.py b/tests/test_apps.wallet.segwit.signtx.native_p2wpkh.py index d56c24c099..d9370b43a0 100644 --- a/tests/test_apps.wallet.segwit.signtx.native_p2wpkh.py +++ b/tests/test_apps.wallet.segwit.signtx.native_p2wpkh.py @@ -44,7 +44,7 @@ class TestSignSegwitTxNativeP2WPKH(unittest.TestCase): address_n=None, # @todo ask honza about sanitizing ) out2 = TxOutputType( - address='tb1q694ccp5qcc0udmfwgp692u2s2hjpq5h407urtu', + address='tb1q694ccp5qcc0udmfwgp692u2s2hjpq5h407urtu', script_type=OutputScriptType.PAYTOADDRESS, amount=12300000 - 11000 - 5000000, address_n=None, @@ -170,7 +170,8 @@ class TestSignSegwitTxNativeP2WPKH(unittest.TestCase): # returned serialized inp1 serialized_tx=unhexlify('010000000001018a44999c07bba32df1cacdc50987944e68e3205b4429438fdde35c76024614090000000000ffffffff'), )), - TxAck(tx=TransactionType(outputs=[out1])), + # the out has to be cloned not to send the same object which was modified + TxAck(tx=TransactionType(outputs=[TxOutputType(**out1.__dict__)])), TxRequest(request_type=TXOUTPUT, details=TxRequestDetailsType(request_index=1, tx_hash=None), serialized=TxRequestSerializedType( # returned serialized out1 @@ -178,7 +179,7 @@ class TestSignSegwitTxNativeP2WPKH(unittest.TestCase): signature_index=None, signature=None, )), - TxAck(tx=TransactionType(outputs=[out2])), + TxAck(tx=TransactionType(outputs=[TxOutputType(**out2.__dict__)])), # segwit TxRequest(request_type=TXINPUT, details=TxRequestDetailsType(request_index=0, tx_hash=None), serialized=TxRequestSerializedType( diff --git a/tests/test_apps.wallet.segwit.signtx.p2wpkh_in_p2sh.py b/tests/test_apps.wallet.segwit.signtx.p2wpkh_in_p2sh.py index 25ae38c445..83fe499e30 100644 --- a/tests/test_apps.wallet.segwit.signtx.p2wpkh_in_p2sh.py +++ b/tests/test_apps.wallet.segwit.signtx.p2wpkh_in_p2sh.py @@ -175,8 +175,9 @@ class TestSignSegwitTxP2WPKHInP2SH(unittest.TestCase): serialized_tx=unhexlify( '0100000000010137c361fb8f2d9056ba8c98c5611930fcb48cacfdd0fe2e0449d83eea982f91200000000017160014d16b8c0680c61fc6ed2e407455715055e41052f5ffffffff'), )), - TxAck(tx=TransactionType(outputs=[out1])), - # here + # the out has to be cloned not to send the same object which was modified + TxAck(tx=TransactionType(outputs=[TxOutputType(**out1.__dict__)])), + TxRequest(request_type=TXOUTPUT, details=TxRequestDetailsType(request_index=1, tx_hash=None), serialized=TxRequestSerializedType( # returned serialized out1 @@ -185,7 +186,7 @@ class TestSignSegwitTxP2WPKHInP2SH(unittest.TestCase): signature_index=None, signature=None, )), - TxAck(tx=TransactionType(outputs=[out2])), + TxAck(tx=TransactionType(outputs=[TxOutputType(**out2.__dict__)])), # segwit TxRequest(request_type=TXINPUT, details=TxRequestDetailsType(request_index=0, tx_hash=None), @@ -284,7 +285,8 @@ class TestSignSegwitTxP2WPKHInP2SH(unittest.TestCase): serialized_tx=unhexlify( '0100000000010137c361fb8f2d9056ba8c98c5611930fcb48cacfdd0fe2e0449d83eea982f91200000000017160014d16b8c0680c61fc6ed2e407455715055e41052f5ffffffff'), )), - TxAck(tx=TransactionType(outputs=[out1])), + # the out has to be cloned not to send the same object which was modified + TxAck(tx=TransactionType(outputs=[TxOutputType(**out1.__dict__)])), TxRequest(request_type=TXOUTPUT, details=TxRequestDetailsType(request_index=1, tx_hash=None), serialized=TxRequestSerializedType( @@ -294,7 +296,7 @@ class TestSignSegwitTxP2WPKHInP2SH(unittest.TestCase): signature_index=None, signature=None, )), - TxAck(tx=TransactionType(outputs=[out2])), + TxAck(tx=TransactionType(outputs=[TxOutputType(**out2.__dict__)])), # segwit TxRequest(request_type=TXINPUT, details=TxRequestDetailsType(request_index=0, tx_hash=None), From 165faaeba42afc86487c0462db729b4f88b9d66a Mon Sep 17 00:00:00 2001 From: Jan Pochyla Date: Tue, 21 Nov 2017 16:13:33 +0100 Subject: [PATCH 20/23] wallet/signing: codestyle --- src/apps/wallet/sign_tx/layout.py | 8 +++- src/apps/wallet/sign_tx/segwit_bip143.py | 6 ++- src/apps/wallet/sign_tx/signing.py | 51 +++++++++++++----------- src/apps/wallet/sign_tx/writers.py | 4 +- 4 files changed, 40 insertions(+), 29 deletions(-) diff --git a/src/apps/wallet/sign_tx/layout.py b/src/apps/wallet/sign_tx/layout.py index b85a7ca901..ce1bb6b19c 100644 --- a/src/apps/wallet/sign_tx/layout.py +++ b/src/apps/wallet/sign_tx/layout.py @@ -2,6 +2,7 @@ from trezor import ui from trezor.utils import chunks from trezor.ui.text import Text from trezor.messages import ButtonRequestType +from trezor.messages import OutputScriptType from apps.common.confirm import confirm from apps.common.confirm import hold_to_confirm @@ -15,11 +16,14 @@ def split_address(address): async def confirm_output(ctx, output, coin): - # TODO: handle OP_RETURN correctly + if output.script_type == OutputScriptType.PAYTOOPRETURN: + address = 'OP_RETURN' # TODO: handle OP_RETURN correctly + else: + address = output.address content = Text('Confirm output', ui.ICON_RESET, ui.BOLD, format_amount(output.amount, coin), ui.NORMAL, 'to', - ui.MONO, *split_address(output.address)) + ui.MONO, *split_address(address)) return await confirm(ctx, content, ButtonRequestType.ConfirmOutput) diff --git a/src/apps/wallet/sign_tx/segwit_bip143.py b/src/apps/wallet/sign_tx/segwit_bip143.py index 4a97c52336..e19d1a4e41 100644 --- a/src/apps/wallet/sign_tx/segwit_bip143.py +++ b/src/apps/wallet/sign_tx/segwit_bip143.py @@ -59,7 +59,9 @@ class Bip143: # for P2WPKH this is always 0x1976a914{20-byte-pubkey-hash}88ac def derive_script_code(self, txi: TxInputType, pubkeyhash: bytes) -> bytearray: # p2wpkh in p2sh or native p2wpkh - if txi.script_type in (InputScriptType.SPENDP2SHWITNESS, InputScriptType.SPENDWITNESS): + is_segwit = (txi.script_type == InputScriptType.SPENDWITNESS or + txi.script_type == InputScriptType.SPENDP2SHWITNESS) + if is_segwit: s = bytearray(25) s[0] = 0x76 # OP_DUP s[1] = 0xA9 # OP_HASH_160 @@ -69,5 +71,5 @@ class Bip143: s[24] = 0xAC # OP_CHECKSIG return s else: - raise Bip143Error(FailureType.SyntaxError, + raise Bip143Error(FailureType.DataError, 'Unknown input script type for bip143 script code') diff --git a/src/apps/wallet/sign_tx/signing.py b/src/apps/wallet/sign_tx/signing.py index f0598e9449..6e948456d3 100644 --- a/src/apps/wallet/sign_tx/signing.py +++ b/src/apps/wallet/sign_tx/signing.py @@ -1,3 +1,5 @@ +from micropython import const + from trezor.crypto.hashlib import sha256, ripemd160 from trezor.crypto.curve import secp256k1 from trezor.crypto import base58, der, bech32 @@ -66,8 +68,9 @@ async def check_tx_fee(tx: SignTx, root): write_tx_input_check(h_first, txi) bip143.add_prevouts(txi) bip143.add_sequence(txi) - if (txi.script_type == InputScriptType.SPENDWITNESS or - txi.script_type == InputScriptType.SPENDP2SHWITNESS): + is_segwit = (txi.script_type == InputScriptType.SPENDWITNESS or + txi.script_type == InputScriptType.SPENDP2SHWITNESS) + if is_segwit: if not coin.segwit: raise SigningError(FailureType.DataError, 'Segwit not enabled on this coin') @@ -140,29 +143,20 @@ async def sign_tx(tx: SignTx, root): tx_req.serialized = None for i_sign in range(tx.inputs_count): - # hash of what we are signing with this input - h_sign = HashWriter(sha256) - # same as h_first, checked at the end of this iteration - h_second = HashWriter(sha256) - txi_sign = None key_sign = None key_sign_pub = None - write_uint32(h_sign, tx.version) - - write_varint(h_sign, tx.inputs_count) - if segwit[i_sign]: # STAGE_REQUEST_SEGWIT_INPUT txi_sign = await request_tx_input(tx_req, i_sign) - if (txi_sign.script_type != InputScriptType.SPENDWITNESS and - txi_sign.script_type != InputScriptType.SPENDP2SHWITNESS): + is_segwit = (txi_sign.script_type == InputScriptType.SPENDWITNESS or + txi_sign.script_type == InputScriptType.SPENDP2SHWITNESS) + if not is_segwit: raise SigningError(FailureType.ProcessError, 'Transaction has changed during signing') input_check_wallet_path(txi_sign, wallet_path) - write_tx_input_check(h_second, txi_sign) key_sign = node_derive(root, txi_sign.address_n) key_sign_pub = key_sign.public_key() @@ -176,6 +170,14 @@ async def sign_tx(tx: SignTx, root): tx_req.serialized = tx_ser else: + # hash of what we are signing with this input + h_sign = HashWriter(sha256) + # same as h_first, checked before signing the digest + h_second = HashWriter(sha256) + + write_uint32(h_sign, tx.version) + write_varint(h_sign, tx.inputs_count) + for i in range(tx.inputs_count): # STAGE_REQUEST_4_INPUT txi = await request_tx_input(tx_req, i) @@ -232,7 +234,6 @@ async def sign_tx(tx: SignTx, root): txo = await request_tx_output(tx_req, o) txo_bin.amount = txo.amount txo_bin.script_pubkey = output_derive_script(txo, coin, root) - write_tx_output(h_second, txo_bin) # for segwit (not yet checked) # serialize output w_txo_bin = bytearray_with_cap( @@ -247,13 +248,17 @@ async def sign_tx(tx: SignTx, root): tx_req.serialized = tx_ser + any_segwit = True in segwit.values() + for i in range(tx.inputs_count): if segwit[i]: # STAGE_REQUEST_SEGWIT_WITNESS txi = await request_tx_input(tx_req, i) input_check_wallet_path(txi, wallet_path) - if txi.amount > authorized_in: + is_segwit = (txi.script_type == InputScriptType.SPENDWITNESS or + txi.script_type == InputScriptType.SPENDP2SHWITNESS) + if not is_segwit or txi.amount > authorized_in: raise SigningError(FailureType.ProcessError, 'Transaction has changed during signing') authorized_in -= txi.amount @@ -265,12 +270,15 @@ async def sign_tx(tx: SignTx, root): signature = ecdsa_sign(key_sign, bip143_hash) witness = get_p2wpkh_witness(signature, key_sign_pub) + tx_ser.serialized_tx = witness tx_ser.signature_index = i tx_ser.signature = signature - tx_ser.serialized_tx = witness - tx_req.serialized = tx_ser - else: - pass # TODO: empty witness + elif any_segwit: + tx_ser.serialized_tx = bytearray(1) # empty witness for non-segwit inputs + tx_ser.signature_index = None + tx_ser.signature = None + + tx_req.serialized = tx_ser write_uint32(tx_ser.serialized_tx, tx.lock_time) @@ -460,13 +468,10 @@ def output_is_change(o: TxOutputType, wallet_path: list) -> bool: def input_derive_script(i: TxInputType, pubkey: bytes, signature: bytes=None) -> bytes: if i.script_type == InputScriptType.SPENDADDRESS: return input_script_p2pkh_or_p2sh(pubkey, signature) # p2pkh or p2sh - if i.script_type == InputScriptType.SPENDP2SHWITNESS: # p2wpkh using p2sh return input_script_p2wpkh_in_p2sh(ecdsa_hash_pubkey(pubkey)) - elif i.script_type == InputScriptType.SPENDWITNESS: # native p2wpkh or p2wsh return input_script_native_p2wpkh_or_p2wsh() - else: raise SigningError(FailureType.ProcessError, 'Invalid script type') diff --git a/src/apps/wallet/sign_tx/writers.py b/src/apps/wallet/sign_tx/writers.py index a9e8423bfe..18eb757b91 100644 --- a/src/apps/wallet/sign_tx/writers.py +++ b/src/apps/wallet/sign_tx/writers.py @@ -19,12 +19,12 @@ def write_tx_input(w, i: TxInputType): def write_tx_input_check(w, i: TxInputType): write_bytes(w, i.prev_hash) write_uint32(w, i.prev_index) + write_uint32(w, i.script_type) write_uint32(w, len(i.address_n)) for n in i.address_n: write_uint32(w, n) write_uint32(w, i.sequence) - i_amount = i.amount if i.amount is not None else 0 - write_uint32(w, i_amount) # this is probably redundant, but better safe than sorry + write_uint32(w, i.amount or 0) def write_tx_output(w, o: TxOutputBinType): From 65481308e6d8eb6cf34aed713f539ea16bf3537f Mon Sep 17 00:00:00 2001 From: Tomas Susanka Date: Thu, 23 Nov 2017 16:45:40 +0100 Subject: [PATCH 21/23] wallet/signing: imports fix --- src/apps/wallet/sign_tx/segwit_bip143.py | 1 + src/apps/wallet/sign_tx/signing.py | 1 + src/apps/wallet/sign_tx/writers.py | 5 ++--- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/apps/wallet/sign_tx/segwit_bip143.py b/src/apps/wallet/sign_tx/segwit_bip143.py index e19d1a4e41..8168ce1b92 100644 --- a/src/apps/wallet/sign_tx/segwit_bip143.py +++ b/src/apps/wallet/sign_tx/segwit_bip143.py @@ -2,6 +2,7 @@ from trezor.crypto.hashlib import sha256 from trezor.messages.SignTx import SignTx from trezor.messages import InputScriptType, FailureType +from apps.wallet.sign_tx.writers import * class Bip143Error(ValueError): pass diff --git a/src/apps/wallet/sign_tx/signing.py b/src/apps/wallet/sign_tx/signing.py index 6e948456d3..2106bf00b5 100644 --- a/src/apps/wallet/sign_tx/signing.py +++ b/src/apps/wallet/sign_tx/signing.py @@ -14,6 +14,7 @@ from apps.common import coins from apps.wallet.sign_tx.segwit_bip143 import * from apps.wallet.sign_tx.helpers import * from apps.wallet.sign_tx.scripts import * +from apps.wallet.sign_tx.writers import * # the number of bip32 levels used in a wallet (chain and address) _BIP32_WALLET_DEPTH = const(2) diff --git a/src/apps/wallet/sign_tx/writers.py b/src/apps/wallet/sign_tx/writers.py index 18eb757b91..2e4bd857f9 100644 --- a/src/apps/wallet/sign_tx/writers.py +++ b/src/apps/wallet/sign_tx/writers.py @@ -1,9 +1,8 @@ -from micropython import const -from trezor.messages.TxOutputBinType import TxOutputBinType -from trezor.messages.TxInputType import TxInputType from trezor.crypto.hashlib import sha256 +from apps.wallet.sign_tx.writers import * + # TX Serialization # === From 7de6a082d3b8dd85b8755e6e8c328b7abe291772 Mon Sep 17 00:00:00 2001 From: Jan Pochyla Date: Thu, 23 Nov 2017 17:30:43 +0100 Subject: [PATCH 22/23] wallet/signing: extract addresses.py --- src/apps/wallet/sign_tx/__init__.py | 2 + src/apps/wallet/sign_tx/addresses.py | 84 ++++++++++++++++++++++++++++ src/apps/wallet/sign_tx/helpers.py | 1 - src/apps/wallet/sign_tx/signing.py | 78 ++------------------------ 4 files changed, 90 insertions(+), 75 deletions(-) create mode 100644 src/apps/wallet/sign_tx/addresses.py diff --git a/src/apps/wallet/sign_tx/__init__.py b/src/apps/wallet/sign_tx/__init__.py index 02cc913a4c..1f13deece8 100644 --- a/src/apps/wallet/sign_tx/__init__.py +++ b/src/apps/wallet/sign_tx/__init__.py @@ -20,6 +20,8 @@ async def sign_tx(ctx, msg): req = signer.send(res) except signing.SigningError as e: raise wire.FailureError(*e.args) + except signing.AddressError as e: + raise wire.FailureError(*e.args) if req.__qualname__ == 'TxRequest': if req.request_type == TXFINISHED: break diff --git a/src/apps/wallet/sign_tx/addresses.py b/src/apps/wallet/sign_tx/addresses.py new file mode 100644 index 0000000000..81051c0d22 --- /dev/null +++ b/src/apps/wallet/sign_tx/addresses.py @@ -0,0 +1,84 @@ +from micropython import const + +from trezor.crypto.hashlib import sha256, ripemd160 +from trezor.crypto import base58, bech32 +from trezor.utils import ensure + +from trezor.messages.CoinType import CoinType +from trezor.messages import FailureType +from trezor.messages import InputScriptType + +# supported witness version for bech32 addresses +_BECH32_WITVER = const(0x00) + + +class AddressError(Exception): + pass + + +def get_address(script_type: InputScriptType, coin: CoinType, node) -> bytes: + + if script_type == InputScriptType.SPENDADDRESS: # p2pkh + return node.address(coin.address_type) + + elif script_type == InputScriptType.SPENDWITNESS: # native p2wpkh + if not coin.segwit or not coin.bech32_prefix: + raise AddressError(FailureType.ProcessError, + 'Segwit not enabled on this coin') + return address_p2wpkh(node.public_key(), coin.bech32_prefix) + + elif script_type == InputScriptType.SPENDP2SHWITNESS: # p2wpkh using p2sh + if not coin.segwit or not coin.address_type_p2sh: + raise AddressError(FailureType.ProcessError, + 'Segwit not enabled on this coin') + return address_p2wpkh_in_p2sh(node.public_key(), coin.address_type_p2sh) + + else: + raise AddressError(FailureType.ProcessError, + 'Invalid script type') + + +def address_p2wpkh_in_p2sh(pubkey: bytes, addrtype: int) -> str: + s = bytearray(21) + s[0] = addrtype + s[1:21] = address_p2wpkh_in_p2sh_raw(pubkey) + return base58.encode_check(bytes(s)) + + +def address_p2wpkh_in_p2sh_raw(pubkey: bytes) -> bytes: + s = bytearray(22) + s[0] = 0x00 # OP_0 + s[1] = 0x14 # pushing 20 bytes + s[2:22] = ecdsa_hash_pubkey(pubkey) + h = sha256(s).digest() + h = ripemd160(h).digest() + return h + + +def address_p2wpkh(pubkey: bytes, hrp: str) -> str: + pubkeyhash = ecdsa_hash_pubkey(pubkey) + address = bech32.encode(hrp, _BECH32_WITVER, pubkeyhash) + if address is None: + raise AddressError(FailureType.ProcessError, + 'Invalid address') + return address + + +def decode_bech32_address(prefix: str, address: str) -> bytes: + witver, raw = bech32.decode(prefix, address) + if witver != _BECH32_WITVER: + raise AddressError(FailureType.ProcessError, + 'Invalid address witness program') + return bytes(raw) + + +def ecdsa_hash_pubkey(pubkey: bytes) -> bytes: + if pubkey[0] == 0x04: + ensure(len(pubkey) == 65) # uncompressed format + elif pubkey[0] == 0x00: + ensure(len(pubkey) == 1) # point at infinity + else: + ensure(len(pubkey) == 33) # compresssed format + h = sha256(pubkey).digest() + h = ripemd160(h).digest() + return h diff --git a/src/apps/wallet/sign_tx/helpers.py b/src/apps/wallet/sign_tx/helpers.py index 91b0034484..3574e09733 100644 --- a/src/apps/wallet/sign_tx/helpers.py +++ b/src/apps/wallet/sign_tx/helpers.py @@ -1,4 +1,3 @@ - from trezor.messages.CoinType import CoinType from trezor.messages.TxOutputType import TxOutputType from trezor.messages.TxOutputBinType import TxOutputBinType diff --git a/src/apps/wallet/sign_tx/signing.py b/src/apps/wallet/sign_tx/signing.py index 2106bf00b5..1f7c5863f2 100644 --- a/src/apps/wallet/sign_tx/signing.py +++ b/src/apps/wallet/sign_tx/signing.py @@ -1,9 +1,8 @@ from micropython import const -from trezor.crypto.hashlib import sha256, ripemd160 +from trezor.crypto.hashlib import sha256 from trezor.crypto.curve import secp256k1 -from trezor.crypto import base58, der, bech32 -from trezor.utils import ensure +from trezor.crypto import base58, der from trezor.messages.TxRequestSerializedType import TxRequestSerializedType from trezor.messages.TxRequestDetailsType import TxRequestDetailsType @@ -11,8 +10,9 @@ from trezor.messages import OutputScriptType from apps.common import address_type from apps.common import coins -from apps.wallet.sign_tx.segwit_bip143 import * +from apps.wallet.sign_tx.addresses import * from apps.wallet.sign_tx.helpers import * +from apps.wallet.sign_tx.segwit_bip143 import * from apps.wallet.sign_tx.scripts import * from apps.wallet.sign_tx.writers import * @@ -345,64 +345,6 @@ def get_p2wpkh_witness(signature: bytes, pubkey: bytes): return w -def get_address(script_type: InputScriptType, coin: CoinType, node) -> bytes: - - if script_type == InputScriptType.SPENDADDRESS: # p2pkh - return node.address(coin.address_type) - - elif script_type == InputScriptType.SPENDWITNESS: # native p2wpkh - if not coin.segwit or not coin.bech32_prefix: - raise SigningError(FailureType.ProcessError, - 'Segwit not enabled on this coin') - return address_p2wpkh(node.public_key(), coin.bech32_prefix) - - elif script_type == InputScriptType.SPENDP2SHWITNESS: # p2wpkh using p2sh - if not coin.segwit or not coin.address_type_p2sh: - raise SigningError(FailureType.ProcessError, - 'Segwit not enabled on this coin') - return address_p2wpkh_in_p2sh(node.public_key(), coin.address_type_p2sh) - - else: - raise SigningError(FailureType.ProcessError, 'Invalid script type') - - -def address_p2wpkh_in_p2sh(pubkey: bytes, addrtype: int) -> str: - s = bytearray(21) - s[0] = addrtype - s[1:21] = address_p2wpkh_in_p2sh_raw(pubkey) - return base58.encode_check(bytes(s)) - - -def address_p2wpkh_in_p2sh_raw(pubkey: bytes) -> bytes: - s = bytearray(22) - s[0] = 0x00 # OP_0 - s[1] = 0x14 # pushing 20 bytes - s[2:22] = ecdsa_hash_pubkey(pubkey) - h = sha256(s).digest() - h = ripemd160(h).digest() - return h - - -_BECH32_WITVER = const(0x00) - - -def address_p2wpkh(pubkey: bytes, hrp: str) -> str: - pubkeyhash = ecdsa_hash_pubkey(pubkey) - address = bech32.encode(hrp, _BECH32_WITVER, pubkeyhash) - if address is None: - raise SigningError(FailureType.ProcessError, - 'Invalid address') - return address - - -def decode_bech32_address(prefix: str, address: str) -> bytes: - witver, raw = bech32.decode(prefix, address) - if witver != _BECH32_WITVER: - raise SigningError(FailureType.DataError, - 'Invalid address witness program') - return bytes(raw) - - # TX Outputs # === @@ -505,18 +447,6 @@ def node_derive(root, address_n: list): return node -def ecdsa_hash_pubkey(pubkey: bytes) -> bytes: - if pubkey[0] == 0x04: - ensure(len(pubkey) == 65) # uncompressed format - elif pubkey[0] == 0x00: - ensure(len(pubkey) == 1) # point at infinity - else: - ensure(len(pubkey) == 33) # compresssed format - h = sha256(pubkey).digest() - h = ripemd160(h).digest() - return h - - def ecdsa_sign(node, digest: bytes) -> bytes: sig = secp256k1.sign(node.private_key(), digest) sigder = der.encode_seq((sig[1:33], sig[33:65])) From 7a0bfd289b26a954c32d0e32e2ea4668fb5fa527 Mon Sep 17 00:00:00 2001 From: Jan Pochyla Date: Thu, 23 Nov 2017 17:35:27 +0100 Subject: [PATCH 23/23] wallet/get_address: support segwit --- src/apps/wallet/get_address.py | 6 ++++-- src/apps/wallet/sign_tx/addresses.py | 2 +- tests/run_tests_device.sh | 2 -- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/apps/wallet/get_address.py b/src/apps/wallet/get_address.py index 63adac96fd..ee136ab23d 100644 --- a/src/apps/wallet/get_address.py +++ b/src/apps/wallet/get_address.py @@ -8,17 +8,19 @@ async def layout_get_address(ctx, msg): from trezor.messages.FailureType import ProcessError from ..common import coins from ..common import seed + from ..wallet.sign_tx import addresses if msg.multisig: raise wire.FailureError(ProcessError, 'GetAddress.multisig is unsupported') address_n = msg.address_n or () coin_name = msg.coin_name or 'Bitcoin' + coin = coins.by_name(coin_name) node = await seed.get_root(ctx) node.derive_path(address_n) - coin = coins.by_name(coin_name) - address = node.address(coin.address_type) + + address = addresses.get_address(msg.script_type, coin, node) if msg.show_display: await _show_address(ctx, address) diff --git a/src/apps/wallet/sign_tx/addresses.py b/src/apps/wallet/sign_tx/addresses.py index 81051c0d22..afa8963c1c 100644 --- a/src/apps/wallet/sign_tx/addresses.py +++ b/src/apps/wallet/sign_tx/addresses.py @@ -16,7 +16,7 @@ class AddressError(Exception): pass -def get_address(script_type: InputScriptType, coin: CoinType, node) -> bytes: +def get_address(script_type: InputScriptType, coin: CoinType, node) -> str: if script_type == InputScriptType.SPENDADDRESS: # p2pkh return node.address(coin.address_type) diff --git a/tests/run_tests_device.sh b/tests/run_tests_device.sh index 5289f94151..506805bf8d 100755 --- a/tests/run_tests_device.sh +++ b/tests/run_tests_device.sh @@ -17,8 +17,6 @@ pytest \ --ignore test_msg_ethereum_signmessage.py \ --ignore test_msg_ethereum_signtx.py \ --ignore test_msg_ethereum_verifymessage.py \ - --ignore test_msg_getaddress_segwit_native.py \ - --ignore test_msg_getaddress_segwit.py \ --ignore test_msg_getaddress_show.py \ --ignore test_msg_loaddevice_xprv.py \ --ignore test_msg_nem_getaddress.py \