What changed, and why it matters
This commit adds a new command-line feature to export the secret 'preimage' for a settled Lightning invoice, and also includes the preimage in the result of checking a held invoice. The preimage is the cryptographic proof that a Lightning payment was received; anyone who knows it can claim the payment. The developers deliberately avoided exposing the preimage in some other command outputs because it could be accidentally shared with a payer or exposed via the payment server. The change itself is a feature addition, not a fix for an active vulnerability, but it touches sensitive secret material.
Treat this as a security-sensitive feature addition rather than an emergency vulnerability. Review access controls around the new CLI command and ensure the preimage is not logged, cached, or returned in contexts where it could be forwarded to untrusted parties. Verify that the payserver and any RPC consumers do not inadvertently expose `export_requests` preimage data. Consider documenting the command's sensitivity for users.
Security signals we found
New CLI command exports Lightning payment preimage, a secret that authorizes payment settlement
Commit message explicitly discusses risk of preimage exposure via CLI responses and payserver
Assertion added to verify preimage correctness before returning it
Preimage added to `check_hold_invoice` return value and to paid Lightning request exports
No CVE, advisory, or vendor security disclosure present in supplied materials
Evidence from the diff
The patch introduces export_lightning_preimage in electrum/commands.py, which returns the stored SHA256 preimage for a given Lightning payment hash, with an assertion that the returned preimage’s hash matches the requested payment hash. It also modifies check_hold_invoice to include the preimage in its return value when available, and updates wallet.py so that export_requests includes the preimage for paid Lightning requests. The commit message explicitly notes the security sensitivity of preimage exposure and deliberately avoids returning it in wallet.export_requests for unpaid or broadly-shared contexts.
Changed components
electrum/commands.pyelectrum/wallet.pytests/test_commands.pyInspect captured patch +34 / −2
diff --git a/electrum/commands.py b/electrum/commands.py
index 09bc31a..1852434 100644
--- a/electrum/commands.py
+++ b/electrum/commands.py
@@ -1514,10 +1514,22 @@ class Commands(Logger):
plist = wallet.lnworker.get_payments(status='settled')[bfh(payment_hash)]
_dir, amount_msat, _fee, _ts = wallet.lnworker.get_payment_value(info, plist)
result["received_amount_sat"] = amount_msat // 1000
+ result['preimage'] = wallet.lnworker.get_preimage_hex(payment_hash)
if info is not None:
result["invoice_amount_sat"] = (info.amount_msat or 0) // 1000
return result
+ @command('wl')
+ async def export_lightning_preimage(self, payment_hash: str, wallet: 'Abstract_Wallet' = None) -> Optional[str]:
+ """
+ Returns the stored preimage of the given payment_hash if it is known.
+
+ arg:str:payment_hash: Hash of the preimage
+ """
+ preimage = wallet.lnworker.get_preimage_hex(payment_hash)
+ assert preimage is None or crypto.sha256(bytes.fromhex(preimage)).hex() == payment_hash
+ return preimage
+
@command('w')
async def addtransaction(self, tx, wallet: Abstract_Wallet = None):
"""
diff --git a/electrum/wallet.py b/electrum/wallet.py
index 7bf807e..ab1ac29 100644
--- a/electrum/wallet.py
+++ b/electrum/wallet.py
@@ -2930,8 +2930,11 @@ class Abstract_Wallet(ABC, Logger, EventListener):
if x.is_lightning():
d['rhash'] = x.rhash
d['lightning_invoice'] = self.get_bolt11_invoice(x)
- if self.lnworker and status == PR_UNPAID:
- d['can_receive'] = self.lnworker.can_receive_invoice(x)
+ if self.lnworker:
+ if status == PR_UNPAID:
+ d['can_receive'] = self.lnworker.can_receive_invoice(x)
+ elif status == PR_PAID and (preimage := self.lnworker.get_preimage(x.payment_hash)):
+ d['preimage'] = preimage.hex()
if address := x.get_address():
d['address'] = address
d['URI'] = self.get_request_URI(x)
diff --git a/tests/test_commands.py b/tests/test_commands.py
index ccd052b..a97b99f 100644
--- a/tests/test_commands.py
+++ b/tests/test_commands.py
@@ -572,6 +572,7 @@ class TestCommandsTestnet(ElectrumTestCase):
assert settled_status['status'] == 'settled'
assert settled_status['received_amount_sat'] == 10000
assert settled_status['invoice_amount_sat'] == 10000
+ assert settled_status['preimage'] == preimage.hex()
with self.assertRaises(AssertionError):
# cancelling a settled invoice should raise
@@ -723,3 +724,19 @@ class TestCommandsTestnet(ElectrumTestCase):
}
}
self.assertEqual(result, expected_result)
+
+ @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
+ async def test_export_lightning_preimage(self, *mock_args):
+ w = restore_wallet_from_text__for_unittest(
+ 'disagree rug lemon bean unaware square alone beach tennis exhibit fix mimic',
+ path='if_this_exists_mocking_failed_648151893',
+ config=self.config)['wallet']
+ cmds = Commands(config=self.config)
+
+ preimage = os.urandom(32)
+ payment_hash = sha256(preimage)
+ w.lnworker.save_preimage(payment_hash, preimage)
+
+ assert await cmds.export_lightning_preimage(payment_hash=payment_hash.hex(), wallet=w) == preimage.hex()
+ assert await cmds.export_lightning_preimage(payment_hash=os.urandom(32).hex(), wallet=w) is None
+
Why this scored 21/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.