From ba8b34b2d760edbdb009eeeb91290584921e181d Mon Sep 17 00:00:00 2001 From: Andrew Kozlik Date: Fri, 27 Mar 2020 14:50:31 +0100 Subject: [PATCH] core: Refactor signing. --- core/src/apps/wallet/sign_tx/__init__.py | 12 +- core/src/apps/wallet/sign_tx/decred.py | 219 +++- core/src/apps/wallet/sign_tx/signing.py | 1230 ++++++++++------------ 3 files changed, 754 insertions(+), 707 deletions(-) diff --git a/core/src/apps/wallet/sign_tx/__init__.py b/core/src/apps/wallet/sign_tx/__init__.py index feb7cfbcf8..9b8797e67c 100644 --- a/core/src/apps/wallet/sign_tx/__init__.py +++ b/core/src/apps/wallet/sign_tx/__init__.py @@ -3,7 +3,7 @@ from trezor.messages.RequestType import TXFINISHED from trezor.messages.TxAck import TxAck from trezor.messages.TxRequest import TxRequest -from apps.common import paths +from apps.common import coins, paths from apps.wallet.sign_tx import ( addresses, helpers, @@ -15,9 +15,17 @@ from apps.wallet.sign_tx import ( signing, ) +if not utils.BITCOIN_ONLY: + from apps.wallet.sign_tx import decred + async def sign_tx(ctx, msg, keychain): - signer = signing.sign_tx(msg, keychain) + coin_name = msg.coin_name if msg.coin_name is not None else "Bitcoin" + coin = coins.by_name(coin_name) + if not utils.BITCOIN_ONLY and coin.decred: + signer = decred.Decred().signer(msg, keychain, coin) + else: + signer = signing.Bitcoin().signer(msg, keychain, coin) res = None while True: diff --git a/core/src/apps/wallet/sign_tx/decred.py b/core/src/apps/wallet/sign_tx/decred.py index e9ff7beb68..2aba52d277 100644 --- a/core/src/apps/wallet/sign_tx/decred.py +++ b/core/src/apps/wallet/sign_tx/decred.py @@ -1,16 +1,23 @@ +import gc from micropython import const from trezor.crypto.hashlib import blake256 +from trezor.messages import FailureType, InputScriptType from trezor.messages.SignTx import SignTx from trezor.messages.TxInputType import TxInputType from trezor.messages.TxOutputBinType import TxOutputBinType +from trezor.messages.TxOutputType import TxOutputType +from trezor.messages.TxRequestSerializedType import TxRequestSerializedType from trezor.utils import HashWriter -from apps.wallet.sign_tx.writers import ( - write_tx_input_decred, - write_tx_output, - write_uint32, - write_varint, +from apps.common import coininfo, seed +from apps.wallet.sign_tx import addresses, helpers, multisig, progress, scripts, writers +from apps.wallet.sign_tx.signing import ( + Bitcoin, + SigningError, + ecdsa_sign, + input_check_multisig_fingerprint, + input_check_wallet_path, ) DECRED_SERIALIZE_FULL = const(0 << 16) @@ -24,42 +31,202 @@ class DecredPrefixHasher: """ While Decred does not have the exact same implementation as bip143/zip143, the semantics for using the prefix hash of transactions are close enough - that a pseudo-bip143 class can be used to store the prefix hash during the - check_fee stage of transaction signature to then reuse it at the sign_tx - stage without having to request the inputs again. + that a pseudo-bip143 class can be used. """ def __init__(self, tx: SignTx): self.h_prefix = HashWriter(blake256()) - self.last_output_bytes = None - write_uint32(self.h_prefix, tx.version | DECRED_SERIALIZE_NO_WITNESS) - write_varint(self.h_prefix, tx.inputs_count) + writers.write_uint32(self.h_prefix, tx.version | DECRED_SERIALIZE_NO_WITNESS) + writers.write_varint(self.h_prefix, tx.inputs_count) def add_prevouts(self, txi: TxInputType): - write_tx_input_decred(self.h_prefix, txi) + writers.write_tx_input_decred(self.h_prefix, txi) def add_sequence(self, txi: TxInputType): pass def add_output_count(self, tx: SignTx): - write_varint(self.h_prefix, tx.outputs_count) + writers.write_varint(self.h_prefix, tx.outputs_count) def add_output(self, txo_bin: TxOutputBinType): - write_tx_output(self.h_prefix, txo_bin) - - def set_last_output_bytes(self, w_txo_bin: bytearray): - """ - This is required because the last serialized output obtained in - `check_fee` will only be sent to the client in `sign_tx` - """ - self.last_output_bytes = w_txo_bin - - def get_last_output_bytes(self): - return self.last_output_bytes + writers.write_tx_output(self.h_prefix, txo_bin) def add_locktime_expiry(self, tx: SignTx): - write_uint32(self.h_prefix, tx.lock_time) - write_uint32(self.h_prefix, tx.expiry) + writers.write_uint32(self.h_prefix, tx.lock_time) + writers.write_uint32(self.h_prefix, tx.expiry) def prefix_hash(self) -> bytes: return self.h_prefix.get_digest() + + +class Decred(Bitcoin): + def initialize(self, tx: SignTx, keychain: seed.Keychain, coin: coininfo.CoinInfo): + super().initialize(tx, keychain, coin) + + # This is required because the last serialized output obtained in + # `check_fee` will only be sent to the client in `sign_tx` + self.last_output_bytes = None # type: bytearray + + def init_hash143(self): + self.hash143 = DecredPrefixHasher(self.tx) # pseudo BIP-0143 prefix hashing + + async def phase1(self): + await super().phase1() + self.hash143.add_locktime_expiry(self.tx) + + async def phase1_process_input(self, i: int, txi: TxInputType): + await super().phase1_process_input(i, txi) + w_txi = writers.empty_bytearray(8 if i == 0 else 0 + 9 + len(txi.prev_hash)) + if i == 0: # serializing first input => prepend headers + self.write_tx_header(w_txi) + writers.write_tx_input_decred(w_txi, txi) + self.tx_req.serialized = TxRequestSerializedType(None, None, w_txi) + + async def phase1_confirm_output( + self, i: int, txo: TxOutputType, txo_bin: TxOutputBinType + ): + if txo.decred_script_version is not None and txo.decred_script_version != 0: + raise SigningError( + FailureType.ActionCancelled, + "Cannot send to output with script version != 0", + ) + txo_bin.decred_script_version = txo.decred_script_version + + w_txo_bin = writers.empty_bytearray(4 + 8 + 2 + 4 + len(txo_bin.script_pubkey)) + if i == 0: # serializing first output => prepend outputs count + writers.write_varint(w_txo_bin, self.tx.outputs_count) + self.hash143.add_output_count(self.tx) + + writers.write_tx_output(w_txo_bin, txo_bin) + self.tx_req.serialized = TxRequestSerializedType(serialized_tx=w_txo_bin) + self.last_output_bytes = w_txo_bin + + await super().phase1_confirm_output(i, txo, txo_bin) + + async def phase2(self): + self.tx_req.serialized = None + + prefix_hash = self.hash143.prefix_hash() + + for i_sign in range(self.tx.inputs_count): + progress.advance() + + txi_sign = await helpers.request_tx_input(self.tx_req, i_sign, self.coin) + + input_check_wallet_path(txi_sign, self.wallet_path) + input_check_multisig_fingerprint(txi_sign, self.multisig_fp) + + key_sign = self.keychain.derive(txi_sign.address_n, self.coin.curve_name) + key_sign_pub = key_sign.public_key() + + if txi_sign.script_type == InputScriptType.SPENDMULTISIG: + prev_pkscript = scripts.output_script_multisig( + multisig.multisig_get_pubkeys(txi_sign.multisig), + txi_sign.multisig.m, + ) + elif txi_sign.script_type == InputScriptType.SPENDADDRESS: + prev_pkscript = scripts.output_script_p2pkh( + addresses.ecdsa_hash_pubkey(key_sign_pub, self.coin) + ) + else: + raise SigningError("Unsupported input script type") + + h_witness = HashWriter(blake256()) + writers.write_uint32( + h_witness, self.tx.version | DECRED_SERIALIZE_WITNESS_SIGNING + ) + writers.write_varint(h_witness, self.tx.inputs_count) + + for ii in range(self.tx.inputs_count): + if ii == i_sign: + writers.write_bytes_prefixed(h_witness, prev_pkscript) + else: + writers.write_varint(h_witness, 0) + + witness_hash = writers.get_tx_hash( + h_witness, double=self.coin.sign_hash_double, reverse=False + ) + + h_sign = HashWriter(blake256()) + writers.write_uint32(h_sign, DECRED_SIGHASHALL) + writers.write_bytes_fixed(h_sign, prefix_hash, writers.TX_HASH_SIZE) + writers.write_bytes_fixed(h_sign, witness_hash, writers.TX_HASH_SIZE) + + sig_hash = writers.get_tx_hash(h_sign, double=self.coin.sign_hash_double) + signature = ecdsa_sign(key_sign, sig_hash) + + # serialize input with correct signature + gc.collect() + txi_sign.script_sig = self.input_derive_script( + txi_sign, key_sign_pub, signature + ) + w_txi_sign = writers.empty_bytearray( + 8 + 4 + len(self.last_output_bytes) + if i_sign == 0 + else 0 + 16 + 4 + len(txi_sign.script_sig) + ) + + if i_sign == 0: + writers.write_bytes_unchecked(w_txi_sign, self.last_output_bytes) + writers.write_uint32(w_txi_sign, self.tx.lock_time) + writers.write_uint32(w_txi_sign, self.tx.expiry) + writers.write_varint(w_txi_sign, self.tx.inputs_count) + + writers.write_tx_input_decred_witness(w_txi_sign, txi_sign) + self.tx_req.serialized = TxRequestSerializedType( + i_sign, signature, w_txi_sign + ) + + return await helpers.request_tx_finish(self.tx_req) + + async def get_prevtx_output_value(self, prev_hash: bytes, prev_index: int) -> int: + total_out = 0 # sum of output amounts + + # STAGE_REQUEST_2_PREV_META + tx = await helpers.request_tx_meta(self.tx_req, self.coin, prev_hash) + + if tx.outputs_cnt <= prev_index: + raise SigningError( + FailureType.ProcessError, "Not enough outputs in previous transaction." + ) + + txh = HashWriter(blake256()) + writers.write_uint32(txh, tx.version | DECRED_SERIALIZE_NO_WITNESS) + writers.write_varint(txh, tx.inputs_cnt) + + for i in range(tx.inputs_cnt): + # STAGE_REQUEST_2_PREV_INPUT + txi = await helpers.request_tx_input(self.tx_req, i, self.coin, prev_hash) + writers.write_tx_input_decred(txh, txi) + + writers.write_varint(txh, tx.outputs_cnt) + + for o in range(tx.outputs_cnt): + # STAGE_REQUEST_2_PREV_OUTPUT + txo_bin = await helpers.request_tx_output( + self.tx_req, o, self.coin, prev_hash + ) + writers.write_tx_output(txh, txo_bin) + if o == prev_index: + total_out += txo_bin.amount + if ( + txo_bin.decred_script_version is not None + and txo_bin.decred_script_version != 0 + ): + raise SigningError( + FailureType.ProcessError, + "Cannot use utxo that has script_version != 0", + ) + + writers.write_uint32(txh, tx.lock_time) + writers.write_uint32(txh, tx.expiry) + + if ( + writers.get_tx_hash(txh, double=self.coin.sign_hash_double, reverse=True) + != prev_hash + ): + raise SigningError( + FailureType.ProcessError, "Encountered invalid prev_hash" + ) + + return total_out diff --git a/core/src/apps/wallet/sign_tx/signing.py b/core/src/apps/wallet/sign_tx/signing.py index 33232b981f..93aeea6c74 100644 --- a/core/src/apps/wallet/sign_tx/signing.py +++ b/core/src/apps/wallet/sign_tx/signing.py @@ -4,7 +4,7 @@ from micropython import const from trezor import utils from trezor.crypto import base58, bip32, cashaddr, der from trezor.crypto.curve import secp256k1 -from trezor.crypto.hashlib import blake256, sha256 +from trezor.crypto.hashlib import sha256 from trezor.messages import FailureType, InputScriptType, OutputScriptType from trezor.messages.SignTx import SignTx from trezor.messages.TxInputType import TxInputType @@ -14,7 +14,7 @@ from trezor.messages.TxRequest import TxRequest from trezor.messages.TxRequestDetailsType import TxRequestDetailsType from trezor.messages.TxRequestSerializedType import TxRequestSerializedType -from apps.common import address_type, coininfo, coins, seed +from apps.common import address_type, coininfo, seed from apps.wallet.sign_tx import ( addresses, helpers, @@ -27,7 +27,7 @@ from apps.wallet.sign_tx import ( ) if not utils.BITCOIN_ONLY: - from apps.wallet.sign_tx import decred, zcash + from apps.wallet.sign_tx import zcash # the number of bip32 levels used in a wallet (chain and address) _BIP32_WALLET_DEPTH = const(2) @@ -51,771 +51,643 @@ class SigningError(ValueError): # === -# Phase 1 -# - check inputs, previous transactions, and outputs -# - ask for confirmations -# - check fee -async def check_tx_fee(tx: SignTx, keychain: seed.Keychain, coin: coininfo.CoinInfo): - # 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 - h_first = utils.HashWriter(sha256()) # not a real tx hash +class Bitcoin: + async def signer( + self, tx: SignTx, keychain: seed.Keychain, coin: coininfo.CoinInfo + ): + self.initialize(tx, keychain, coin) - if not utils.BITCOIN_ONLY and coin.decred: - hash143 = decred.DecredPrefixHasher(tx) # pseudo BIP-0143 prefix hashing - tx_ser = TxRequestSerializedType() - elif not utils.BITCOIN_ONLY and coin.overwintered: - if tx.version == 3: - branch_id = tx.branch_id or 0x5BA81B19 # Overwinter - hash143 = zcash.Zip143(branch_id) # ZIP-0143 transaction hashing - elif tx.version == 4: - branch_id = tx.branch_id or 0x76B809BB # Sapling - hash143 = zcash.Zip243(branch_id) # ZIP-0243 transaction hashing + progress.init(self.tx.inputs_count, self.tx.outputs_count) + + # Phase 1 + # - check inputs, previous transactions, and outputs + # - ask for confirmations + # - check fee + await self.phase1() + + # Phase 2 + # - sign inputs + # - check that nothing changed + await self.phase2() + + def initialize(self, tx: SignTx, keychain: seed.Keychain, coin: coininfo.CoinInfo): + self.coin = coin + self.tx = helpers.sanitize_sign_tx(tx, self.coin) + self.keychain = keychain + + self.multisig_fp = ( + multisig.MultisigFingerprint() + ) # control checksum of multisig inputs + self.wallet_path = [] # common prefix of input paths + self.bip143_in = 0 # sum of segwit input amounts + self.segwit = {} # dict of booleans stating if input is segwit + self.total_in = 0 # sum of input amounts + self.total_out = 0 # sum of output amounts + self.change_out = 0 # change output amount + + self.tx_req = TxRequest() + self.tx_req.details = TxRequestDetailsType() + + # h_first is used to make sure the inputs and outputs streamed in Phase 1 + # are the same as in Phase 2 when signing legacy inputs. it is thus not required to fully hash the + # tx, as the SignTx info is streamed only once + self.h_first = utils.HashWriter(sha256()) # not a real tx hash + + self.init_hash143() + + def init_hash143(self): + if not utils.BITCOIN_ONLY and self.coin.overwintered: + if self.tx.version == 3: + branch_id = self.tx.branch_id or 0x5BA81B19 # Overwinter + self.hash143 = zcash.Zip143(branch_id) # ZIP-0143 transaction hashing + elif self.tx.version == 4: + branch_id = self.tx.branch_id or 0x76B809BB # Sapling + self.hash143 = zcash.Zip243(branch_id) # ZIP-0243 transaction hashing + else: + raise SigningError( + FailureType.DataError, + "Unsupported version for overwintered transaction", + ) else: - raise SigningError( - FailureType.DataError, - "Unsupported version for overwintered transaction", - ) - else: - hash143 = segwit_bip143.Bip143() # BIP-0143 transaction hashing + self.hash143 = segwit_bip143.Bip143() # BIP-0143 transaction hashing - multisig_fp = multisig.MultisigFingerprint() # control checksum of multisig inputs - weight = tx_weight.TxWeightCalculator(tx.inputs_count, tx.outputs_count) + async def phase1(self): + weight = tx_weight.TxWeightCalculator( + self.tx.inputs_count, self.tx.outputs_count + ) - total_in = 0 # sum of input amounts - bip143_in = 0 # sum of segwit 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 + # compute sum of input amounts (total_in) + # add inputs to hash143 and h_first + for i in range(self.tx.inputs_count): + # STAGE_REQUEST_1_INPUT + progress.advance() + txi = await helpers.request_tx_input(self.tx_req, i, self.coin) + weight.add_input(txi) + await self.phase1_process_input(i, txi) - # output structures - txo_bin = TxOutputBinType() - tx_req = TxRequest() - tx_req.details = TxRequestDetailsType() + txo_bin = TxOutputBinType() + for i in range(self.tx.outputs_count): + # STAGE_REQUEST_3_OUTPUT + txo = await helpers.request_tx_output(self.tx_req, i, self.coin) + txo_bin.amount = txo.amount + txo_bin.script_pubkey = self.output_derive_script(txo) + weight.add_output(txo_bin.script_pubkey) + await self.phase1_confirm_output(i, txo, txo_bin) - for i in range(tx.inputs_count): - progress.advance() - # STAGE_REQUEST_1_INPUT - txi = await helpers.request_tx_input(tx_req, i, coin) - wallet_path = input_extract_wallet_path(txi, wallet_path) - writers.write_tx_input_check(h_first, txi) - weight.add_input(txi) - hash143.add_prevouts(txi) # all inputs are included (non-segwit as well) - hash143.add_sequence(txi) + fee = self.total_in - self.total_out - if not addresses.validate_full_path(txi.address_n, coin, txi.script_type): + if not utils.BITCOIN_ONLY and self.coin.negative_fee: + pass # bypass check for negative fee coins, required for reward TX + else: + if fee < 0: + raise SigningError(FailureType.NotEnoughFunds, "Not enough funds") + + # fee > (coin.maxfee per byte * tx size) + if fee > (self.coin.maxfee_kb / 1000) * (weight.get_total() / 4): + if not await helpers.confirm_feeoverthreshold(fee, self.coin): + raise SigningError(FailureType.ActionCancelled, "Signing cancelled") + + if self.tx.lock_time > 0: + if not await helpers.confirm_nondefault_locktime(self.tx.lock_time): + raise SigningError(FailureType.ActionCancelled, "Locktime cancelled") + + if not await helpers.confirm_total( + self.total_in - self.change_out, fee, self.coin + ): + raise SigningError(FailureType.ActionCancelled, "Total cancelled") + + async def phase1_process_input(self, i: int, txi: TxInputType): + self.wallet_path = input_extract_wallet_path(txi, self.wallet_path) + writers.write_tx_input_check(self.h_first, txi) + self.hash143.add_prevouts(txi) # all inputs are included (non-segwit as well) + self.hash143.add_sequence(txi) + + if not addresses.validate_full_path(txi.address_n, self.coin, txi.script_type): await helpers.confirm_foreign_address(txi.address_n) if txi.multisig: - multisig_fp.add(txi.multisig) + self.multisig_fp.add(txi.multisig) else: - multisig_fp.mismatch = True + self.multisig_fp.mismatch = True if txi.script_type in ( InputScriptType.SPENDWITNESS, InputScriptType.SPENDP2SHWITNESS, ): - if not coin.segwit: + if not self.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 - bip143_in += txi.amount - total_in += txi.amount - + self.segwit[i] = True + self.bip143_in += txi.amount + self.total_in += txi.amount elif txi.script_type in ( InputScriptType.SPENDADDRESS, InputScriptType.SPENDMULTISIG, ): - if not utils.BITCOIN_ONLY and (coin.force_bip143 or coin.overwintered): + if not utils.BITCOIN_ONLY and ( + self.coin.force_bip143 or self.coin.overwintered + ): if not txi.amount: raise SigningError( FailureType.DataError, "Expected input with amount" ) - segwit[i] = False - bip143_in += txi.amount - total_in += txi.amount + self.segwit[i] = False + self.bip143_in += txi.amount + self.total_in += txi.amount else: - segwit[i] = False - total_in += await get_prevtx_output_value( - coin, tx_req, txi.prev_hash, txi.prev_index + self.segwit[i] = False + self.total_in += await self.get_prevtx_output_value( + txi.prev_hash, txi.prev_index ) - else: raise SigningError(FailureType.DataError, "Wrong input script type") - if not utils.BITCOIN_ONLY and coin.decred: - w_txi = writers.empty_bytearray(8 if i == 0 else 0 + 9 + len(txi.prev_hash)) - if i == 0: # serializing first input => prepend headers - # decred doesn't support segwit - write_tx_header(w_txi, coin, tx, False) - writers.write_tx_input_decred(w_txi, txi) - tx_ser.serialized_tx = w_txi - tx_req.serialized = tx_ser - - if not utils.BITCOIN_ONLY and coin.decred: - hash143.add_output_count(tx) - - for o in range(tx.outputs_count): - # STAGE_REQUEST_3_OUTPUT - txo = await helpers.request_tx_output(tx_req, o, coin) - txo_bin.amount = txo.amount - txo_bin.script_pubkey = output_derive_script(txo, coin, keychain) - weight.add_output(txo_bin.script_pubkey) - - if change_out == 0 and output_is_change(txo, wallet_path, multisig_fp): + async def phase1_confirm_output( + self, i: int, txo: TxOutputType, txo_bin: TxOutputBinType + ): + if self.change_out == 0 and self.output_is_change(txo): # output is change and does not need confirmation - change_out = txo.amount - elif not await helpers.confirm_output(txo, coin): + self.change_out = txo.amount + elif not await helpers.confirm_output(txo, self.coin): raise SigningError(FailureType.ActionCancelled, "Output cancelled") - if not utils.BITCOIN_ONLY and coin.decred: - if txo.decred_script_version is not None and txo.decred_script_version != 0: - raise SigningError( - FailureType.ActionCancelled, - "Cannot send to output with script version != 0", - ) - txo_bin.decred_script_version = txo.decred_script_version + writers.write_tx_output(self.h_first, txo_bin) + self.hash143.add_output(txo_bin) + self.total_out += txo_bin.amount - w_txo_bin = writers.empty_bytearray( - 4 + 8 + 2 + 4 + len(txo_bin.script_pubkey) - ) - if o == 0: # serializing first output => prepend outputs count - writers.write_varint(w_txo_bin, tx.outputs_count) - writers.write_tx_output(w_txo_bin, txo_bin) - tx_ser.serialized_tx = w_txo_bin - tx_req.serialized = tx_ser - hash143.set_last_output_bytes(w_txo_bin) + async def phase2(self): + self.tx_req.serialized = None - writers.write_tx_output(h_first, txo_bin) - hash143.add_output(txo_bin) - total_out += txo_bin.amount - - fee = total_in - total_out - - if not utils.BITCOIN_ONLY and coin.negative_fee: - pass # bypass check for negative fee coins, required for reward TX - else: - if fee < 0: - raise SigningError(FailureType.NotEnoughFunds, "Not enough funds") - - # fee > (coin.maxfee per byte * tx size) - if fee > (coin.maxfee_kb / 1000) * (weight.get_total() / 4): - if not await helpers.confirm_feeoverthreshold(fee, coin): - raise SigningError(FailureType.ActionCancelled, "Signing cancelled") - - if tx.lock_time > 0: - if not await helpers.confirm_nondefault_locktime(tx.lock_time): - raise SigningError(FailureType.ActionCancelled, "Locktime cancelled") - - if not await helpers.confirm_total(total_in - change_out, fee, coin): - raise SigningError(FailureType.ActionCancelled, "Total cancelled") - - if not utils.BITCOIN_ONLY and coin.decred: - hash143.add_locktime_expiry(tx) - - return h_first, hash143, segwit, bip143_in, wallet_path, multisig_fp - - -async def sign_tx(tx: SignTx, keychain: seed.Keychain): - coin_name = tx.coin_name if tx.coin_name is not None else "Bitcoin" - coin = coins.by_name(coin_name) - tx = helpers.sanitize_sign_tx(tx, coin) - - progress.init(tx.inputs_count, tx.outputs_count) - - # Phase 1 - - ( - h_first, - hash143, - segwit, - authorized_bip143_in, - wallet_path, - multisig_fp, - ) = await check_tx_fee(tx, keychain, coin) - - # Phase 2 - # - sign inputs - # - check that nothing changed - - any_segwit = True in segwit.values() - tx_ser = TxRequestSerializedType() - - txo_bin = TxOutputBinType() - tx_req = TxRequest() - tx_req.details = TxRequestDetailsType() - tx_req.serialized = None - - if not utils.BITCOIN_ONLY and coin.decred: - prefix_hash = hash143.prefix_hash() - - for i_sign in range(tx.inputs_count): - progress.advance() - txi_sign = None - key_sign = None - key_sign_pub = None - - if segwit[i_sign]: - # STAGE_REQUEST_SEGWIT_INPUT - txi_sign = await helpers.request_tx_input(tx_req, i_sign, coin) - - if not input_is_segwit(txi_sign): - raise SigningError( - FailureType.ProcessError, "Transaction has changed during signing" - ) - input_check_wallet_path(txi_sign, wallet_path) - # NOTE: No need to check the multisig fingerprint, because we won't be signing - # the script here. Signatures are produced in STAGE_REQUEST_SEGWIT_WITNESS. - - key_sign = keychain.derive(txi_sign.address_n, coin.curve_name) - key_sign_pub = key_sign.public_key() - txi_sign.script_sig = input_derive_script(coin, txi_sign, key_sign_pub) - - w_txi = writers.empty_bytearray( - 7 + len(txi_sign.prev_hash) + 4 + len(txi_sign.script_sig) + 4 - ) - if i_sign == 0: # serializing first input => prepend headers - write_tx_header(w_txi, coin, tx, True) - writers.write_tx_input(w_txi, txi_sign) - tx_ser.serialized_tx = w_txi - tx_ser.signature_index = None - tx_ser.signature = None - tx_req.serialized = tx_ser - - elif not utils.BITCOIN_ONLY and (coin.force_bip143 or coin.overwintered): - # STAGE_REQUEST_SEGWIT_INPUT - txi_sign = await helpers.request_tx_input(tx_req, i_sign, coin) - input_check_wallet_path(txi_sign, wallet_path) - input_check_multisig_fingerprint(txi_sign, multisig_fp) - - is_bip143 = ( - txi_sign.script_type == InputScriptType.SPENDADDRESS - or txi_sign.script_type == InputScriptType.SPENDMULTISIG - ) - if not is_bip143 or txi_sign.amount > authorized_bip143_in: - raise SigningError( - FailureType.ProcessError, "Transaction has changed during signing" - ) - authorized_bip143_in -= txi_sign.amount - - key_sign = keychain.derive(txi_sign.address_n, coin.curve_name) - key_sign_pub = key_sign.public_key() - hash143_hash = hash143.preimage_hash( - coin, - tx, - txi_sign, - addresses.ecdsa_hash_pubkey(key_sign_pub, coin), - get_hash_type(coin), - ) - - # if multisig, check if signing with a key that is included in multisig - if txi_sign.multisig: - multisig.multisig_pubkey_index(txi_sign.multisig, key_sign_pub) - - signature = ecdsa_sign(key_sign, hash143_hash) - tx_ser.signature_index = i_sign - tx_ser.signature = signature - - # serialize input with correct signature - gc.collect() - txi_sign.script_sig = input_derive_script( - coin, txi_sign, key_sign_pub, signature - ) - w_txi_sign = writers.empty_bytearray( - 5 + len(txi_sign.prev_hash) + 4 + len(txi_sign.script_sig) + 4 - ) - if i_sign == 0: # serializing first input => prepend headers - write_tx_header(w_txi_sign, coin, tx, any_segwit) - writers.write_tx_input(w_txi_sign, txi_sign) - tx_ser.serialized_tx = w_txi_sign - - tx_req.serialized = tx_ser - - elif not utils.BITCOIN_ONLY and coin.decred: - txi_sign = await helpers.request_tx_input(tx_req, i_sign, coin) - - input_check_wallet_path(txi_sign, wallet_path) - input_check_multisig_fingerprint(txi_sign, multisig_fp) - - key_sign = keychain.derive(txi_sign.address_n, coin.curve_name) - key_sign_pub = key_sign.public_key() - - if txi_sign.script_type == InputScriptType.SPENDMULTISIG: - prev_pkscript = scripts.output_script_multisig( - multisig.multisig_get_pubkeys(txi_sign.multisig), - txi_sign.multisig.m, - ) - elif txi_sign.script_type == InputScriptType.SPENDADDRESS: - prev_pkscript = scripts.output_script_p2pkh( - addresses.ecdsa_hash_pubkey(key_sign_pub, coin) - ) + # Serialize inputs and sign non-segwit inputs. + for i in range(self.tx.inputs_count): + progress.advance() + if self.segwit[i]: + await self.phase2_serialize_segwit_input(i) + elif not utils.BITCOIN_ONLY and ( + self.coin.force_bip143 or self.coin.overwintered + ): + await self.phase2_sign_bip143_input(i) else: - raise SigningError("Unsupported input script type") + await self.phase2_sign_legacy_input(i) - h_witness = utils.HashWriter(blake256()) - writers.write_uint32( - h_witness, tx.version | decred.DECRED_SERIALIZE_WITNESS_SIGNING - ) - writers.write_varint(h_witness, tx.inputs_count) + # Serialize outputs. + tx_ser = TxRequestSerializedType() + for i in range(self.tx.outputs_count): + # STAGE_REQUEST_5_OUTPUT + progress.advance() + tx_ser.serialized_tx = await self.phase2_serialize_output(i) + self.tx_req.serialized = tx_ser - for ii in range(tx.inputs_count): - if ii == i_sign: - writers.write_bytes_prefixed(h_witness, prev_pkscript) - else: - writers.write_varint(h_witness, 0) + # Sign segwit inputs. + any_segwit = True in self.segwit.values() + for i in range(self.tx.inputs_count): + progress.advance() + if self.segwit[i]: + # STAGE_REQUEST_SEGWIT_WITNESS + witness, signature = await self.phase2_sign_segwit_input(i) + tx_ser.serialized_tx = witness + tx_ser.signature_index = i + tx_ser.signature = signature + elif any_segwit: + tx_ser.serialized_tx += bytearray( + 1 + ) # empty witness for non-segwit inputs + tx_ser.signature_index = None + tx_ser.signature = None - witness_hash = writers.get_tx_hash( - h_witness, double=coin.sign_hash_double, reverse=False - ) + self.tx_req.serialized = tx_ser - h_sign = utils.HashWriter(blake256()) - writers.write_uint32(h_sign, decred.DECRED_SIGHASHALL) - writers.write_bytes_fixed(h_sign, prefix_hash, writers.TX_HASH_SIZE) - writers.write_bytes_fixed(h_sign, witness_hash, writers.TX_HASH_SIZE) + writers.write_uint32(tx_ser.serialized_tx, self.tx.lock_time) - sig_hash = writers.get_tx_hash(h_sign, double=coin.sign_hash_double) - signature = ecdsa_sign(key_sign, sig_hash) - tx_ser.signature_index = i_sign - tx_ser.signature = signature - - # serialize input with correct signature - gc.collect() - txi_sign.script_sig = input_derive_script( - coin, txi_sign, key_sign_pub, signature - ) - w_txi_sign = writers.empty_bytearray( - 8 + 4 + len(hash143.get_last_output_bytes()) - if i_sign == 0 - else 0 + 16 + 4 + len(txi_sign.script_sig) - ) - - if i_sign == 0: - writers.write_bytes_unchecked( - w_txi_sign, hash143.get_last_output_bytes() - ) - writers.write_uint32(w_txi_sign, tx.lock_time) - writers.write_uint32(w_txi_sign, tx.expiry) - writers.write_varint(w_txi_sign, tx.inputs_count) - - writers.write_tx_input_decred_witness(w_txi_sign, txi_sign) - tx_ser.serialized_tx = w_txi_sign - tx_req.serialized = tx_ser - - else: - # hash of what we are signing with this input - h_sign = utils.HashWriter(sha256()) - # same as h_first, checked before signing the digest - h_second = utils.HashWriter(sha256()) - - writers.write_uint32(h_sign, tx.version) # nVersion - if not utils.BITCOIN_ONLY and coin.timestamp: - writers.write_uint32(h_sign, tx.timestamp) - - writers.write_varint(h_sign, tx.inputs_count) - - for i in range(tx.inputs_count): - # STAGE_REQUEST_4_INPUT - txi = await helpers.request_tx_input(tx_req, i, coin) - input_check_wallet_path(txi, wallet_path) - writers.write_tx_input_check(h_second, txi) - if i == i_sign: - txi_sign = txi - input_check_multisig_fingerprint(txi_sign, multisig_fp) - key_sign = keychain.derive(txi.address_n, coin.curve_name) - key_sign_pub = key_sign.public_key() - # for the signing process the script_sig is equal - # to the previous tx's scriptPubKey (P2PKH) or a redeem script (P2SH) - if txi_sign.script_type == InputScriptType.SPENDMULTISIG: - txi_sign.script_sig = scripts.output_script_multisig( - multisig.multisig_get_pubkeys(txi_sign.multisig), - txi_sign.multisig.m, - ) - elif txi_sign.script_type == InputScriptType.SPENDADDRESS: - txi_sign.script_sig = scripts.output_script_p2pkh( - addresses.ecdsa_hash_pubkey(key_sign_pub, coin) - ) - else: - raise SigningError( - FailureType.ProcessError, "Unknown transaction type" - ) - else: - txi.script_sig = bytes() - writers.write_tx_input(h_sign, txi) - - writers.write_varint(h_sign, tx.outputs_count) - - for o in range(tx.outputs_count): - # STAGE_REQUEST_4_OUTPUT - txo = await helpers.request_tx_output(tx_req, o, coin) - txo_bin.amount = txo.amount - txo_bin.script_pubkey = output_derive_script(txo, coin, keychain) - writers.write_tx_output(h_second, txo_bin) - writers.write_tx_output(h_sign, txo_bin) - - writers.write_uint32(h_sign, tx.lock_time) - writers.write_uint32(h_sign, get_hash_type(coin)) - - # check the control digests - if writers.get_tx_hash(h_first, False) != writers.get_tx_hash(h_second): + if not utils.BITCOIN_ONLY and self.coin.overwintered: + if self.tx.version == 3: + writers.write_uint32( + tx_ser.serialized_tx, self.tx.expiry + ) # expiryHeight + writers.write_varint(tx_ser.serialized_tx, 0) # nJoinSplit + elif self.tx.version == 4: + writers.write_uint32( + tx_ser.serialized_tx, self.tx.expiry + ) # expiryHeight + writers.write_uint64(tx_ser.serialized_tx, 0) # valueBalance + writers.write_varint(tx_ser.serialized_tx, 0) # nShieldedSpend + writers.write_varint(tx_ser.serialized_tx, 0) # nShieldedOutput + writers.write_varint(tx_ser.serialized_tx, 0) # nJoinSplit + else: raise SigningError( - FailureType.ProcessError, "Transaction has changed during signing" + FailureType.DataError, + "Unsupported version for overwintered transaction", ) - # if multisig, check if signing with a key that is included in multisig - if txi_sign.multisig: - multisig.multisig_pubkey_index(txi_sign.multisig, key_sign_pub) + await helpers.request_tx_finish(self.tx_req) - # compute the signature from the tx digest - signature = ecdsa_sign( - key_sign, writers.get_tx_hash(h_sign, double=coin.sign_hash_double) + async def phase2_serialize_segwit_input(self, i_sign): + # STAGE_REQUEST_SEGWIT_INPUT + txi_sign = await helpers.request_tx_input(self.tx_req, i_sign, self.coin) + + if not input_is_segwit(txi_sign): + raise SigningError( + FailureType.ProcessError, "Transaction has changed during signing" ) - tx_ser.signature_index = i_sign - tx_ser.signature = signature + input_check_wallet_path(txi_sign, self.wallet_path) + # NOTE: No need to check the multisig fingerprint, because we won't be signing + # the script here. Signatures are produced in STAGE_REQUEST_SEGWIT_WITNESS. - # serialize input with correct signature - gc.collect() - txi_sign.script_sig = input_derive_script( - coin, txi_sign, key_sign_pub, signature + key_sign = self.keychain.derive(txi_sign.address_n, self.coin.curve_name) + key_sign_pub = key_sign.public_key() + txi_sign.script_sig = self.input_derive_script(txi_sign, key_sign_pub) + + w_txi = writers.empty_bytearray( + 7 + len(txi_sign.prev_hash) + 4 + len(txi_sign.script_sig) + 4 + ) + if i_sign == 0: # serializing first input => prepend headers + self.write_tx_header(w_txi) + writers.write_tx_input(w_txi, txi_sign) + self.tx_req.serialized = TxRequestSerializedType(serialized_tx=w_txi) + + async def phase2_sign_segwit_input(self, i): + txi = await helpers.request_tx_input(self.tx_req, i, self.coin) + + input_check_wallet_path(txi, self.wallet_path) + input_check_multisig_fingerprint(txi, self.multisig_fp) + + if not input_is_segwit(txi) or txi.amount > self.bip143_in: + raise SigningError( + FailureType.ProcessError, "Transaction has changed during signing" ) - w_txi_sign = writers.empty_bytearray( - 5 + len(txi_sign.prev_hash) + 4 + len(txi_sign.script_sig) + 4 + self.bip143_in -= txi.amount + + key_sign = self.keychain.derive(txi.address_n, self.coin.curve_name) + key_sign_pub = key_sign.public_key() + hash143_hash = self.hash143.preimage_hash( + self.coin, + self.tx, + txi, + addresses.ecdsa_hash_pubkey(key_sign_pub, self.coin), + self.get_hash_type(), + ) + + signature = ecdsa_sign(key_sign, hash143_hash) + if txi.multisig: + # find out place of our signature based on the pubkey + signature_index = multisig.multisig_pubkey_index(txi.multisig, key_sign_pub) + witness = scripts.witness_p2wsh( + txi.multisig, signature, signature_index, self.get_hash_type() + ) + else: + witness = scripts.witness_p2wpkh( + signature, key_sign_pub, self.get_hash_type() ) - if i_sign == 0: # serializing first input => prepend headers - write_tx_header(w_txi_sign, coin, tx, any_segwit) - writers.write_tx_input(w_txi_sign, txi_sign) - tx_ser.serialized_tx = w_txi_sign - tx_req.serialized = tx_ser + return witness, signature - if not utils.BITCOIN_ONLY and coin.decred: - return await helpers.request_tx_finish(tx_req) + async def phase2_sign_bip143_input(self, i_sign): + # STAGE_REQUEST_SEGWIT_INPUT + txi_sign = await helpers.request_tx_input(self.tx_req, i_sign, self.coin) + input_check_wallet_path(txi_sign, self.wallet_path) + input_check_multisig_fingerprint(txi_sign, self.multisig_fp) - for o in range(tx.outputs_count): - progress.advance() - # STAGE_REQUEST_5_OUTPUT - txo = await helpers.request_tx_output(tx_req, o, coin) + is_bip143 = ( + txi_sign.script_type == InputScriptType.SPENDADDRESS + or txi_sign.script_type == InputScriptType.SPENDMULTISIG + ) + if not is_bip143 or txi_sign.amount > self.bip143_in: + raise SigningError( + FailureType.ProcessError, "Transaction has changed during signing" + ) + self.bip143_in -= txi_sign.amount + + key_sign = self.keychain.derive(txi_sign.address_n, self.coin.curve_name) + key_sign_pub = key_sign.public_key() + self.hash143_hash = self.hash143.preimage_hash( + self.coin, + self.tx, + txi_sign, + addresses.ecdsa_hash_pubkey(key_sign_pub, self.coin), + self.get_hash_type(), + ) + + # if multisig, check if signing with a key that is included in multisig + if txi_sign.multisig: + multisig.multisig_pubkey_index(txi_sign.multisig, key_sign_pub) + + signature = ecdsa_sign(key_sign, self.hash143_hash) + + # serialize input with correct signature + gc.collect() + txi_sign.script_sig = self.input_derive_script( + txi_sign, key_sign_pub, signature + ) + w_txi_sign = writers.empty_bytearray( + 5 + len(txi_sign.prev_hash) + 4 + len(txi_sign.script_sig) + 4 + ) + if i_sign == 0: # serializing first input => prepend headers + self.write_tx_header(w_txi_sign) + writers.write_tx_input(w_txi_sign, txi_sign) + self.tx_req.serialized = TxRequestSerializedType(i_sign, signature, w_txi_sign) + + async def phase2_sign_legacy_input(self, i_sign): + # hash of what we are signing with this input + h_sign = utils.HashWriter(sha256()) + # same as h_first, checked before signing the digest + h_second = utils.HashWriter(sha256()) + + writers.write_uint32(h_sign, self.tx.version) # nVersion + if not utils.BITCOIN_ONLY and self.coin.timestamp: + writers.write_uint32(h_sign, self.tx.timestamp) + + writers.write_varint(h_sign, self.tx.inputs_count) + + for i in range(self.tx.inputs_count): + # STAGE_REQUEST_4_INPUT + txi = await helpers.request_tx_input(self.tx_req, i, self.coin) + input_check_wallet_path(txi, self.wallet_path) + writers.write_tx_input_check(h_second, txi) + if i == i_sign: + txi_sign = txi + input_check_multisig_fingerprint(txi_sign, self.multisig_fp) + key_sign = self.keychain.derive(txi.address_n, self.coin.curve_name) + key_sign_pub = key_sign.public_key() + # for the signing process the script_sig is equal + # to the previous tx's scriptPubKey (P2PKH) or a redeem script (P2SH) + if txi_sign.script_type == InputScriptType.SPENDMULTISIG: + txi_sign.script_sig = scripts.output_script_multisig( + multisig.multisig_get_pubkeys(txi_sign.multisig), + txi_sign.multisig.m, + ) + elif txi_sign.script_type == InputScriptType.SPENDADDRESS: + txi_sign.script_sig = scripts.output_script_p2pkh( + addresses.ecdsa_hash_pubkey(key_sign_pub, self.coin) + ) + else: + raise SigningError( + FailureType.ProcessError, "Unknown transaction type" + ) + else: + txi.script_sig = bytes() + writers.write_tx_input(h_sign, txi) + + writers.write_varint(h_sign, self.tx.outputs_count) + + txo_bin = TxOutputBinType() + for i in range(self.tx.outputs_count): + # STAGE_REQUEST_4_OUTPUT + txo = await helpers.request_tx_output(self.tx_req, i, self.coin) + txo_bin.amount = txo.amount + txo_bin.script_pubkey = self.output_derive_script(txo) + writers.write_tx_output(h_second, txo_bin) + writers.write_tx_output(h_sign, txo_bin) + + writers.write_uint32(h_sign, self.tx.lock_time) + writers.write_uint32(h_sign, self.get_hash_type()) + + # check the control digests + if writers.get_tx_hash(self.h_first, False) != writers.get_tx_hash(h_second): + raise SigningError( + FailureType.ProcessError, "Transaction has changed during signing" + ) + + # if multisig, check if signing with a key that is included in multisig + if txi_sign.multisig: + multisig.multisig_pubkey_index(txi_sign.multisig, key_sign_pub) + + # compute the signature from the tx digest + signature = ecdsa_sign( + key_sign, writers.get_tx_hash(h_sign, double=self.coin.sign_hash_double) + ) + + # serialize input wittx_reqh correct signature + gc.collect() + txi_sign.script_sig = self.input_derive_script( + txi_sign, key_sign_pub, signature + ) + w_txi_sign = writers.empty_bytearray( + 5 + len(txi_sign.prev_hash) + 4 + len(txi_sign.script_sig) + 4 + ) + if i_sign == 0: # serializing first input => prepend headers + self.write_tx_header(w_txi_sign) + writers.write_tx_input(w_txi_sign, txi_sign) + self.tx_req.serialized = TxRequestSerializedType(i_sign, signature, w_txi_sign) + + async def phase2_serialize_output(self, i: int): + txo = await helpers.request_tx_output(self.tx_req, i, self.coin) + txo_bin = TxOutputBinType() txo_bin.amount = txo.amount - txo_bin.script_pubkey = output_derive_script(txo, coin, keychain) + txo_bin.script_pubkey = self.output_derive_script(txo) # serialize output w_txo_bin = writers.empty_bytearray(5 + 8 + 5 + len(txo_bin.script_pubkey) + 4) - if o == 0: # serializing first output => prepend outputs count - writers.write_varint(w_txo_bin, tx.outputs_count) + if i == 0: # serializing first output => prepend outputs count + writers.write_varint(w_txo_bin, self.tx.outputs_count) writers.write_tx_output(w_txo_bin, txo_bin) - tx_ser.signature_index = None - tx_ser.signature = None - tx_ser.serialized_tx = w_txo_bin + return w_txo_bin - tx_req.serialized = tx_ser + async def get_prevtx_output_value(self, prev_hash: bytes, prev_index: int) -> int: + total_out = 0 # sum of output amounts - for i in range(tx.inputs_count): - progress.advance() - if segwit[i]: - # STAGE_REQUEST_SEGWIT_WITNESS - txi = await helpers.request_tx_input(tx_req, i, coin) - input_check_wallet_path(txi, wallet_path) - input_check_multisig_fingerprint(txi, multisig_fp) + # STAGE_REQUEST_2_PREV_META + tx = await helpers.request_tx_meta(self.tx_req, self.coin, prev_hash) - if not input_is_segwit(txi) or txi.amount > authorized_bip143_in: - raise SigningError( - FailureType.ProcessError, "Transaction has changed during signing" - ) - authorized_bip143_in -= txi.amount - - key_sign = keychain.derive(txi.address_n, coin.curve_name) - key_sign_pub = key_sign.public_key() - hash143_hash = hash143.preimage_hash( - coin, - tx, - txi, - addresses.ecdsa_hash_pubkey(key_sign_pub, coin), - get_hash_type(coin), - ) - - signature = ecdsa_sign(key_sign, hash143_hash) - if txi.multisig: - # find out place of our signature based on the pubkey - signature_index = multisig.multisig_pubkey_index( - txi.multisig, key_sign_pub - ) - witness = scripts.witness_p2wsh( - txi.multisig, signature, signature_index, get_hash_type(coin) - ) - else: - witness = scripts.witness_p2wpkh( - signature, key_sign_pub, get_hash_type(coin) - ) - - tx_ser.serialized_tx = witness - tx_ser.signature_index = i - tx_ser.signature = signature - 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 - - writers.write_uint32(tx_ser.serialized_tx, tx.lock_time) - - if not utils.BITCOIN_ONLY and coin.overwintered: - if tx.version == 3: - writers.write_uint32(tx_ser.serialized_tx, tx.expiry) # expiryHeight - writers.write_varint(tx_ser.serialized_tx, 0) # nJoinSplit - elif tx.version == 4: - writers.write_uint32(tx_ser.serialized_tx, tx.expiry) # expiryHeight - writers.write_uint64(tx_ser.serialized_tx, 0) # valueBalance - writers.write_varint(tx_ser.serialized_tx, 0) # nShieldedSpend - writers.write_varint(tx_ser.serialized_tx, 0) # nShieldedOutput - writers.write_varint(tx_ser.serialized_tx, 0) # nJoinSplit - else: + if tx.outputs_cnt <= prev_index: raise SigningError( - FailureType.DataError, - "Unsupported version for overwintered transaction", + FailureType.ProcessError, "Not enough outputs in previous transaction." ) - await helpers.request_tx_finish(tx_req) - - -async def get_prevtx_output_value( - coin: coininfo.CoinInfo, tx_req: TxRequest, prev_hash: bytes, prev_index: int -) -> int: - total_out = 0 # sum of output amounts - - # STAGE_REQUEST_2_PREV_META - tx = await helpers.request_tx_meta(tx_req, coin, prev_hash) - - if tx.outputs_cnt <= prev_index: - raise SigningError( - FailureType.ProcessError, "Not enough outputs in previous transaction." - ) - - if not utils.BITCOIN_ONLY and coin.decred: - txh = utils.HashWriter(blake256()) - else: txh = utils.HashWriter(sha256()) - if not utils.BITCOIN_ONLY and coin.overwintered: - writers.write_uint32( - txh, tx.version | zcash.OVERWINTERED - ) # nVersion | fOverwintered - writers.write_uint32(txh, tx.version_group_id) # nVersionGroupId - elif not utils.BITCOIN_ONLY and coin.decred: - writers.write_uint32(txh, tx.version | decred.DECRED_SERIALIZE_NO_WITNESS) - else: - writers.write_uint32(txh, tx.version) # nVersion - if not utils.BITCOIN_ONLY and coin.timestamp: - writers.write_uint32(txh, tx.timestamp) - - writers.write_varint(txh, tx.inputs_cnt) - - for i in range(tx.inputs_cnt): - # STAGE_REQUEST_2_PREV_INPUT - txi = await helpers.request_tx_input(tx_req, i, coin, prev_hash) - if not utils.BITCOIN_ONLY and coin.decred: - writers.write_tx_input_decred(txh, txi) + if not utils.BITCOIN_ONLY and self.coin.overwintered: + writers.write_uint32( + txh, tx.version | zcash.OVERWINTERED + ) # nVersion | fOverwintered + writers.write_uint32(txh, tx.version_group_id) # nVersionGroupId else: + writers.write_uint32(txh, tx.version) # nVersion + if not utils.BITCOIN_ONLY and self.coin.timestamp: + writers.write_uint32(txh, tx.timestamp) + + writers.write_varint(txh, tx.inputs_cnt) + + for i in range(tx.inputs_cnt): + # STAGE_REQUEST_2_PREV_INPUT + txi = await helpers.request_tx_input(self.tx_req, i, self.coin, prev_hash) writers.write_tx_input(txh, txi) - writers.write_varint(txh, tx.outputs_cnt) + writers.write_varint(txh, tx.outputs_cnt) - for o in range(tx.outputs_cnt): - # STAGE_REQUEST_2_PREV_OUTPUT - txo_bin = await helpers.request_tx_output(tx_req, o, coin, prev_hash) - writers.write_tx_output(txh, txo_bin) - if o == prev_index: - total_out += txo_bin.amount - if ( - not utils.BITCOIN_ONLY - and coin.decred - and txo_bin.decred_script_version is not None - and txo_bin.decred_script_version != 0 - ): - raise SigningError( - FailureType.ProcessError, - "Cannot use utxo that has script_version != 0", + for o in range(tx.outputs_cnt): + # STAGE_REQUEST_2_PREV_OUTPUT + txo_bin = await helpers.request_tx_output( + self.tx_req, o, self.coin, prev_hash + ) + writers.write_tx_output(txh, txo_bin) + if o == prev_index: + total_out += txo_bin.amount + + writers.write_uint32(txh, tx.lock_time) + + if not utils.BITCOIN_ONLY and self.coin.extra_data: + ofs = 0 + while ofs < tx.extra_data_len: + size = min(1024, tx.extra_data_len - ofs) + data = await helpers.request_tx_extra_data( + self.tx_req, ofs, size, prev_hash ) + writers.write_bytes_unchecked(txh, data) + ofs += len(data) - writers.write_uint32(txh, tx.lock_time) + if ( + writers.get_tx_hash(txh, double=self.coin.sign_hash_double, reverse=True) + != prev_hash + ): + raise SigningError( + FailureType.ProcessError, "Encountered invalid prev_hash" + ) - if not utils.BITCOIN_ONLY and (coin.overwintered or coin.decred): - writers.write_uint32(txh, tx.expiry) + return total_out - if not utils.BITCOIN_ONLY and coin.extra_data: - ofs = 0 - while ofs < tx.extra_data_len: - size = min(1024, tx.extra_data_len - ofs) - data = await helpers.request_tx_extra_data(tx_req, ofs, size, prev_hash) - writers.write_bytes_unchecked(txh, data) - ofs += len(data) + # TX Helpers + # === - if ( - writers.get_tx_hash(txh, double=coin.sign_hash_double, reverse=True) - != prev_hash - ): - raise SigningError(FailureType.ProcessError, "Encountered invalid prev_hash") + def get_hash_type(self) -> int: + SIGHASH_FORKID = const(0x40) + SIGHASH_ALL = const(0x01) + hashtype = SIGHASH_ALL + if self.coin.fork_id is not None: + hashtype |= (self.coin.fork_id << 8) | SIGHASH_FORKID + return hashtype - return total_out - - -# TX Helpers -# === - - -def get_hash_type(coin: coininfo.CoinInfo) -> int: - SIGHASH_FORKID = const(0x40) - SIGHASH_ALL = const(0x01) - hashtype = SIGHASH_ALL - if coin.fork_id is not None: - hashtype |= (coin.fork_id << 8) | SIGHASH_FORKID - return hashtype - - -def write_tx_header( - w: writers.Writer, coin: coininfo.CoinInfo, tx: SignTx, segwit: bool -) -> None: - if not utils.BITCOIN_ONLY and coin.overwintered: - # nVersion | fOverwintered - writers.write_uint32(w, tx.version | zcash.OVERWINTERED) - writers.write_uint32(w, tx.version_group_id) # nVersionGroupId - else: - writers.write_uint32(w, tx.version) # nVersion - if not utils.BITCOIN_ONLY and coin.timestamp: - writers.write_uint32(w, tx.timestamp) - if segwit: - writers.write_varint(w, 0x00) # segwit witness marker - writers.write_varint(w, 0x01) # segwit witness flag - writers.write_varint(w, tx.inputs_count) - - -# TX Outputs -# === - - -def output_derive_script( - o: TxOutputType, coin: coininfo.CoinInfo, keychain: seed.Keychain -) -> bytes: - - if o.script_type == OutputScriptType.PAYTOOPRETURN: - return scripts.output_script_paytoopreturn(o.op_return_data) - - if o.address_n: - # change output - o.address = get_address_for_change(o, coin, keychain) - - if coin.bech32_prefix and o.address.startswith(coin.bech32_prefix): - # p2wpkh or p2wsh - witprog = addresses.decode_bech32_address(coin.bech32_prefix, o.address) - return scripts.output_script_native_p2wpkh_or_p2wsh(witprog) - - if ( - not utils.BITCOIN_ONLY - and coin.cashaddr_prefix is not None - and o.address.startswith(coin.cashaddr_prefix + ":") - ): - prefix, addr = o.address.split(":") - version, data = cashaddr.decode(prefix, addr) - if version == cashaddr.ADDRESS_TYPE_P2KH: - version = coin.address_type - elif version == cashaddr.ADDRESS_TYPE_P2SH: - version = coin.address_type_p2sh + def write_tx_header(self, w: writers.Writer) -> None: + if not utils.BITCOIN_ONLY and self.coin.overwintered: + # nVersion | fOverwintered + writers.write_uint32(w, self.tx.version | zcash.OVERWINTERED) + writers.write_uint32(w, self.tx.version_group_id) # nVersionGroupId else: - raise SigningError("Unknown cashaddr address type") - raw_address = bytes([version]) + data - else: + writers.write_uint32(w, self.tx.version) # nVersion + if not utils.BITCOIN_ONLY and self.coin.timestamp: + writers.write_uint32(w, self.tx.timestamp) + if True in self.segwit.values(): + writers.write_varint(w, 0x00) # segwit witness marker + writers.write_varint(w, 0x01) # segwit witness flag + writers.write_varint(w, self.tx.inputs_count) + + # TX Outputs + # === + + def output_derive_script(self, o: TxOutputType) -> bytes: + if o.script_type == OutputScriptType.PAYTOOPRETURN: + return scripts.output_script_paytoopreturn(o.op_return_data) + + if o.address_n: + # change output + o.address = self.get_address_for_change(o) + + if self.coin.bech32_prefix and o.address.startswith(self.coin.bech32_prefix): + # p2wpkh or p2wsh + witprog = addresses.decode_bech32_address( + self.coin.bech32_prefix, o.address + ) + return scripts.output_script_native_p2wpkh_or_p2wsh(witprog) + + if ( + not utils.BITCOIN_ONLY + and self.coin.cashaddr_prefix is not None + and o.address.startswith(self.coin.cashaddr_prefix + ":") + ): + prefix, addr = o.address.split(":") + version, data = cashaddr.decode(prefix, addr) + if version == cashaddr.ADDRESS_TYPE_P2KH: + version = self.coin.address_type + elif version == cashaddr.ADDRESS_TYPE_P2SH: + version = self.coin.address_type_p2sh + else: + raise SigningError("Unknown cashaddr address type") + raw_address = bytes([version]) + data + else: + try: + raw_address = base58.decode_check(o.address, self.coin.b58_hash) + except ValueError: + raise SigningError(FailureType.DataError, "Invalid address") + + if address_type.check(self.coin.address_type, raw_address): + # p2pkh + pubkeyhash = address_type.strip(self.coin.address_type, raw_address) + script = scripts.output_script_p2pkh(pubkeyhash) + return script + + elif address_type.check(self.coin.address_type_p2sh, raw_address): + # p2sh + scripthash = address_type.strip(self.coin.address_type_p2sh, raw_address) + script = scripts.output_script_p2sh(scripthash) + return script + + raise SigningError(FailureType.DataError, "Invalid address type") + + def get_address_for_change(self, o: TxOutputType): try: - raw_address = base58.decode_check(o.address, coin.b58_hash) - except ValueError: - raise SigningError(FailureType.DataError, "Invalid address") + input_script_type = helpers.CHANGE_OUTPUT_TO_INPUT_SCRIPT_TYPES[ + o.script_type + ] + except KeyError: + raise SigningError(FailureType.DataError, "Invalid script type") + node = self.keychain.derive(o.address_n, self.coin.curve_name) + return addresses.get_address(input_script_type, self.coin, node, o.multisig) - if address_type.check(coin.address_type, raw_address): - # p2pkh - pubkeyhash = address_type.strip(coin.address_type, raw_address) - script = scripts.output_script_p2pkh(pubkeyhash) - return script - - elif address_type.check(coin.address_type_p2sh, raw_address): - # p2sh - scripthash = address_type.strip(coin.address_type_p2sh, raw_address) - script = scripts.output_script_p2sh(scripthash) - return script - - raise SigningError(FailureType.DataError, "Invalid address type") - - -def get_address_for_change( - o: TxOutputType, coin: coininfo.CoinInfo, keychain: seed.Keychain -): - try: - input_script_type = helpers.CHANGE_OUTPUT_TO_INPUT_SCRIPT_TYPES[o.script_type] - except KeyError: - raise SigningError(FailureType.DataError, "Invalid script type") - node = keychain.derive(o.address_n, coin.curve_name) - return addresses.get_address(input_script_type, coin, node, o.multisig) - - -def output_is_change( - o: TxOutputType, wallet_path: list, multisig_fp: multisig.MultisigFingerprint, -) -> bool: - if o.script_type not in helpers.CHANGE_OUTPUT_SCRIPT_TYPES: - return False - if o.multisig and not multisig_fp.matches(o.multisig): - return False - return ( - wallet_path is not None - and wallet_path == o.address_n[:-_BIP32_WALLET_DEPTH] - and o.address_n[-2] <= _BIP32_CHANGE_CHAIN - and o.address_n[-1] <= _BIP32_MAX_LAST_ELEMENT - ) - - -# Tx Inputs -# === - - -def input_derive_script( - coin: coininfo.CoinInfo, i: TxInputType, pubkey: bytes, signature: bytes = None -) -> bytes: - if i.script_type == InputScriptType.SPENDADDRESS: - # p2pkh or p2sh - return scripts.input_script_p2pkh_or_p2sh( - pubkey, signature, get_hash_type(coin) + def output_is_change(self, o: TxOutputType) -> bool: + if o.script_type not in helpers.CHANGE_OUTPUT_SCRIPT_TYPES: + return False + if o.multisig and not self.multisig_fp.matches(o.multisig): + return False + return ( + self.wallet_path is not None + and self.wallet_path == o.address_n[:-_BIP32_WALLET_DEPTH] + and o.address_n[-2] <= _BIP32_CHANGE_CHAIN + and o.address_n[-1] <= _BIP32_MAX_LAST_ELEMENT ) - if i.script_type == InputScriptType.SPENDP2SHWITNESS: - # p2wpkh or p2wsh using p2sh + # Tx Inputs + # === - if i.multisig: - # p2wsh in p2sh - pubkeys = multisig.multisig_get_pubkeys(i.multisig) - witness_script_hasher = utils.HashWriter(sha256()) - scripts.output_script_multisig(pubkeys, i.multisig.m, witness_script_hasher) - witness_script_hash = witness_script_hasher.get_digest() - return scripts.input_script_p2wsh_in_p2sh(witness_script_hash) + def input_derive_script( + self, i: TxInputType, pubkey: bytes, signature: bytes = None + ) -> bytes: + if i.script_type == InputScriptType.SPENDADDRESS: + # p2pkh or p2sh + return scripts.input_script_p2pkh_or_p2sh( + pubkey, signature, self.get_hash_type() + ) - # p2wpkh in p2sh - return scripts.input_script_p2wpkh_in_p2sh( - addresses.ecdsa_hash_pubkey(pubkey, coin) - ) + if i.script_type == InputScriptType.SPENDP2SHWITNESS: + # p2wpkh or p2wsh using p2sh - elif i.script_type == InputScriptType.SPENDWITNESS: - # native p2wpkh or p2wsh - return scripts.input_script_native_p2wpkh_or_p2wsh() + if i.multisig: + # p2wsh in p2sh + pubkeys = multisig.multisig_get_pubkeys(i.multisig) + witness_script_hasher = utils.HashWriter(sha256()) + scripts.output_script_multisig( + pubkeys, i.multisig.m, witness_script_hasher + ) + witness_script_hash = witness_script_hasher.get_digest() + return scripts.input_script_p2wsh_in_p2sh(witness_script_hash) - elif i.script_type == InputScriptType.SPENDMULTISIG: - # p2sh multisig - signature_index = multisig.multisig_pubkey_index(i.multisig, pubkey) - return scripts.input_script_multisig( - i.multisig, signature, signature_index, get_hash_type(coin), coin - ) - - else: - raise SigningError(FailureType.ProcessError, "Invalid script type") + # p2wpkh in p2sh + return scripts.input_script_p2wpkh_in_p2sh( + addresses.ecdsa_hash_pubkey(pubkey, self.coin) + ) + elif i.script_type == InputScriptType.SPENDWITNESS: + # native p2wpkh or p2wsh + return scripts.input_script_native_p2wpkh_or_p2wsh() + elif i.script_type == InputScriptType.SPENDMULTISIG: + # p2sh multisig + signature_index = multisig.multisig_pubkey_index(i.multisig, pubkey) + return scripts.input_script_multisig( + i.multisig, signature, signature_index, self.get_hash_type(), self.coin + ) + else: + raise SigningError(FailureType.ProcessError, "Invalid script type") def input_is_segwit(i: TxInputType) -> bool: