This commit is contained in:
Justin Sharp 2026-07-20 13:03:17 -06:00 committed by GitHub
commit dbf9dda5b8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 116 additions and 6 deletions

View file

@ -49,7 +49,7 @@ async def _make_local_call(*args: str):
# in the CLN grpc interface yet.
# Pass the command as a discrete argv list (create_subprocess_exec, not
# _shell) so user-controlled arguments such as the bolt11 in decodepay
# _shell) so user-controlled arguments such as the string in decode
# can never be interpreted as shell syntax.
testnet = config("BAPI_NETWORK") == "testnet"
argv = ["lightning-cli", "-k", *(["--testnet"] if testnet else []), *args]
@ -551,7 +551,7 @@ class LnNodeCLNgRPC(LightningNodeBase):
async def decode_pay_request(self, pay_req: str) -> PaymentRequest:
logger.trace(f"decode_pay_request(pay_req={pay_req})")
res = await _make_local_call("decodepay", f"bolt11={pay_req}")
res = await _make_local_call("decode", f"string={pay_req}")
if not res:
raise HTTPException(
@ -569,7 +569,21 @@ class LnNodeCLNgRPC(LightningNodeBase):
raise_for_pay_req_decode_error(decoded)
return PaymentRequest.from_cln_json(json.loads(decoded))
data = json.loads(decoded)
if not data.get("valid", True):
# unlike decodepay, decode reports recognized-but-invalid strings
# as a normal result with valid=false + warning_* fields instead
# of an RPC error
m = "; ".join(
str(data[k]) for k in sorted(data) if k.startswith("warning")
) or "invalid payment request"
logger.error(m)
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail=f"Could not decode the payment request: {m}",
)
return PaymentRequest.from_cln_json(data)
@logger.catch(exclude=(HTTPException,))
async def get_fee_revenue(self) -> FeeRevenue:

View file

@ -467,10 +467,22 @@ class LnNodeCLNjRPC(LightningNodeBase):
return self._bolt11_cache[pay_req]
params = [pay_req]
res = await self._send_request("decodepay", params)
res = await self._send_request("decode", params)
if "error" not in res:
res = res["result"]
if not res.get("valid", True):
# unlike decodepay, decode reports recognized-but-invalid
# strings as a normal result with valid=false + warning_*
# fields instead of an RPC error
m = "; ".join(
str(res[k]) for k in sorted(res) if k.startswith("warning")
) or "invalid payment request"
logger.error(m)
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail=f"Could not decode the payment request: {m}",
)
req = PaymentRequest.from_cln_json(res)
self._bolt11_cache[pay_req] = req
return req

View file

@ -45,7 +45,7 @@ async def test_make_local_call_passes_args_without_a_shell(capture_exec):
from app.lightning.impl import cln_grpc
payload = "lnbc1pdummy; touch /tmp/pwned"
await cln_grpc._make_local_call("decodepay", f"bolt11={payload}")
await cln_grpc._make_local_call("decode", f"string={payload}")
assert capture_exec["shell_used"] is False, (
"must not run user input through a shell"
@ -54,7 +54,7 @@ async def test_make_local_call_passes_args_without_a_shell(capture_exec):
assert argv is not None, "create_subprocess_exec was not called"
# the whole bolt11 value, metacharacters and all, must arrive as one
# discrete argv token so the shell never sees it
assert f"bolt11={payload}" in argv
assert f"string={payload}" in argv
assert argv[0] == "lightning-cli"

View file

@ -125,3 +125,87 @@ async def test_cln_grpc_wrong_network_returns_400(monkeypatch):
await node.decode_pay_request("lnbcrt500u1pjvvyly-grpc")
assert exc.value.status_code == 400
# --- CLN decodepay removal ----------------------------------------------------
# CLN deprecated `decodepay` in v24.11, disables it by default in v25.12
# (JSON-RPC -32601 'Command "decodepay" is deprecated') and removes it in
# v26.04. Calling it turned every Transactions load into a 500; both CLN
# backends must use `decode` instead.
_DECODE_RESULT = {
"type": "bolt11 invoice",
"valid": True,
"currency": "bc",
"created_at": 1759640918,
"expiry": 3599,
"payee": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e",
"amount_msat": 100000,
"description": "test",
"min_final_cltv_expiry": 80,
"payment_secret": (
"097795ffadc7849ab82737572eb3dd36c5dc5d17277effcdf2de709c07cd4a9a"
),
"payment_hash": (
"4620a5027133dd4f049df5e6c2024368e7ed804e9ad759feb349af2c2f60cde6"
),
}
async def test_cln_jrpc_uses_decode_not_decodepay():
node = LnNodeCLNjRPC()
seen = {}
async def fake_send(method, params=None):
seen["method"] = method
return {"result": dict(_DECODE_RESULT)}
node._send_request = fake_send
req = await node.decode_pay_request("lnbc100u1p-jrpc-decode")
assert seen["method"] == "decode"
assert req.payment_hash == _DECODE_RESULT["payment_hash"]
async def test_cln_jrpc_decode_valid_false_returns_400():
# unlike decodepay, decode reports recognized-but-invalid strings as a
# normal result with valid=false + warning_* fields instead of an error
node = LnNodeCLNjRPC()
async def fake_send(method, params=None):
return {
"result": {
"type": "bolt12 invoice",
"valid": False,
"warning_invoice_missing_amount": "invoice without an amount",
}
}
node._send_request = fake_send
with pytest.raises(HTTPException) as exc:
await node.decode_pay_request("lni1-invalid")
assert exc.value.status_code == 400
assert "invoice without an amount" in str(exc.value.detail)
async def test_cln_grpc_uses_decode_not_decodepay(monkeypatch):
import json
from app.lightning.impl import cln_grpc
node = LnNodeCLNgRPC()
seen = {}
async def fake_call(*args):
seen["args"] = args
return (json.dumps(_DECODE_RESULT).encode(), b"")
monkeypatch.setattr(cln_grpc, "_make_local_call", fake_call)
req = await node.decode_pay_request("lnbc100u1p-grpc-decode")
assert seen["args"] == ("decode", "string=lnbc100u1p-grpc-decode")
assert req.payment_hash == _DECODE_RESULT["payment_hash"]