diff --git a/tests/test_pay.py b/tests/test_pay.py index ca10585a2536..6aaf84b1f13d 100644 --- a/tests/test_pay.py +++ b/tests/test_pay.py @@ -2716,3 +2716,57 @@ def test_partial_payment_htlc_loss(node_factory, bitcoind): with pytest.raises(RpcError, match=r'WIRE_PERMANENT_CHANNEL_FAILURE \(reply from remote\)'): l1.rpc.waitsendpay(payment_hash=inv['payment_hash'], timeout=TIMEOUT, partid=1) + + +@pytest.mark.xfail(strict=True) +@unittest.skipIf(not DEVELOPER, "needs use_shadow") +def test_blockheight_disagreement(node_factory, bitcoind, executor): + """ + While a payment is in-transit from payer to payee, a block + might be mined, so that the blockheight the payer used to + initiate the payment is no longer the blockheight when the + payee receives it. + This leads to a failure which *used* to be + `final_expiry_too_soon`, a non-permanent failure, but + which is *now* `incorrect_or_unknown_payment_details`, + a permanent failure. + `pay` treats permanent failures as, well, permanent, and + gives up on receiving such failure from the payee, but + this particular subcase of blockheight disagreement is + actually a non-permanent failure (the payer only needs + to synchronize to the same blockheight as the payee). + """ + l1, l2 = node_factory.line_graph(2) + + sync_blockheight(bitcoind, [l1, l2]) + + # Arrange l1 to stop getting new blocks. + def no_more_blocks(req): + return {"result": None, + "error": {"code": -8, "message": "Block height out of range"}, "id": req['id']} + l1.daemon.rpcproxy.mock_rpc('getblockhash', no_more_blocks) + + # Increase blockheight and make sure l2 knows it. + # Why 2? Because `pay` uses min_final_cltv_expiry + 1. + # But 2 blocks coming in close succession, plus slow + # forwarding nodes and block propagation, are still + # possible on the mainnet, thus this test. + bitcoind.generate_block(2) + sync_blockheight(bitcoind, [l2]) + + # Have l2 make an invoice. + inv = l2.rpc.invoice(1000, 'l', 'd')['bolt11'] + + # Have l1 pay l2 + def pay(l1, inv): + l1.rpc.dev_pay(inv, use_shadow=False) + fut = executor.submit(pay, l1, inv) + + # Make sure l1 sends out the HTLC. + l1.daemon.wait_for_logs([r'NEW:: HTLC LOCAL']) + + # Unblock l1 from new blocks. + l1.daemon.rpcproxy.mock_rpc('getblockhash', None) + + # pay command should complete without error + fut.result()