Skip to content

Commit

Permalink
openchannel: test new hook chainable mechanics
Browse files Browse the repository at this point in the history
  • Loading branch information
m-schmoock committed Sep 3, 2020
1 parent 441e7f1 commit dbbad65
Show file tree
Hide file tree
Showing 6 changed files with 169 additions and 38 deletions.
35 changes: 0 additions & 35 deletions tests/plugins/accepter_close_to.py

This file was deleted.

19 changes: 19 additions & 0 deletions tests/plugins/openchannel_hook_accept.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/usr/bin/env python3
"""Plugin to test openchannel_hook
Will simply acceppt any channel. Useful fot tetsing chained hook.
"""

from pyln.client import Plugin

plugin = Plugin()


@plugin.hook('openchannel')
def on_openchannel(openchannel, plugin, **kwargs):
msg = "accept on principle"
plugin.log(msg)
return {'result': 'continue'}


plugin.run()
51 changes: 51 additions & 0 deletions tests/plugins/openchannel_hook_accepter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
"""Simple plugin to test the openchannel_hook's
'close_to' address functionality.
If the funding amount is:
- 100005sat: we reject correctly w/o close_to
- 100004sat: we reject invalid by setting a close_to
- 100003sat: we send back a valid address (regtest)
- 100002sat: we send back an empty address
- 100001sat: we send back an address for the wrong chain (mainnet)
- otherwise: we don't include the close_to
"""

from pyln.client import Plugin, Millisatoshi

plugin = Plugin()


@plugin.hook('openchannel')
def on_openchannel(openchannel, plugin, **kwargs):
# - 100005sat: we reject correctly w/o close_to
if Millisatoshi(openchannel['funding_satoshis']).to_satoshi() == 100005:
msg = "reject for a reason"
plugin.log(msg)
return {'result': 'reject', 'error_message': msg}

# - 100004sat: we reject invalid by setting a close_to
if Millisatoshi(openchannel['funding_satoshis']).to_satoshi() == 100004:
msg = "I am a broken plugin"
plugin.log(msg)
return {'result': 'reject', 'error_message': msg,
'close_to': "bcrt1q7gtnxmlaly9vklvmfj06amfdef3rtnrdazdsvw"}

# - 100003sat: we send back a valid address (regtest)
if Millisatoshi(openchannel['funding_satoshis']).to_satoshi() == 100003:
return {'result': 'continue', 'close_to': 'bcrt1q7gtnxmlaly9vklvmfj06amfdef3rtnrdazdsvw'}

# - 100002sat: we send back an empty address
if Millisatoshi(openchannel['funding_satoshis']).to_satoshi() == 100002:
return {'result': 'continue', 'close_to': ''}

# - 100001sat: we send back an address for the wrong chain (mainnet)
if Millisatoshi(openchannel['funding_satoshis']).to_satoshi() == 100001:
return {'result': 'continue', 'close_to': 'bc1qlq8srqnz64wgklmqvurv7qnr4rvtq2u96hhfg2'}

# - otherwise: accept and don't include the close_to
plugin.log("accept by design")
return {'result': 'continue'}


plugin.run()
20 changes: 20 additions & 0 deletions tests/plugins/openchannel_hook_reject.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/usr/bin/env python3
"""Plugin to test openchannel_hook
Will simply reject any channel with message "reject on principle".
Useful fot tetsing chained hook.
"""

from pyln.client import Plugin

plugin = Plugin()


@plugin.hook('openchannel')
def on_openchannel(openchannel, plugin, **kwargs):
msg = "reject on principle"
plugin.log(msg)
return {'result': 'reject', 'error_message': msg}


plugin.run()
6 changes: 3 additions & 3 deletions tests/test_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -1050,13 +1050,13 @@ def test_funding_cancel_race(node_factory, bitcoind, executor):
def test_funding_close_upfront(node_factory, bitcoind):
l1 = node_factory.get_node()

opts = {'plugin': os.path.join(os.getcwd(), 'tests/plugins/accepter_close_to.py')}
opts = {'plugin': os.path.join(os.getcwd(), 'tests/plugins/openchannel_hook_accepter.py')}
l2 = node_factory.get_node(options=opts)

# The 'accepter_close_to' plugin uses the channel funding amount to determine
# whether or not to include a 'close_to' address
amt_normal = 100007 # continues without returning a close_to
amt_addr = 100001 # returns valid regtest address
amt_normal = 100000 # continues without returning a close_to
amt_addr = 100003 # returns valid regtest address

remote_valid_addr = 'bcrt1q7gtnxmlaly9vklvmfj06amfdef3rtnrdazdsvw'

Expand Down
76 changes: 76 additions & 0 deletions tests/test_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,82 @@ def test_openchannel_hook(node_factory, bitcoind):
l1.rpc.fundchannel(l2.info['id'], 100001)


def test_openchannel_hook_error_handling(node_factory, bitcoind):
""" l2 uses a plugin that should fatal() crash the node.
This is because the plugin rejects a channel while
also setting a close_to address which isn't allowed.
"""
opts = {'plugin': os.path.join(os.getcwd(), 'tests/plugins/openchannel_hook_accepter.py')}
# openchannel_reject_but_set_close_to.py')}
l1 = node_factory.get_node()
l2 = node_factory.get_node(options=opts,
expect_fail=True,
may_fail=True,
allow_broken_log=True)
l1.connect(l2)

# Get some funds.
addr = l1.rpc.newaddr()['bech32']
txid = bitcoind.rpc.sendtoaddress(addr, 10)
numfunds = len(l1.rpc.listfunds()['outputs'])
bitcoind.generate_block(1, txid)
wait_for(lambda: len(l1.rpc.listfunds()['outputs']) > numfunds)

# next fundchannel should fail fatal() for l2
with pytest.raises(RpcError, match=r'Owning subdaemon openingd died'):
l1.rpc.fundchannel(l2.info['id'], 100004)
assert l2.daemon.is_in_log("Plugin rejected openchannel but also set a close_to")


def test_openchannel_hook_chaining(node_factory, bitcoind):
""" l2 uses a set of plugin that all use the openchannel_hook.
We test that chaining works by using multiple plugins in a way
that we check for the first plugin that rejects prevents from evaluating
further plugin responses down the chain.
"""
opts = [{}, {'plugin': [
os.path.join(os.getcwd(), 'tests/plugins/openchannel_hook_accept.py'),
os.path.join(os.getcwd(), 'tests/plugins/openchannel_hook_accepter.py'),
os.path.join(os.getcwd(), 'tests/plugins/openchannel_hook_reject.py')
]}]
l1, l2 = node_factory.line_graph(2, fundchannel=False, opts=opts)

# Get some funds.
addr = l1.rpc.newaddr()['bech32']
txid = bitcoind.rpc.sendtoaddress(addr, 10)
numfunds = len(l1.rpc.listfunds()['outputs'])
bitcoind.generate_block(1, txid)
wait_for(lambda: len(l1.rpc.listfunds()['outputs']) > numfunds)

hook_msg = "openchannel_hook rejects and says '"
# 100005sat fundchannel should fail fatal() for l2
# because hook_accepter.py rejects on that amount 'for a reason'
with pytest.raises(RpcError, match=r'They sent error channel'):
l1.rpc.fundchannel(l2.info['id'], 100005)

# Note: hook chain order is currently undefined, because hooks are registerd
# as a result of the getmanifest call, which may take some random time.
# We need to workaround that fact, so test can be stable
correct_order = l2.daemon.is_in_log(hook_msg + "reject for a reason")
if correct_order:
assert l2.daemon.wait_for_log(hook_msg + "reject for a reason")
# the other plugin must not be called
assert not l2.daemon.is_in_log("reject on principle")
else:
assert l2.daemon.wait_for_log(hook_msg + "reject on principle")
# the other plugin must not be called
assert not l2.daemon.is_in_log("reject for a reason")

# 100000sat is good for hook_accepter, so it should fail 'on principle'
# at third hook openchannel_reject.py
with pytest.raises(RpcError, match=r'They sent error channel'):
l1.rpc.fundchannel(l2.info['id'], 100000)
assert l2.daemon.wait_for_log(hook_msg + "reject on principle")


@unittest.skipIf(not DEVELOPER, "without DEVELOPER=1, gossip v slow")
def test_htlc_accepted_hook_fail(node_factory):
"""Send payments from l1 to l2, but l2 just declines everything.
Expand Down

0 comments on commit dbbad65

Please sign in to comment.