From 490550d508b1efe70cefb0d28feb9b5c58d289b6 Mon Sep 17 00:00:00 2001 From: Christian Decker Date: Thu, 6 Feb 2020 17:35:18 +0100 Subject: [PATCH] plugin: Allow multiple plugins to register the `htlc_accepted` hook Make the `htlc_accepted` hook the first chained hook in our repertoire. The plugins are called one after the other in order until we have no more plugins or the HTLC was handled by one of the plugins. If no plugins handles the HTLC we continue to handle it internally like always. Handling in this case means the plugin returns either `{"result": "resolve", ...}` or `{"result": "fail", ...}`. Changelog-Changed: plugin: Multiple plugins can now register for the htlc_accepted hook. --- lightningd/peer_htlcs.c | 2 +- tests/plugins/hook-chain-even.py | 24 +++++++++++ tests/plugins/hook-chain-odd.py | 24 +++++++++++ tests/test_plugin.py | 73 ++++++++++++++++++++++++++++++++ 4 files changed, 122 insertions(+), 1 deletion(-) create mode 100755 tests/plugins/hook-chain-even.py create mode 100755 tests/plugins/hook-chain-odd.py diff --git a/lightningd/peer_htlcs.c b/lightningd/peer_htlcs.c index 9635475c5..116ffd622 100644 --- a/lightningd/peer_htlcs.c +++ b/lightningd/peer_htlcs.c @@ -873,7 +873,7 @@ htlc_accepted_hook_callback(struct htlc_accepted_hook_payload *request, tal_free(request); } -REGISTER_PLUGIN_HOOK(htlc_accepted, PLUGIN_HOOK_SINGLE, +REGISTER_PLUGIN_HOOK(htlc_accepted, PLUGIN_HOOK_CHAIN, htlc_accepted_hook_callback, struct htlc_accepted_hook_payload *, htlc_accepted_hook_serialize, diff --git a/tests/plugins/hook-chain-even.py b/tests/plugins/hook-chain-even.py new file mode 100755 index 000000000..59dda00d0 --- /dev/null +++ b/tests/plugins/hook-chain-even.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +from pyln.client import Plugin +from hashlib import sha256 +from binascii import hexlify + +"""A simple plugin that accepts invoices with "BB"*32 preimages +""" +plugin = Plugin() + + +@plugin.hook('htlc_accepted') +def on_htlc_accepted(htlc, plugin, **kwargs): + preimage = b"\xBB" * 32 + payment_hash = sha256(preimage).hexdigest() + preimage = hexlify(preimage).decode('ASCII') + print("htlc_accepted called for payment_hash {}".format(htlc['payment_hash'])) + + if htlc['payment_hash'] == payment_hash: + return {'result': 'resolve', 'payment_key': preimage} + else: + return {'result': 'continue'} + + +plugin.run() diff --git a/tests/plugins/hook-chain-odd.py b/tests/plugins/hook-chain-odd.py new file mode 100755 index 000000000..d1134fa8c --- /dev/null +++ b/tests/plugins/hook-chain-odd.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +from pyln.client import Plugin +from hashlib import sha256 +from binascii import hexlify + +"""A simple plugin that accepts invoices with "AA"*32 preimages +""" +plugin = Plugin() + + +@plugin.hook('htlc_accepted') +def on_htlc_accepted(htlc, plugin, **kwargs): + preimage = b"\xAA" * 32 + payment_hash = sha256(preimage).hexdigest() + preimage = hexlify(preimage).decode('ASCII') + print("htlc_accepted called for payment_hash {}".format(htlc['payment_hash'])) + + if htlc['payment_hash'] == payment_hash: + return {'result': 'resolve', 'payment_key': preimage} + else: + return {'result': 'continue'} + + +plugin.run() diff --git a/tests/test_plugin.py b/tests/test_plugin.py index d090d99a6..180b25261 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -1,6 +1,7 @@ from collections import OrderedDict from fixtures import * # noqa: F401,F403 from flaky import flaky # noqa: F401 +from hashlib import sha256 from pyln.client import RpcError, Millisatoshi from pyln.proto import Invoice from utils import ( @@ -859,3 +860,75 @@ def test_plugin_feature_announce(node_factory): # Check the featurebit set in the `node_announcement` node = l1.rpc.listnodes(l1.info['id'])['nodes'][0] assert(int(node['features'], 16) & (1 << 103) != 0) + + +def test_hook_chaining(node_factory): + """Check that hooks are called in order and the chain exits correctly + + We start two nodes, l2 will have two plugins registering the same hook + (`htlc_accepted`) but handle different cases: + + - the `odd` plugin only handles the "AA"*32 preimage + - the `even` plugin only handles the "BB"*32 preimage + + We check that plugins are called in the order they are registering the + hook, and that they exit the call chain as soon as one plugin returns a + result that isn't `continue`. On exiting the chain the remaining plugins + are not called. If no plugin exits the chain we continue to handle + internally as usual. + + """ + l1, l2 = node_factory.line_graph(2) + + # Start the plugins manually instead of specifying them on the command + # line, otherwise we cannot guarantee the order in which the hooks are + # registered. + p1 = os.path.join(os.path.dirname(__file__), "plugins/hook-chain-odd.py") + p2 = os.path.join(os.path.dirname(__file__), "plugins/hook-chain-even.py") + l2.rpc.plugin_start(p1) + l2.rpc.plugin_start(p2) + + preimage1 = b'\xAA' * 32 + preimage2 = b'\xBB' * 32 + preimage3 = b'\xCC' * 32 + hash1 = sha256(preimage1).hexdigest() + hash2 = sha256(preimage2).hexdigest() + hash3 = sha256(preimage3).hexdigest() + + inv = l2.rpc.invoice(123, 'odd', "Odd payment handled by the first plugin", + preimage="AA" * 32)['bolt11'] + l1.rpc.pay(inv) + + # The first plugin will handle this, the second one should not be called. + assert(l2.daemon.is_in_log( + r'plugin-hook-chain-odd.py: htlc_accepted called for payment_hash {}'.format(hash1) + )) + assert(not l2.daemon.is_in_log( + r'plugin-hook-chain-even.py: htlc_accepted called for payment_hash {}'.format(hash1) + )) + + # The second run is with a payment_hash that `hook-chain-even.py` knows + # about. `hook-chain-odd.py` is called, it returns a `continue`, and then + # `hook-chain-even.py` resolves it. + inv = l2.rpc.invoice( + 123, 'even', "Even payment handled by the second plugin", preimage="BB" * 32 + )['bolt11'] + l1.rpc.pay(inv) + assert(l2.daemon.is_in_log( + r'plugin-hook-chain-odd.py: htlc_accepted called for payment_hash {}'.format(hash2) + )) + assert(l2.daemon.is_in_log( + r'plugin-hook-chain-even.py: htlc_accepted called for payment_hash {}'.format(hash2) + )) + + # And finally an invoice that neither know about, so it should get settled + # by the internal invoice handling. + inv = l2.rpc.invoice(123, 'neither', "Neither plugin handles this", + preimage="CC" * 32)['bolt11'] + l1.rpc.pay(inv) + assert(l2.daemon.is_in_log( + r'plugin-hook-chain-odd.py: htlc_accepted called for payment_hash {}'.format(hash3) + )) + assert(l2.daemon.is_in_log( + r'plugin-hook-chain-even.py: htlc_accepted called for payment_hash {}'.format(hash3) + ))