diff --git a/contrib/pyln-client/pyln/client/lightning.py b/contrib/pyln-client/pyln/client/lightning.py index 230f8be64..63b15bac2 100644 --- a/contrib/pyln-client/pyln/client/lightning.py +++ b/contrib/pyln-client/pyln/client/lightning.py @@ -997,6 +997,16 @@ class LightningRpc(UnixDomainSocketRpc): } return self.call("waitanyinvoice", payload) + def waitblockheight(self, blockheight, timeout=None): + """ + Wait for the blockchain to reach the specified block height. + """ + payload = { + "blockheight": blockheight, + "timeout": timeout + } + return self.call("waitblockheight", payload) + def waitinvoice(self, label): """ Wait for an incoming payment matching the invoice with {label} diff --git a/doc/Makefile b/doc/Makefile index 80a9c3692..db2b30029 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -42,6 +42,7 @@ MANPAGES := doc/lightning-cli.1 \ doc/lightning-txsend.7 \ doc/lightning-waitinvoice.7 \ doc/lightning-waitanyinvoice.7 \ + doc/lightning-waitblockheight.7 \ doc/lightning-waitsendpay.7 \ doc/lightning-withdraw.7 diff --git a/doc/index.rst b/doc/index.rst index 1eab0f613..d54b644f3 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -27,8 +27,8 @@ c-lightning Documentation :maxdepth: 1 :caption: Manpages - lightningd lightningd-config + lightningd lightning-autocleaninvoice lightning-check lightning-checkmessage @@ -64,6 +64,7 @@ c-lightning Documentation lightning-txprepare lightning-txsend lightning-waitanyinvoice + lightning-waitblockheight lightning-waitinvoice lightning-waitsendpay lightning-withdraw diff --git a/doc/lightning-waitblockheight.7 b/doc/lightning-waitblockheight.7 new file mode 100644 index 000000000..72f88f796 --- /dev/null +++ b/doc/lightning-waitblockheight.7 @@ -0,0 +1,36 @@ +.TH "LIGHTNING-WAITBLOCKHEIGHT" "7" "" "" "lightning-waitblockheight" +.SH NAME +lightning-waitblockheight -- Command for waiting for blocks on the blockchain + +lightning-waitblockheight -- Command for waiting for blocks on the blockchain + +.SH SYNOPSIS + +\fBwaitblockheight\fR \fIblockheight\fR [\fItimeout\fR] + +.SH DESCRIPTION + +The \fBwaitblockheight\fR RPC command waits until the blockchain +has reached the specified \fIblockheight\fR. +It will only wait up to \fItimeout\fR seconds (default 60). + +If the \fIblockheight\fR is a present or past block height, then this +command returns immediately. + +.SH RETURN VALUE + +Once the specified block height has been achieved by the blockchain, +an object with the single field \fIblockheight\fR is returned, which is +the block height at the time the command returns. + +If \fItimeout\fR seconds is reached without the specified blockheight +being reached, this command will fail. + +.SH AUTHOR + +ZmnSCPxj <\fIZmnSCPxj@protonmail.com\fR> is mainly responsible. + +.SH RESOURCES + +Main web site: \fIhttps://github.com/ElementsProject/lightning\fR + diff --git a/doc/lightning-waitblockheight.7.md b/doc/lightning-waitblockheight.7.md new file mode 100644 index 000000000..0e260b6f1 --- /dev/null +++ b/doc/lightning-waitblockheight.7.md @@ -0,0 +1,37 @@ +lightning-waitblockheight -- Command for waiting for blocks on the blockchain +============================================================================= + +SYNOPSIS +-------- + +**waitblockheight** *blockheight* \[*timeout*\] + +DESCRIPTION +----------- + +The **waitblockheight** RPC command waits until the blockchain +has reached the specified *blockheight*. +It will only wait up to *timeout* seconds (default 60). + +If the *blockheight* is a present or past block height, then this +command returns immediately. + +RETURN VALUE +------------ + +Once the specified block height has been achieved by the blockchain, +an object with the single field *blockheight* is returned, which is +the block height at the time the command returns. + +If *timeout* seconds is reached without the specified blockheight +being reached, this command will fail. + +AUTHOR +------ + +ZmnSCPxj <> is mainly responsible. + +RESOURCES +--------- + +Main web site: diff --git a/lightningd/lightningd.c b/lightningd/lightningd.c index 0c5499e38..f8247afc6 100644 --- a/lightningd/lightningd.c +++ b/lightningd/lightningd.c @@ -185,6 +185,7 @@ static struct lightningd *new_lightningd(const tal_t *ctx) list_head_init(&ld->sendpay_commands); list_head_init(&ld->close_commands); list_head_init(&ld->ping_commands); + list_head_init(&ld->waitblockheight_commands); /*~ Tal also explicitly supports arrays: it stores the number of * elements, which can be accessed with tal_count() (or tal_bytelen() @@ -597,6 +598,7 @@ void notify_new_block(struct lightningd *ld, u32 block_height) htlcs_notify_new_block(ld, block_height); channel_notify_new_block(ld, block_height); gossip_notify_new_block(ld, block_height); + waitblockheight_notify_new_block(ld, block_height); } static void on_sigint(int _ UNUSED) diff --git a/lightningd/lightningd.h b/lightningd/lightningd.h index adfa8b573..240fa2e37 100644 --- a/lightningd/lightningd.h +++ b/lightningd/lightningd.h @@ -251,6 +251,9 @@ struct lightningd { bool encrypted_hsm; mode_t initial_umask; + + /* Outstanding waitblockheight commands. */ + struct list_head waitblockheight_commands; }; /* Turning this on allows a tal allocation to return NULL, rather than aborting. diff --git a/lightningd/peer_control.c b/lightningd/peer_control.c index 1e1bc98c4..4116dae7b 100644 --- a/lightningd/peer_control.c +++ b/lightningd/peer_control.c @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -1750,6 +1751,112 @@ static const struct json_command getinfo_command = { }; AUTODATA(json_command, &getinfo_command); +/* Wait for at least a specific blockheight, then return, or time out. */ +struct waitblockheight_waiter { + /* struct lightningd::waitblockheight_commands. */ + struct list_node list; + /* Command structure. This is the parent of the close command. */ + struct command *cmd; + /* The block height being waited for. */ + u32 block_height; + /* Whether we have been removed from the list. */ + bool removed; +}; +/* Completes a pending waitblockheight. */ +static struct command_result * +waitblockheight_complete(struct command *cmd, + u32 block_height) +{ + struct json_stream *response; + + response = json_stream_success(cmd); + json_add_num(response, "blockheight", block_height); + return command_success(cmd, response); +} +/* Called when command is destroyed without being resolved. */ +static void +destroy_waitblockheight_waiter(struct waitblockheight_waiter *w) +{ + if (!w->removed) + list_del(&w->list); +} +/* Called on timeout. */ +static void +timeout_waitblockheight_waiter(struct waitblockheight_waiter *w) +{ + list_del(&w->list); + w->removed = true; + tal_steal(tmpctx, w); + was_pending(command_fail(w->cmd, LIGHTNINGD, + "Timed out.")); +} +/* Called by lightningd at each new block. */ +void waitblockheight_notify_new_block(struct lightningd *ld, + u32 block_height) +{ + struct waitblockheight_waiter *w, *n; + char *to_delete = tal(NULL, char); + + /* Use safe since we could resolve commands and thus + * trigger removal of list elements. + */ + list_for_each_safe(&ld->waitblockheight_commands, w, n, list) { + /* Skip commands that have not been reached yet. */ + if (w->block_height > block_height) + continue; + + list_del(&w->list); + w->removed = true; + tal_steal(to_delete, w); + was_pending(waitblockheight_complete(w->cmd, + block_height)); + } + tal_free(to_delete); +} +static struct command_result *json_waitblockheight(struct command *cmd, + const char *buffer, + const jsmntok_t *obj, + const jsmntok_t *params) +{ + unsigned int *target_block_height; + u32 block_height; + unsigned int *timeout; + struct waitblockheight_waiter *w; + + if (!param(cmd, buffer, params, + p_req("blockheight", param_number, &target_block_height), + p_opt_def("timeout", param_number, &timeout, 60), + NULL)) + return command_param_failed(); + + /* Check if already reached anyway. */ + block_height = get_block_height(cmd->ld->topology); + if (*target_block_height <= block_height) + return waitblockheight_complete(cmd, block_height); + + /* Create a new waitblockheight command. */ + w = tal(cmd, struct waitblockheight_waiter); + tal_add_destructor(w, &destroy_waitblockheight_waiter); + list_add(&cmd->ld->waitblockheight_commands, &w->list); + w->cmd = cmd; + w->block_height = *target_block_height; + w->removed = false; + /* Install the timeout. */ + (void) new_reltimer(cmd->ld->timers, w, time_from_sec(*timeout), + &timeout_waitblockheight_waiter, w); + + return command_still_pending(cmd); +} + +static const struct json_command waitblockheight_command = { + "waitblockheight", + "utility", + &json_waitblockheight, + "Wait for the blockchain to reach {blockheight}, up to " + "{timeout} seconds." +}; +AUTODATA(json_command, &waitblockheight_command); + static struct command_result *param_channel_or_all(struct command *cmd, const char *name, const char *buffer, diff --git a/lightningd/peer_control.h b/lightningd/peer_control.h index 68e8fb60c..100756699 100644 --- a/lightningd/peer_control.h +++ b/lightningd/peer_control.h @@ -95,4 +95,8 @@ struct htlc_in_map *load_channels_from_wallet(struct lightningd *ld); void peer_dev_memleak(struct command *cmd); #endif /* DEVELOPER */ +/* Triggered at each new block. */ +void waitblockheight_notify_new_block(struct lightningd *ld, + u32 block_height); + #endif /* LIGHTNING_LIGHTNINGD_PEER_CONTROL_H */ diff --git a/lightningd/test/run-find_my_abspath.c b/lightningd/test/run-find_my_abspath.c index deb55d493..9534db7d0 100644 --- a/lightningd/test/run-find_my_abspath.c +++ b/lightningd/test/run-find_my_abspath.c @@ -192,6 +192,9 @@ bool wallet_network_check(struct wallet *w UNNEEDED) /* Generated stub for wallet_new */ struct wallet *wallet_new(struct lightningd *ld UNNEEDED, struct timers *timers UNNEEDED) { fprintf(stderr, "wallet_new called!\n"); abort(); } +/* Generated stub for waitblockheight_notify_new_block */ +void waitblockheight_notify_new_block(struct lightningd *ld UNNEEDED, u32 blockheight UNNEEDED) +{ fprintf(stderr, "waitblockheight_notify_new_block called!\n"); abort(); } /* AUTOGENERATED MOCKS END */ struct log *crashlog; diff --git a/tests/test_misc.py b/tests/test_misc.py index a0b4e4bde..ea40d3e10 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -1993,7 +1993,7 @@ def test_new_node_is_mainnet(node_factory): assert os.path.isfile(os.path.join(basedir, "lightningd-bitcoin.pid")) -def test_unicode_rpc(node_factory): +def test_unicode_rpc(node_factory, executor, bitcoind): node = node_factory.get_node() desc = "Some candy 🍬 and a nice glass of milk 🥛." @@ -2019,3 +2019,41 @@ def test_unix_socket_path_length(node_factory, bitcoind, directory, executor, db # Let's just call it again to make sure it really works. l1.rpc.listconfigs() l1.stop() + + +def test_waitblockheight(node_factory, executor, bitcoind): + node = node_factory.get_node() + + sync_blockheight(bitcoind, [node]) + + blockheight = node.rpc.getinfo()['blockheight'] + + # Should succeed without waiting. + node.rpc.waitblockheight(blockheight - 2) + node.rpc.waitblockheight(blockheight - 1) + node.rpc.waitblockheight(blockheight) + + # Should not succeed yet. + fut2 = executor.submit(node.rpc.waitblockheight, blockheight + 2) + fut1 = executor.submit(node.rpc.waitblockheight, blockheight + 1) + assert not fut1.done() + assert not fut2.done() + + # Should take about ~1second and time out. + with pytest.raises(RpcError): + node.rpc.waitblockheight(blockheight + 2, 1) + + # Others should still not be done. + assert not fut1.done() + assert not fut2.done() + + # Trigger just one more block. + bitcoind.generate_block(1) + sync_blockheight(bitcoind, [node]) + fut1.result(5) + assert not fut2.done() + + # Trigger two blocks. + bitcoind.generate_block(1) + sync_blockheight(bitcoind, [node]) + fut2.result(5)