From d8548dc9a7868035a1be0889cf5a1539003ad258 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 5 Aug 2026 11:43:49 +0000 Subject: [PATCH 1/4] transaction: add comments about base43 (and 58) deser being quadratic --- electrum/bitcoin.py | 7 ++++++- electrum/transaction.py | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/electrum/bitcoin.py b/electrum/bitcoin.py index 7afde6e54c7c..2c177c37386e 100644 --- a/electrum/bitcoin.py +++ b/electrum/bitcoin.py @@ -539,7 +539,10 @@ class BaseDecodeError(BitcoinException): pass def base_encode(v: bytes, *, base: int) -> str: - """ encode v, which is a string of bytes, to base58.""" + """ encode v, which is a string of bytes, to base58. + + note: time complexity is O(len(v)^2), due to big-int arithmetic. + """ assert_bytes(v) if base not in (58, 43): raise ValueError('not supported base: {}'.format(base)) @@ -564,6 +567,8 @@ def base_encode(v: bytes, *, base: int) -> str: def base_decode(v: Union[bytes, str], *, base: int) -> Optional[bytes]: """ decode v into a string of len bytes. + note: time complexity is O(len(v)^2), due to big-int arithmetic. + based on the work of David Keijser in https://github.com/keis/base58 """ # assert_bytes(v) diff --git a/electrum/transaction.py b/electrum/transaction.py index 46b9351c765d..b371b898c8a6 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -1228,7 +1228,8 @@ def to_qr_data(self) -> Tuple[str, bool]: tx.convert_all_utxos_to_witness_utxos() is_complete = False tx_bytes = tx.serialize_as_bytes() - return base_encode(tx_bytes, base=43), is_complete + tx_base43 = base_encode(tx_bytes, base=43) # FIXME this takes quadratic time in len(tx) + return tx_base43, is_complete def txid(self) -> Optional[str]: if self._cached_txid is None: @@ -1501,6 +1502,10 @@ def convert_raw_tx_to_hex(raw: Union[str, bytes]) -> str: pass # try base43 try: + # FIXME This takes quadratic time in len(tx). + # We could prefix all txs we base43-serialize with e.g. "BASE43TX:", + # (and break-compat with old versions). Then at least we would not attempt + # the expensive deser here if it's not needed. return base_decode(raw, base=43).hex() except Exception: pass From 5de8ae887d1440d90d54976b7f0c9fdd82c786d2 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 5 Aug 2026 11:45:45 +0000 Subject: [PATCH 2/4] bitcoin.py: small speedup for base_encode() $ python3 -m timeit --setup "import os; from electrum.bitcoin import base_encode; bvec = os.urandom(3*10**4);" "base_encode(bvec, base=58)" 1 loop, best of 5: 673 msec per loop $ python3 -m timeit --setup "import os; from electrum.bitcoin import base_encode; bvec = os.urandom(3*10**4);" "base_encode(bvec, base=58)" 1 loop, best of 5: 655 msec per loop --- electrum/bitcoin.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/electrum/bitcoin.py b/electrum/bitcoin.py index 2c177c37386e..b9906ce72f5a 100644 --- a/electrum/bitcoin.py +++ b/electrum/bitcoin.py @@ -555,10 +555,11 @@ def base_encode(v: bytes, *, base: int) -> str: newlen = len(v) num = int.from_bytes(v, byteorder='big') - string = b"" + string_rev = bytearray() while num: num, idx = divmod(num, base) - string = chars[idx:idx + 1] + string + string_rev += chars[idx:idx + 1] + string = string_rev[::-1] result = chars[0:1] * (origlen - newlen) + string return result.decode('ascii') From 9d9a503a91df92a136234b6cd2fc88d679207232 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 5 Aug 2026 11:49:54 +0000 Subject: [PATCH 3/4] transaction: convert_raw_tx_to_hex: try base43 later as it's expensive --- electrum/transaction.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/electrum/transaction.py b/electrum/transaction.py index b371b898c8a6..f3d95c0f2f56 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -1500,6 +1500,12 @@ def convert_raw_tx_to_hex(raw: Union[str, bytes]) -> str: return binascii.unhexlify(raw).hex() except Exception: pass + # try base64 + if raw[0:6] in ('cHNidP', b'cHNidP'): # base64 psbt + try: + return base64.b64decode(raw, validate=True).hex() + except Exception: + pass # try base43 try: # FIXME This takes quadratic time in len(tx). @@ -1509,12 +1515,6 @@ def convert_raw_tx_to_hex(raw: Union[str, bytes]) -> str: return base_decode(raw, base=43).hex() except Exception: pass - # try base64 - if raw[0:6] in ('cHNidP', b'cHNidP'): # base64 psbt - try: - return base64.b64decode(raw, validate=True).hex() - except Exception: - pass # raw bytes if isinstance(raw, (bytes, bytearray)): return raw.hex() From 08622c7433b7e98966bd67b1798c6b8bbdf43df3 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 5 Aug 2026 12:02:23 +0000 Subject: [PATCH 4/4] transaction: convert_raw_tx_to_hex: forbid base43 for too large txs --- electrum/transaction.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/electrum/transaction.py b/electrum/transaction.py index f3d95c0f2f56..3641e6399351 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -1512,6 +1512,11 @@ def convert_raw_tx_to_hex(raw: Union[str, bytes]) -> str: # We could prefix all txs we base43-serialize with e.g. "BASE43TX:", # (and break-compat with old versions). Then at least we would not attempt # the expensive deser here if it's not needed. + if len(raw) > 30_000: + # note: base_decode for this length takes around 0.2 sec on my laptop. + # note: We only use/expect base43 inside QR codes. The max data a QR can fit is around 4 KB, + # serializing that to b43 results in a length of ~5500. 30k is already over 5x that. + raise ValueError("raw tx too large for base43") return base_decode(raw, base=43).hex() except Exception: pass