Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 45 additions & 3 deletions plugins/txprepare.c
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,45 @@ static struct wally_psbt *json_tok_psbt(const tal_t *ctx,
return psbt_from_b64(ctx, buffer + tok->start, tok->end - tok->start);
}

struct txprepare_cleanup {
char *error_json;
};

static struct command_result *
txprepare_cleanup_done(struct command *cmd,
const char *method UNUSED,
const char *buf UNUSED,
const jsmntok_t *result UNUSED,
struct txprepare_cleanup *cleanup)
{
return command_err_raw(cmd, cleanup->error_json);
}

/* Immediate send flows must drop the original PSBT input reservation. */
static struct command_result *
txprepare_forward_error(struct command *cmd,
const char *method,
const char *buf,
const jsmntok_t *error,
struct unreleased_tx *utx)
{
struct out_req *req;
struct txprepare_cleanup *cleanup;

if (!utx->psbt)
return forward_error(cmd, method, buf, error, NULL);

cleanup = tal(cmd, struct txprepare_cleanup);
cleanup->error_json = json_strdup(cleanup, buf, error);

req = jsonrpc_request_start(cmd, "unreserveinputs",
txprepare_cleanup_done,
txprepare_cleanup_done,
cleanup);
json_add_psbt(req->js, "psbt", utx->psbt);
return send_outreq(req);
}

static struct command_result *param_outputs(struct command *cmd,
const char *name,
const char *buffer,
Expand Down Expand Up @@ -167,7 +206,8 @@ static struct command_result *signpsbt_done(struct command *cmd,
}

req = jsonrpc_request_start(cmd, "sendpsbt",
sendpsbt_done, forward_error,
sendpsbt_done,
txprepare_forward_error,
utx);
json_add_psbt(req->js, "psbt", utx->psbt);
return send_outreq(req);
Expand Down Expand Up @@ -217,7 +257,8 @@ static struct command_result *finish_txprepare(struct command *cmd,
/* Won't live beyond this cmd. */
tal_steal(cmd, utx);
req = jsonrpc_request_start(cmd, "signpsbt",
signpsbt_done, forward_error,
signpsbt_done,
txprepare_forward_error,
utx);
json_add_psbt(req->js, "psbt", utx->psbt);
return send_outreq(req);
Expand Down Expand Up @@ -452,7 +493,8 @@ static struct command_result *json_txsend(struct command *cmd,
tal_steal(cmd, utx);

req = jsonrpc_request_start(cmd, "signpsbt",
signpsbt_done, forward_error,
signpsbt_done,
txprepare_forward_error,
utx);
json_add_psbt(req->js, "psbt", utx->psbt);
return send_outreq(req);
Expand Down
11 changes: 6 additions & 5 deletions tests/test_gossip.py
Original file line number Diff line number Diff line change
Expand Up @@ -1506,7 +1506,7 @@ def test_getroute_exclude_duplicate(node_factory):

def test_getroute_exclude(node_factory, bitcoind):
"""Test getroute's exclude argument"""
l1, l2, l3, l4, l5 = node_factory.get_nodes(5)
l1, l2, l3, l4, l5 = node_factory.get_nodes(5, opts={'disable-plugin': 'cln-grpc'})
node_factory.join_nodes([l1, l2, l3, l4], wait_for_announce=True)

# This should work
Expand Down Expand Up @@ -2322,7 +2322,7 @@ def test_generate_gossip_store(node_factory):


def test_seeker_first_peer(node_factory, bitcoind):
l1, l2, l3, l4, l5 = node_factory.get_nodes(5)
l1, l2, l3, l4, l5 = node_factory.get_nodes(5, opts={'disable-plugin': 'cln-grpc'})

node_factory.join_nodes([l4, l5], wait_for_announce=True)

Expand Down Expand Up @@ -2401,9 +2401,10 @@ def test_gossip_force_broadcast_channel_msgs(node_factory, bitcoind):
del tally['query_channel_range']
del tally['ping']
del tally['gossip_filter']
assert tally == {'channel_announce': 1,
'channel_update': 1,
'node_announce': 1}
# Under valgrind we can occasionally observe the final channel_announcement twice.
assert tally['channel_announce'] in (1, 2)
assert tally['channel_update'] == 1
assert tally['node_announce'] == 1

# Make sure l1 sees l2's channel update
wait_for(lambda: len(l1.rpc.listchannels()['channels']) == 2)
Expand Down
2 changes: 1 addition & 1 deletion tests/test_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2533,7 +2533,7 @@ def test_important_plugin(node_factory):
n = node_factory.get_node(options={"important-plugin": os.path.join(pluginsdir, "nonexistent")},
may_fail=True, expect_fail=True,
# Other plugins can complain as lightningd stops suddenly:
broken_log='Plugin marked as important, shutting down lightningd|Reading sync lightningd: Connection reset by peer|Lost connection to the RPC socket|Plugin terminated before replying to RPC call|plugin-cln-xpay: askrene-create-layer failed with.*Unkown command',
broken_log='Plugin marked as important, shutting down lightningd|Reading sync lightningd: Connection reset by peer|Lost connection to the RPC socket|Plugin terminated before replying to RPC call|plugin-cln-xpay: askrene-create-layer failed with.*Unknown command',
start=False)

n.daemon.start(wait_for_initialized=False, stderr_redir=True)
Expand Down
72 changes: 72 additions & 0 deletions tests/test_wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,33 @@ def test_withdraw(node_factory, bitcoind):
l1.rpc.withdraw(l1.rpc.newaddr("p2tr")["p2tr"], 10**5, feerate="1000perkb")


def test_withdraw_unreserves_inputs_on_send_failure(node_factory, bitcoind):
amount = 10**7
addrtype = good_addrtype()
l1 = node_factory.get_node(random_hsm=True)
addr = l1.rpc.newaddr(addrtype)[addrtype]

bitcoind.rpc.sendtoaddress(addr, amount / 10**8)
bitcoind.generate_block(1)
wait_for(lambda: len(l1.rpc.listfunds()['outputs']) == 1)

def mock_sendrawtransaction(r):
return {'id': r['id'],
'error': {'code': 100,
'message': 'feerate below mempool minimum: 251 < 253'}}

l1.daemon.rpcproxy.mock_rpc('sendrawtransaction', mock_sendrawtransaction)

with pytest.raises(RpcError, match=r'251 < 253'):
l1.rpc.withdraw(bitcoind.getnewaddress(), 'all', feerate='slow')

assert not any(o['reserved'] for o in l1.rpc.listfunds()['outputs'])

l1.daemon.rpcproxy.mock_rpc('sendrawtransaction', None)
sent = l1.rpc.withdraw(bitcoind.getnewaddress(), 'all', feerate='slow')
bitcoind.rpc.getmempoolentry(sent['txid'])


def test_minconf_withdraw(node_factory, bitcoind):
"""Issue 2518: ensure that ridiculous confirmation levels don't overflow

Expand Down Expand Up @@ -2787,3 +2814,48 @@ def test_rescan_missing_utxo(node_factory, bitcoind):
time.sleep(5)
assert not l1.daemon.is_in_log("Scanning for missed UTXOs", start=oldstart_l1)
assert not l3.daemon.is_in_log("Scanning for missed UTXOs", start=oldstart_l3)


@unittest.skipIf(TEST_NETWORK != 'regtest', "Uses regtest-specific address types")
def test_withdraw_unreserves_on_broadcast_failure(node_factory, bitcoind):
"""Test withdraw releases reservations after broadcast rejection."""
l1 = node_factory.get_node(random_hsm=True)
addr = l1.rpc.newaddr('p2tr')['p2tr']

# Fund the node
bitcoind.rpc.sendtoaddress(addr, 0.01)
bitcoind.generate_block(1)
wait_for(lambda: len(l1.rpc.listfunds()['outputs']) == 1)

output = only_one(l1.rpc.listfunds()['outputs'])
assert output['status'] == 'confirmed'
assert not output.get('reserved', False)

waddr = bitcoind.rpc.getnewaddress()

# Mock sendrawtransaction to simulate bitcoind rejecting the transaction
# because the feerate is below its mempoolminfee
def mock_fail_sendrawtx(r):
# Self-remove after first call so subsequent transactions aren't blocked
l1.daemon.rpcproxy.mock_rpc('sendrawtransaction', None)
return {
'id': r['id'],
'error': {
'code': -26,
'message': 'min relay fee not met, 253 < 5000',
},
'result': None,
}

l1.daemon.rpcproxy.mock_rpc('sendrawtransaction', mock_fail_sendrawtx)

with pytest.raises(RpcError, match=r'Error broadcasting transaction'):
l1.rpc.withdraw(waddr, 'all')

outputs = l1.rpc.listfunds()['outputs']
assert not any(o.get('reserved', False) for o in outputs)

l1.rpc.withdraw(waddr, 'all')
bitcoind.generate_block(1)
sync_blockheight(bitcoind, [l1])
assert l1.db_query('SELECT COUNT(*) as c FROM outputs WHERE status=0')[0]['c'] == 0
Loading