pyln-client: adapt for modern plugin notifications.
What changed, and why it matters
This commit updates the Python plugin client library (pyln-client) so it understands both old and new styles of plugin notification messages from the lightningd daemon. It adds backward/forward compatibility shims so plugins written for either format still receive their expected 'payload' and 'origin' fields. The changes are in the client library and its tests; there is no direct fix for an exploitable vulnerability.
No immediate security action required. Treat as a normal compatibility update. If you maintain a pyln-client-based plugin, verify notification parameter handling against the new direct-params style described in the changelog.
Security signals we found
No security-relevant keywords in commit title or message
No bounds/overflow, auth, crypto, or permission changes
Change is compatibility/deprecation handling in plugin RPC client
Test-only fix: custom notification params changed from string to dict
Evidence from the diff
The patch modifies contrib/pyln-client/pyln/client/plugin.py to carry an ‘origin’ field on Request objects and to normalize notification parameters: if the incoming JSON uses the old wrapped style {‘payload’: {…}, origin: …}, it copies payload under the method name; if it uses the new unwrapped style {method: {…}, origin: …} and deprecated APIs are enabled, it recreates the legacy ‘payload’ key. Tests are updated to expect both representations and to stop passing a bare string as notification params. This is an API-compatibility change, not a security patch.
Changed components
contrib/pyln-client/pyln/client/plugin.pytests/plugins/custom_notifications.pytests/test_plugin.pyInspect captured patch +29 / −7
diff --git a/contrib/pyln-client/pyln/client/plugin.py b/contrib/pyln-client/pyln/client/plugin.py
index b55312c1..83ee0b5b 100644
--- a/contrib/pyln-client/pyln/client/plugin.py
+++ b/contrib/pyln-client/pyln/client/plugin.py
@@ -103,12 +103,13 @@ class Request(dict):
"""A request object that wraps params and allows async return
"""
def __init__(self, plugin: 'Plugin', req_id: Optional[str], method: str,
- params: Any, background: bool = False):
+ params: Any, background: bool = False, origin: Optional[str] = None):
self.method = method
self.params = params
self.background = background
self.plugin = plugin
self.state = RequestState.PENDING
+ self.origin = origin
self.id = req_id
self.termination_tb: Optional[str] = None
@@ -735,6 +736,23 @@ class Plugin(object):
else:
raise ValueError(f"No subscription for {request.method} found.")
+ # Old style:
+ # params: {payload: {...}, origin: "pluginname"}
+ # New style:
+ # origin: "pluginname", params: {method: {...}}
+
+ # Old style?
+ if 'payload' in request.params and request.method not in request.params:
+ request.params[request.method] = request.params['payload']
+ # New style?
+ elif 'payload' not in request.params and request.method in request.params and self.deprecated_apis:
+ # Create payload for older plugins, on modern systems.
+ request.params['payload'] = request.params[request.method]
+
+ # Always hoist origin into params:
+ if request.origin and 'origin' not in request.params:
+ request.params['origin'] = request.origin
+
try:
self._exec_func(func, request)
except Exception:
@@ -783,6 +801,7 @@ class Plugin(object):
req_id=jsrequest.get('id', None),
method=str(jsrequest['method']),
params=jsrequest['params'],
+ origin=jsrequest.get('origin'),
background=False,
)
return request
diff --git a/tests/plugins/custom_notifications.py b/tests/plugins/custom_notifications.py
index 8ece850d..e09942b8 100755
--- a/tests/plugins/custom_notifications.py
+++ b/tests/plugins/custom_notifications.py
@@ -14,14 +14,14 @@ def on_custom_notification(origin, payload, **kwargs):
def emit(plugin):
"""Emit a simple string notification to topic "custom"
"""
- plugin.notify("custom", "Hello world")
+ plugin.notify("custom", {'message': "Hello world"})
@plugin.method("faulty-emit")
def faulty_emit(plugin):
"""Emit a simple string notification to topic "custom"
"""
- plugin.notify("ididntannouncethis", "Hello world")
+ plugin.notify("ididntannouncethis", {'message': "Hello world"})
@plugin.subscribe("pay_success")
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
index 60c18a89..427085d0 100644
--- a/tests/test_plugin.py
+++ b/tests/test_plugin.py
@@ -2641,7 +2641,7 @@ def test_custom_notification_topics(node_factory):
)
l1, l2 = node_factory.line_graph(2, opts=[{'plugin': plugin}, {}])
l1.rpc.emit()
- l1.daemon.wait_for_log(r'Got a custom notification Hello world')
+ l1.daemon.wait_for_log("Got a custom notification {'message': 'Hello world'} from plugin custom_notifications.py")
inv = l2.rpc.invoice(42, "lbl", "desc")['bolt11']
l1.rpc.pay(inv)
@@ -4409,7 +4409,8 @@ def test_pay_plugin_notifications(node_factory, bitcoind, chainparams):
'timestamp': 0,
'enabled': True}
channel_hint_update = {'origin': 'pay',
- 'payload': {'channel_hint': channel_hint_update_core}}
+ 'payload': {'channel_hint': channel_hint_update_core},
+ 'channel_hint_update': {'channel_hint': channel_hint_update_core}}
assert data == channel_hint_update
# It gets a success notification
@@ -4420,7 +4421,8 @@ def test_pay_plugin_notifications(node_factory, bitcoind, chainparams):
'bolt11': inv1['bolt11']}
# Includes deprecated and modern. pyln-client plugin.py copies fields as necessary.
success = {'origin': 'pay',
- 'payload': success_core}
+ 'payload': success_core,
+ 'pay_success': success_core}
assert data == success
inv2 = l3.rpc.invoice(10000, "second", "desc")
@@ -4434,7 +4436,8 @@ def test_pay_plugin_notifications(node_factory, bitcoind, chainparams):
failure_core = {'payment_hash': inv2['payment_hash'], 'bolt11': inv2['bolt11'], 'error': {'message': 'failed: WIRE_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS (reply from remote)'}}
# Includes deprecated and modern.
failure = {'origin': 'pay',
- 'payload': failure_core}
+ 'payload': failure_core,
+ 'pay_failure': failure_core}
assert data == failure
Why this scored 19/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.