What changed, and why it matters
This change is a performance optimization, not a security fix. It makes whitespace removal from transaction data optional, skipping it when loading transactions from the wallet's own stored files. The default behavior for user-provided input still removes whitespace. There is no direct security issue in the patch itself, though any future code that calls tx_from_any with untrusted input must remember to leave sanitization enabled.
No immediate action required. Treat as routine performance optimization. Review future callers of tx_from_any to ensure sanitize=False is not used with untrusted/user-supplied strings.
Security signals we found
Behavior-preserving refactor: default sanitization remains enabled
Trusted-data paths explicitly opt out of regex sanitization
No new input surface introduced
No validation logic removed; only relocated
Evidence from the diff
The commit refactors tx_from_any() to add a sanitize parameter (default True) that controls whether re.sub(r’\s’, ‘’, raw) is run on string inputs. Internal wallet paths—loading from wallet_db, adding to AddressSynchronizer history, and TxInput utxo handling—now pass sanitize=False to avoid expensive regex calls on already-trusted data. The actual whitespace stripping logic is moved from convert_raw_tx_to_hex() into tx_from_any(). The change preserves existing behavior for user-facing entry points because the default remains sanitize=True.
Changed components
electrum/transaction.py: tx_from_any(), convert_raw_tx_to_hex()electrum/wallet_db.py: transaction deserialization during wallet loadelectrum/address_synchronizer.py: add_transaction_from_storage()electrum/transaction.py: TxInput.parse_script()Inspect captured patch +21 / −18
diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py
index 3e9502e..be57de1 100644
--- a/electrum/address_synchronizer.py
+++ b/electrum/address_synchronizer.py
@@ -293,7 +293,7 @@ class AddressSynchronizer(Logger, EventListener):
if tx_hash is None:
raise Exception("cannot add tx without txid to wallet history")
# For sanity, try to serialize and deserialize tx early:
- tx_from_any(str(tx)) # see if raises (no-side-effects)
+ tx_from_any(str(tx), sanitize=False) # see if raises (no-side-effects)
with self.lock:
# NOTE: returning if tx in self.transactions might seem like a good idea
# BUT we track is_mine inputs in a txn, and during subsequent calls
diff --git a/electrum/transaction.py b/electrum/transaction.py
index 94d4063..591d1ed 100644
--- a/electrum/transaction.py
+++ b/electrum/transaction.py
@@ -380,7 +380,7 @@ class TxInput:
return
# note that tx might be a PartialTransaction
# serialize and de-serialize tx now. this might e.g. convert a complete PartialTx to a Tx
- tx = tx_from_any(str(tx))
+ tx = tx_from_any(str(tx), sanitize=False)
# 'utxo' field should not be a PSBT:
if not tx.is_complete():
return
@@ -1492,16 +1492,6 @@ def convert_raw_tx_to_hex(raw: Union[str, bytes]) -> str:
if not raw:
raise ValueError("empty string")
raw_unstripped = raw
- if isinstance(raw, str):
- # remove all whitespace characters, anywhere, for convenience
- # - leading/trailing whitespaces are quite common for user-input
- # - newlines in the middle can also happen, e.g. when copying a raw tx from a pdf
- # note: we don't do this for bytes-like inputs, as whitespace-looking bytes can appear
- # anywhere in a raw tx. Even leading/trailing pseudo-whitespace: consider that
- # the nVersion or the nLocktime might contain e.g. "0a" bytes
- # consider: "\n".encode().hex() == "0a"
- # For str, this is a non-issue and safe to do.
- raw = re.sub(r'\s', '', raw)
# try hex
try:
return binascii.unhexlify(raw).hex()
@@ -1519,15 +1509,28 @@ def convert_raw_tx_to_hex(raw: Union[str, bytes]) -> str:
except Exception:
pass
# raw bytes (do not strip whitespaces in this case)
+ if isinstance(raw, bytearray):
+ raw = bytes(raw)
if isinstance(raw_unstripped, bytes):
return raw_unstripped.hex()
raise ValueError(f"failed to recognize transaction encoding for txt: {raw[:30]}...")
-def tx_from_any(raw: Union[str, bytes], *,
- deserialize: bool = True) -> Union['PartialTransaction', 'Transaction']:
- if isinstance(raw, bytearray):
- raw = bytes(raw)
+def tx_from_any(
+ raw: Union[str, bytes], *,
+ deserialize: bool = True,
+ sanitize: bool = True) -> Union['PartialTransaction', 'Transaction']:
+ # re.sub is expensive, set sanitize to False if raw data is not from user input
+ if isinstance(raw, str) and sanitize:
+ # remove all whitespace characters, anywhere, for convenience
+ # - leading/trailing whitespaces are quite common for user-input
+ # - newlines in the middle can also happen, e.g. when copying a raw tx from a pdf
+ # note: we don't do this for bytes-like inputs, as whitespace-looking bytes can appear
+ # anywhere in a raw tx. Even leading/trailing pseudo-whitespace: consider that
+ # the nVersion or the nLocktime might contain e.g. "0a" bytes
+ # consider: "\n".encode().hex() == "0a"
+ # For str, this is a non-issue and safe to do.
+ raw = re.sub(r'\s', '', raw)
raw = convert_raw_tx_to_hex(raw)
try:
return PartialTransaction.from_raw_psbt(raw)
diff --git a/electrum/wallet_db.py b/electrum/wallet_db.py
index c53c355..ab3f6af 100644
--- a/electrum/wallet_db.py
+++ b/electrum/wallet_db.py
@@ -104,7 +104,7 @@ class WalletFileExceptionVersion51(WalletFileException): pass
# register dicts that require value conversions not handled by constructor
-register_name('transactions/*', None, lambda x: tx_from_any(x, deserialize=False))
+register_name('transactions/*', None, lambda x: tx_from_any(x, deserialize=False, sanitize=False))
register_name('data_loss_protect_remote_pcp/*', None, lambda x: bytes.fromhex(x))
# register tuples, otherwise they will default to StoredList
register_name('contacts/*', None, tuple)
@@ -1725,7 +1725,7 @@ class WalletDB(JsonDB):
assert isinstance(tx, Transaction), tx
# note that tx might be a PartialTransaction
# serialize and de-serialize tx now. this might e.g. convert a complete PartialTx to a Tx
- tx = tx_from_any(str(tx))
+ tx = tx_from_any(str(tx), sanitize=False)
if not tx_hash:
raise Exception("trying to add tx to db without txid")
if tx_hash != tx.txid():
Why this scored 17/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.