diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 2a06061e..f05b0ac5 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -1,3 +1,8 @@ +# Release 3.0.6 : + + * Fix transaction parsing bug #3788 + + # Release 3.0.5 : (Security update) This is a follow-up to the 3.0.4 release, which did not completely fix diff --git a/contrib/build-wine/build-electrum-git.sh b/contrib/build-wine/build-electrum-git.sh index c849a672..58d7f132 100755 --- a/contrib/build-wine/build-electrum-git.sh +++ b/contrib/build-wine/build-electrum-git.sh @@ -39,10 +39,7 @@ done popd pushd electrum -if [ ! -z "$1" ]; then - git checkout $1 -fi - +git checkout $BRANCH VERSION=`git describe --tags` echo "Last commit: $VERSION" find -exec touch -d '2000-11-11T11:11:11+00:00' {} + diff --git a/contrib/build-wine/prepare-hw.sh b/contrib/build-wine/prepare-hw.sh index 1851b7b0..c73e13fc 100755 --- a/contrib/build-wine/prepare-hw.sh +++ b/contrib/build-wine/prepare-hw.sh @@ -23,6 +23,6 @@ cd tmp $PYTHON -m pip install setuptools --upgrade $PYTHON -m pip install cython --upgrade $PYTHON -m pip install trezor==0.7.16 --upgrade -$PYTHON -m pip install keepkey==4.0.0 --upgrade -$PYTHON -m pip install btchip-python==0.1.23 --upgrade +$PYTHON -m pip install keepkey==4.0.2 --upgrade +$PYTHON -m pip install btchip-python==0.1.24 --upgrade diff --git a/electrum-zcl b/electrum-zcl index 7f79862d..c3d2d96b 100755 --- a/electrum-zcl +++ b/electrum-zcl @@ -281,7 +281,8 @@ def run_offline_command(config, config_options): # arguments passed to function args = [config.get(x) for x in cmd.params] # decode json arguments - args = list(map(json_decode, args)) + if cmdname not in ('setconfig',): + args = list(map(json_decode, args)) # options kwargs = {} for x in cmd.options: diff --git a/gui/kivy/main_window.py b/gui/kivy/main_window.py index eae1343a..a74e2b1f 100644 --- a/gui/kivy/main_window.py +++ b/gui/kivy/main_window.py @@ -928,6 +928,10 @@ class ElectrumWindow(App): return if not self.wallet.can_export(): return - key = str(self.wallet.export_private_key(addr, password)[0]) - pk_label.data = key + try: + key = str(self.wallet.export_private_key(addr, password)[0]) + pk_label.data = key + except InvalidPassword: + self.show_error("Invalid PIN") + return self.protected(_("Enter your PIN code in order to decrypt your private key"), show_private_key, (addr, pk_label)) diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index a0fa152a..ffe26ea5 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -2666,7 +2666,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): unit_combo = QComboBox() unit_combo.addItems(units) unit_combo.setCurrentIndex(units.index(self.base_unit())) - def on_unit(x): + def on_unit(x, nz): unit_result = units[unit_combo.currentIndex()] if self.base_unit() == unit_result: return @@ -2681,13 +2681,14 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): else: raise Exception('Unknown base unit') self.config.set_key('decimal_point', self.decimal_point, True) + nz.setMaximum(self.decimal_point) self.history_list.update() self.request_list.update() self.address_list.update() for edit, amount in zip(edits, amounts): edit.setAmount(amount) self.update_status() - unit_combo.currentIndexChanged.connect(on_unit) + unit_combo.currentIndexChanged.connect(lambda x: on_unit(x, nz)) gui_widgets.append((unit_label, unit_combo)) block_explorers = sorted(util.block_explorer_info().keys()) diff --git a/gui/qt/qrtextedit.py b/gui/qt/qrtextedit.py index aef68f05..48770c73 100644 --- a/gui/qt/qrtextedit.py +++ b/gui/qt/qrtextedit.py @@ -59,11 +59,7 @@ class ScanQRTextEdit(ButtonsTextEdit, MessageBoxMixin): data = '' if not data: data = '' - if self.allow_multi: - new_text = self.text() + data + '\n' - else: - new_text = data - self.setText(new_text) + self.setText(data) return data def contextMenuEvent(self, e): diff --git a/icons/unpaid.png b/icons/unpaid.png index e0f3639d..579ec4eb 100644 Binary files a/icons/unpaid.png and b/icons/unpaid.png differ diff --git a/lib/commands.py b/lib/commands.py index 1ab2a3b1..1f2b3797 100644 --- a/lib/commands.py +++ b/lib/commands.py @@ -34,7 +34,7 @@ from functools import wraps from decimal import Decimal from .import util -from .util import bfh, bh2u, format_satoshis +from .util import bfh, bh2u, format_satoshis, json_decode from .import bitcoin from .bitcoin import is_address, hash_160, COIN, TYPE_ADDRESS from .i18n import _ @@ -151,10 +151,8 @@ class Commands: @command('') def setconfig(self, key, value): """Set a configuration variable. 'value' may be a string or a Python expression.""" - try: - value = ast.literal_eval(value) - except: - pass + if key not in ('rpcuser', 'rpcpassword'): + value = json_decode(value) self.config.set_key(key, value) return True diff --git a/lib/exchange_rate.py b/lib/exchange_rate.py index bb095776..d929b6bb 100644 --- a/lib/exchange_rate.py +++ b/lib/exchange_rate.py @@ -5,6 +5,7 @@ import sys from threading import Thread import time import csv +import decimal from decimal import Decimal from .bitcoin import COIN @@ -165,7 +166,11 @@ class FxThread(ThreadJob): def ccy_amount_str(self, amount, commas): prec = CCY_PRECISIONS.get(self.ccy, 2) fmt_str = "{:%s.%df}" % ("," if commas else "", max(0, prec)) - return fmt_str.format(round(amount, prec)) + try: + rounded_amount = round(amount, prec) + except decimal.InvalidOperation: + rounded_amount = amount + return fmt_str.format(rounded_amount) def run(self): # This runs from the plugins thread which catches exceptions diff --git a/lib/tests/test_transaction.py b/lib/tests/test_transaction.py index 99c6d001..609006cd 100644 --- a/lib/tests/test_transaction.py +++ b/lib/tests/test_transaction.py @@ -231,6 +231,10 @@ class TestTransaction(unittest.TestCase): tx = transaction.Transaction('010000000001010d350cefa29138de18a2d63a93cffda63721b07a6ecfa80a902f9514104b55ca0000000000fdffffff012a4a824a00000000160014b869999d342a5d42d6dc7af1efc28456da40297a024730440220475bb55814a52ea1036919e4408218c693b8bf93637b9f54c821b5baa3b846e102207276ed7a79493142c11fb01808a4142bbdd525ae7bdccdf8ecb7b8e3c856b4d90121024cdeaca7a53a7e23a1edbe9260794eaa83063534b5f111ee3c67d8b0cb88f0eec8010000') self.assertEqual('51087ece75c697cc872d2e643d646b0f3e1f2666fa1820b7bff4343d50dd680e', tx.txid()) + def test_txid_input_p2wsh_p2sh_not_multisig(self): + tx = transaction.Transaction('0100000000010160f84fdcda039c3ca1b20038adea2d49a53db92f7c467e8def13734232bb610804000000232200202814720f16329ab81cb8867c4d447bd13255931f23e6655944c9ada1797fcf88ffffffff0ba3dcfc04000000001976a91488124a57c548c9e7b1dd687455af803bd5765dea88acc9f44900000000001976a914da55045a0ccd40a56ce861946d13eb861eb5f2d788ac49825e000000000017a914ca34d4b190e36479aa6e0023cfe0a8537c6aa8dd87680c0d00000000001976a914651102524c424b2e7c44787c4f21e4c54dffafc088acf02fa9000000000017a914ee6c596e6f7066466d778d4f9ba633a564a6e95d874d250900000000001976a9146ca7976b48c04fd23867748382ee8401b1d27c2988acf5119600000000001976a914cf47d5dcdba02fd547c600697097252d38c3214a88ace08a12000000000017a914017bef79d92d5ec08c051786bad317e5dd3befcf87e3d76201000000001976a9148ec1b88b66d142bcbdb42797a0fd402c23e0eec288ac718f6900000000001976a914e66344472a224ce6f843f2989accf435ae6a808988ac65e51300000000001976a914cad6717c13a2079066f876933834210ebbe68c3f88ac0347304402201a4907c4706104320313e182ecbb1b265b2d023a79586671386de86bb47461590220472c3db9fc99a728ebb9b555a72e3481d20b181bd059a9c1acadfb853d90c96c01210338a46f2a54112fef8803c8478bc17e5f8fc6a5ec276903a946c1fafb2e3a8b181976a914eda8660085bf607b82bd18560ca8f3a9ec49178588ac00000000') + self.assertEqual('e9933221a150f78f9f224899f8568ff6422ffcc28ca3d53d87936368ff7c4b1d', tx.txid()) + class NetworkMock(object): diff --git a/lib/transaction.py b/lib/transaction.py index b18d1842..b01be209 100644 --- a/lib/transaction.py +++ b/lib/transaction.py @@ -45,6 +45,14 @@ class SerializationError(Exception): """ Thrown when there's a problem deserializing or serializing """ +class UnknownTxinType(Exception): + pass + + +class NotRecognizedRedeemScript(Exception): + pass + + class BCDataStream(object): def __init__(self): self.input = None @@ -302,10 +310,23 @@ def parse_scriptSig(d, _bytes): if match_decoded(decoded, match): item = decoded[0][1] if item[0] == 0: + # segwit embedded into p2sh + # witness version 0 + # segwit embedded into p2sh d['address'] = bitcoin.hash160_to_p2sh(bitcoin.hash_160(item)) - d['type'] = 'p2wpkh-p2sh' if len(item) == 22 else 'p2wsh-p2sh' + if len(item) == 22: + d['type'] = 'p2wpkh-p2sh' + elif len(item) == 34: + d['type'] = 'p2wsh-p2sh' + else: + print_error("unrecognized txin type", bh2u(item)) + elif opcodes.OP_1 <= item[0] <= opcodes.OP_16: + # segwit embedded into p2sh + # witness version 1-16 + pass else: - # payto_pubkey + # assert item[0] == 0x30 + # pay-to-pubkey d['type'] = 'p2pk' d['address'] = "(pubkey)" d['signatures'] = [bh2u(item)] @@ -361,7 +382,7 @@ def parse_redeemScript(s): match_multisig = [ op_m ] + [opcodes.OP_PUSHDATA4]*n + [ op_n, opcodes.OP_CHECKMULTISIG ] if not match_decoded(dec2, match_multisig): print_error("cannot find address in input script", bh2u(s)) - return + raise NotRecognizedRedeemScript() x_pubkeys = [bh2u(x[1]) for x in dec2[1:-2]] pubkeys = [safe_parse_pubkey(x) for x in x_pubkeys] redeemScript = multisig_script(pubkeys, m) @@ -430,21 +451,40 @@ def parse_witness(vds, txin): if n == 0xffffffff: txin['value'] = vds.read_uint64() n = vds.read_compact_size() + # now 'n' is the number of items in the witness w = list(bh2u(vds.read_bytes(vds.read_compact_size())) for i in range(n)) + + add_w = lambda x: var_int(len(x) // 2) + x + txin['witness'] = var_int(n) + ''.join(add_w(i) for i in w) + + # FIXME: witness version > 0 will probably fail here. + # For native segwit, we would need the scriptPubKey of the parent txn + # to determine witness program version, and properly parse the witness. + # In case of p2sh-segwit, we can tell based on the scriptSig in this txn. + # The code below assumes witness version 0. + # p2sh-segwit should work in that case; for native segwit we need to tell + # between p2wpkh and p2wsh; we do this based on number of witness items, + # hence (FIXME) p2wsh with n==2 (maybe n==1 ?) will probably fail. + # If v==0 and n==2, we need parent scriptPubKey to distinguish between p2wpkh and p2wsh. if txin['type'] == 'coinbase': pass - elif n > 2: + elif txin['type'] == 'p2wsh-p2sh' or n > 2: + try: + m, n, x_pubkeys, pubkeys, witnessScript = parse_redeemScript(bfh(w[-1])) + except NotRecognizedRedeemScript: + raise UnknownTxinType() txin['signatures'] = parse_sig(w[1:-1]) - m, n, x_pubkeys, pubkeys, witnessScript = parse_redeemScript(bfh(w[-1])) txin['num_sig'] = m txin['x_pubkeys'] = x_pubkeys txin['pubkeys'] = pubkeys txin['witnessScript'] = witnessScript - else: + elif txin['type'] == 'p2wpkh-p2sh' or n == 2: txin['num_sig'] = 1 txin['x_pubkeys'] = [w[1]] txin['pubkeys'] = [safe_parse_pubkey(w[1])] txin['signatures'] = parse_sig([w[0]]) + else: + raise UnknownTxinType() def parse_output(vds, i): d = {} @@ -466,6 +506,23 @@ def deserialize(raw): d['inputs'] = [parse_input(vds) for i in range(n_vin)] n_vout = vds.read_compact_size() d['outputs'] = [parse_output(vds, i) for i in range(n_vout)] + if is_segwit: + for i in range(n_vin): + txin = d['inputs'][i] + try: + parse_witness(vds, txin) + except UnknownTxinType: + txin['type'] = 'unknown' + # FIXME: GUI might show 'unknown' address (e.g. for a non-multisig p2wsh) + continue + # segwit-native script + if not txin.get('scriptSig'): + if txin['num_sig'] == 1: + txin['type'] = 'p2wpkh' + txin['address'] = bitcoin.public_key_to_p2wpkh(bfh(txin['pubkeys'][0])) + else: + txin['type'] = 'p2wsh' + txin['address'] = bitcoin.script_to_p2wsh(txin['witnessScript']) d['lockTime'] = vds.read_uint32() return d @@ -657,7 +714,9 @@ class Transaction: witness_script = multisig_script(pubkeys, txin['num_sig']) witness = var_int(n) + '00' + ''.join(add_w(x) for x in sig_list) + add_w(witness_script) else: - raise BaseException('wrong txin type') + witness = txin.get('witness', None) + if not witness: + raise BaseException('wrong txin type:', txin['type']) if self.is_txin_complete(txin) or estimate_size: value_field = '' else: @@ -666,7 +725,8 @@ class Transaction: @classmethod def is_segwit_input(cls, txin): - return cls.is_segwit_inputtype(txin['type']) + has_nonzero_witness = txin.get('witness', '00') != '00' + return cls.is_segwit_inputtype(txin['type']) or has_nonzero_witness @classmethod def is_segwit_inputtype(cls, txin_type): diff --git a/lib/util.py b/lib/util.py index f295f168..f0423800 100644 --- a/lib/util.py +++ b/lib/util.py @@ -347,7 +347,7 @@ def format_satoshis_plain(x, decimal_point = 8): def format_satoshis(x, is_diff=False, num_zeros = 0, decimal_point = 8, whitespaces=False): from locale import localeconv if x is None: - return 'Unknown' + return 'unknown' x = int(x) # Some callers pass Decimal scale_factor = pow (10, decimal_point) integer_part = "{:n}".format(int(abs(x) / scale_factor)) diff --git a/lib/wallet.py b/lib/wallet.py index cda0cf92..2ff8396b 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -367,7 +367,8 @@ class Abstract_Wallet(PrintError): def add_unverified_tx(self, tx_hash, tx_height): if tx_height == 0 and tx_hash in self.verified_tx: self.verified_tx.pop(tx_hash) - self.verifier.merkle_roots.pop(tx_hash, None) + if self.verifier: + self.verifier.merkle_roots.pop(tx_hash, None) # tx will be verified only if height > 0 if tx_hash not in self.verified_tx: diff --git a/lib/websockets.py b/lib/websockets.py index f2bd9149..415556b3 100644 --- a/lib/websockets.py +++ b/lib/websockets.py @@ -84,7 +84,8 @@ class WsClientThread(util.DaemonThread): l = self.subscriptions.get(addr, []) l.append((ws, amount)) self.subscriptions[addr] = l - self.network.send([('blockchain.address.subscribe', [addr])], self.response_queue.put) + h = self.network.addr_to_scripthash(addr) + self.network.send([('blockchain.scripthash.subscribe', [h])], self.response_queue.put) def run(self): @@ -100,10 +101,13 @@ class WsClientThread(util.DaemonThread): result = r.get('result') if result is None: continue - if method == 'blockchain.address.subscribe': - self.network.send([('blockchain.address.get_balance', params)], self.response_queue.put) - elif method == 'blockchain.address.get_balance': - addr = params[0] + if method == 'blockchain.scripthash.subscribe': + self.network.send([('blockchain.scripthash.get_balance', params)], self.response_queue.put) + elif method == 'blockchain.scripthash.get_balance': + h = params[0] + addr = self.network.h2addr.get(h, None) + if addr is None: + util.print_error("can't find address for scripthash: %s" % h) l = self.subscriptions.get(addr, []) for ws, amount in l: if not ws.closed: diff --git a/plugins/ledger/ledger.py b/plugins/ledger/ledger.py index e89747aa..f9e06cd9 100644 --- a/plugins/ledger/ledger.py +++ b/plugins/ledger/ledger.py @@ -60,6 +60,7 @@ class Ledger_Client(): def versiontuple(self, v): return tuple(map(int, (v.split(".")))) + def test_pin_unlocked(func): """Function decorator to test the Ledger for being unlocked, and if not, raise a human-readable exception.