Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions app/lightning/impl/cln_grpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -551,7 +551,7 @@ async def add_invoice(
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(
Expand All @@ -569,7 +569,21 @@ async def decode_pay_request(self, pay_req: str) -> PaymentRequest:

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:
Expand Down
14 changes: 13 additions & 1 deletion app/lightning/impl/cln_jrpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -467,10 +467,22 @@ async def decode_pay_request(self, pay_req: str) -> PaymentRequest:
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
Expand Down
4 changes: 2 additions & 2 deletions tests/test_cln_shell_safety.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"


Expand Down
84 changes: 84 additions & 0 deletions tests/test_decode_pay_request_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,87 @@ async def fake_call(*args):
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"]