tests: Add tests for extra_tlvs in hook
What changed, and why it matters
This commit only adds new automated tests and a test plugin. It does not change any production code, so it cannot by itself introduce a security vulnerability or fix one. The tests exercise an existing feature that lets plugins attach custom data fields to forwarded Lightning payments, and verify that malformed data triggers a 'BROKEN' log warning.
No security action required; treat as routine test coverage. Review the related production feature separately if concerned about plugin-controlled TLV injection.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The diff adds a test plugin (tests/plugins/htlc_accepted-customtlv.py) and two test cases (test_htlc_accepted_hook_customtlvs and test_htlc_accepted_hook_malformedtlvs) in tests/test_plugin.py. The tests validate that an htlc_accepted_hook can return extra_tlvs that replace update_add_htlc_tlvs on forwarded HTLCs, and that a malformed TLV stream causes a BROKEN log line. No core daemon, library, or protocol code is modified.
Changed components
tests/plugins/htlc_accepted-customtlv.pytests/test_plugin.pyInspect captured patch +80 / −0
diff --git a/tests/plugins/htlc_accepted-customtlv.py b/tests/plugins/htlc_accepted-customtlv.py
new file mode 100755
index 00000000..8358ad2b
--- /dev/null
+++ b/tests/plugins/htlc_accepted-customtlv.py
@@ -0,0 +1,38 @@
+#!/usr/bin/env python3
+"""A simply plugin that returns a custom tlv stream (byte encoded) to be
+attached to a forwarding HTLC.
+"""
+
+from pyln.client import Plugin
+
+
+plugin = Plugin()
+custom_tlvs = None
+
+
+@plugin.hook("htlc_accepted")
+def on_htlc_accepted(htlc, onion, plugin, **kwargs):
+ if 'extra_tlvs' in htlc:
+ print(f"called htlc accepted hook with extra_tlvs: {htlc['extra_tlvs']}")
+ print(f'returning continue with custom extra_tlvs: {custom_tlvs}')
+ if custom_tlvs:
+ return {"result": "continue", "extra_tlvs": custom_tlvs}
+ return {"result": "continue"}
+
+
+@plugin.method("setcustomtlvs")
+def setcustomtlvs(plugin, tlvs):
+ """Sets the custom tlv to return when receiving an incoming HTLC.
+ """
+ global custom_tlvs
+ print(f'setting custom tlv to {tlvs}')
+ custom_tlvs = tlvs
+
+
+@plugin.init()
+def on_init(**kwargs):
+ global custom_tlvs
+ custom_tlvs = None
+
+
+plugin.run()
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
index ece9a020..d187ac61 100644
--- a/tests/test_plugin.py
+++ b/tests/test_plugin.py
@@ -2419,6 +2419,48 @@ def test_htlc_accepted_hook_failmsg(node_factory):
l1.rpc.pay(inv)
+def test_htlc_accepted_hook_customtlvs(node_factory):
+ """ Passes an custom extra tlv field to the hooks return that should be set
+ as the `update_add_htlc_tlvs` in the `update_add_htlc` message on
+ forwards.
+ """
+ plugin = os.path.join(os.path.dirname(__file__), 'plugins/htlc_accepted-customtlv.py')
+ l1, l2, l3 = node_factory.line_graph(3, opts=[{}, {'plugin': plugin}, {'plugin': plugin}], wait_for_announce=True)
+
+ # Single tlv - Check that we receive the extra tlv at l3 attached by l2.
+ single_tlv = "fe00010001012a" # represents type: 65537, lenght: 1, value: 42
+ l2.rpc.setcustomtlvs(tlvs=single_tlv)
+ inv = l3.rpc.invoice(1000, 'customtlvs-singletlv', '')['bolt11']
+ l1.rpc.pay(inv)
+ l3.daemon.wait_for_log(f"called htlc accepted hook with extra_tlvs: {single_tlv}")
+
+ # Mutliple tlvs - Check that we recieve multiple extra tlvs at l3 attached by l2.
+ multi_tlv = "fdffff012afe00010001020539" # represents type: 65535, length: 1, value: 42 and type: 65537, length: 2, value: 1337
+ l2.rpc.setcustomtlvs(tlvs=multi_tlv)
+ inv = l3.rpc.invoice(1000, 'customtlvs-multitlvs', '')['bolt11']
+ l1.rpc.pay(inv)
+ l3.daemon.wait_for_log(f"called htlc accepted hook with extra_tlvs: {multi_tlv}")
+
+
+def test_htlc_accepted_hook_malformedtlvs(node_factory):
+ """ Passes an custom extra tlv field to the hooks return that is malformed
+ and should cause a broken log.
+ l1 -- l2 -- l3
+ """
+ plugin = os.path.join(os.path.dirname(__file__), 'plugins/htlc_accepted-customtlv.py')
+ l1, l2, l3 = node_factory.line_graph(3, opts=[{}, {'plugin': plugin, 'broken_log': "lightningd: ", 'may_fail': True}, {}], wait_for_announce=True)
+
+ mal_tlv = "fe00010001020539fdffff012a" # is malformed, types are 65537 and 65535 not in asc order.
+ l2.rpc.setcustomtlvs(tlvs=mal_tlv)
+ inv = l3.rpc.invoice(1000, 'customtlvs-maltlvs', '')
+ phash = inv['payment_hash']
+ route = l1.rpc.getroute(l3.info['id'], 1000, 1)['route']
+
+ # Here shouldn't use `pay` command because l2 should fail with a broken log.
+ l1.rpc.sendpay(route, phash, payment_secret=inv['payment_secret'])
+ assert l2.daemon.wait_for_log("BROKEN.*htlc_accepted_hook returned bad extra_tlvs")
+
+
def test_hook_dep(node_factory):
dep_a = os.path.join(os.path.dirname(__file__), 'plugins/dep_a.py')
dep_b = os.path.join(os.path.dirname(__file__), 'plugins/dep_b.py')
Why this scored 15/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.