Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fetch block from a peer if we don't have it #7240

Merged
Merged
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
6 changes: 3 additions & 3 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ jobs:
timeout-minutes: 120
env:
BITCOIN_VERSION: "26.1"
ELEMENTS_VERSION: 22.0.2
ELEMENTS_VERSION: 23.2.1
RUST_PROFILE: release # Has to match the one in the compile step
PYTEST_OPTS: --timeout=1200 --force-flaky
needs:
Expand Down Expand Up @@ -320,7 +320,7 @@ jobs:
timeout-minutes: 120
env:
BITCOIN_VERSION: "26.1"
ELEMENTS_VERSION: 22.0.2
ELEMENTS_VERSION: 23.2.1
RUST_PROFILE: release # Has to match the one in the compile step
CFG: compile-gcc
PYTEST_OPTS: --test-group-random-seed=42 --timeout=1800 --force-flaky
Expand Down Expand Up @@ -390,7 +390,7 @@ jobs:
timeout-minutes: 120
env:
BITCOIN_VERSION: "26.1"
ELEMENTS_VERSION: 22.0.2
ELEMENTS_VERSION: 23.2.1
RUST_PROFILE: release
SLOW_MACHINE: 1
TEST_DEBUG: 1
Expand Down
160 changes: 140 additions & 20 deletions plugins/bcli.c
Original file line number Diff line number Diff line change
Expand Up @@ -560,9 +560,13 @@ struct getrawblock_stash {
const char *block_hash;
u32 block_height;
const char *block_hex;
int *peers;
};

static struct command_result *process_getrawblock(struct bitcoin_cli *bcli)
/* Mutual recursion. */
static struct command_result *getrawblock(struct bitcoin_cli *bcli);

static struct command_result *process_rawblock(struct bitcoin_cli *bcli)
{
struct json_stream *response;
struct getrawblock_stash *stash = bcli->stash;
Expand All @@ -577,6 +581,126 @@ static struct command_result *process_getrawblock(struct bitcoin_cli *bcli)
return command_finished(bcli->cmd, response);
}

static struct command_result *process_getblockfrompeer(struct bitcoin_cli *bcli)
{
/* Remove the peer that we tried to get the block from and move along,
* we may also check on errors here */
struct getrawblock_stash *stash = bcli->stash;

if (bcli->exitstatus && *bcli->exitstatus != 0) {
/* We still continue with the execution if we can not fetch the
* block from peer */
plugin_log(bcli->cmd->plugin, LOG_DBG,
"failed to fetch block %s from peer %i, skip.",
stash->block_hash, stash->peers[tal_count(stash->peers) - 1]);
} else {
plugin_log(bcli->cmd->plugin, LOG_DBG,
"try to fetch block %s from peer %i.",
stash->block_hash, stash->peers[tal_count(stash->peers) - 1]);
}
tal_resize(&stash->peers, tal_count(stash->peers) - 1);

/* `getblockfrompeer` is an async call. sleep for a second to allow the
* block to be delivered by the peer. fixme: We could also sleep for
* double the last ping here (with sanity limit)*/
sleep(1);

return getrawblock(bcli);
}

static struct command_result *process_getpeerinfo(struct bitcoin_cli *bcli)
{
const jsmntok_t *t, *toks;
struct getrawblock_stash *stash = bcli->stash;
size_t i;

toks =
json_parse_simple(bcli->output, bcli->output, bcli->output_bytes);

if (!toks) {
return command_err_bcli_badjson(bcli, "cannot parse");
}

stash->peers = tal_arr(bcli->stash, int, 0);

json_for_each_arr(i, t, toks)
{
int id;
if (json_scan(tmpctx, bcli->output, t, "{id:%}",
JSON_SCAN(json_to_int, &id)) == NULL) {
// fixme: future optimization: a) filter for full nodes,
// b) sort by last ping
tal_arr_expand(&stash->peers, id);
}
}

if (tal_count(stash->peers) <= 0) {
/* We don't have peers yet, retry from `getrawblock` */
plugin_log(bcli->cmd->plugin, LOG_DBG,
"got an empty peer list.");
return getrawblock(bcli);
}

start_bitcoin_cli(NULL, bcli->cmd, process_getblockfrompeer, true,
BITCOIND_HIGH_PRIO, stash, "getblockfrompeer",
stash->block_hash,
take(tal_fmt(NULL, "%i", stash->peers[0])), NULL);

return command_still_pending(bcli->cmd);
}

static struct command_result *process_getrawblock(struct bitcoin_cli *bcli)
{
/* We failed to get the raw block. */
if (bcli->exitstatus && *bcli->exitstatus != 0) {
struct getrawblock_stash *stash = bcli->stash;

plugin_log(bcli->cmd->plugin, LOG_DBG,
"failed to fetch block %s from the bitcoin backend (maybe pruned).",
stash->block_hash);

if (bitcoind->version >= 230000) {
rustyrussell marked this conversation as resolved.
Show resolved Hide resolved
/* `getblockformpeer` was introduced in v23.0.0 */

if (!stash->peers) {
/* We don't have peers to fetch blocks from, get
* some! */
start_bitcoin_cli(NULL, bcli->cmd,
process_getpeerinfo, true,
BITCOIND_HIGH_PRIO, stash,
"getpeerinfo", NULL);

return command_still_pending(bcli->cmd);
}

if (tal_count(stash->peers) > 0) {
/* We have peers left that we can ask for the
* block */
start_bitcoin_cli(
NULL, bcli->cmd, process_getblockfrompeer,
true, BITCOIND_HIGH_PRIO, stash,
"getblockfrompeer", stash->block_hash,
take(tal_fmt(NULL, "%i", stash->peers[0])),
NULL);

return command_still_pending(bcli->cmd);
}

/* We failed to fetch the block from from any peer we
* got. */
plugin_log(
bcli->cmd->plugin, LOG_DBG,
"asked all known peers about block %s, retry",
stash->block_hash);
stash->peers = tal_free(stash->peers);
}

return NULL;
}

return process_rawblock(bcli);
}

static struct command_result *
getrawblockbyheight_notfound(struct bitcoin_cli *bcli)
{
Expand All @@ -589,6 +713,19 @@ getrawblockbyheight_notfound(struct bitcoin_cli *bcli)
return command_finished(bcli->cmd, response);
}

static struct command_result *getrawblock(struct bitcoin_cli *bcli)
{
struct getrawblock_stash *stash = bcli->stash;

start_bitcoin_cli(NULL, bcli->cmd, process_getrawblock, true,
BITCOIND_HIGH_PRIO, stash, "getblock",
stash->block_hash,
/* Non-verbose: raw block. */
"0", NULL);

return command_still_pending(bcli->cmd);
}

static struct command_result *process_getblockhash(struct bitcoin_cli *bcli)
{
struct getrawblock_stash *stash = bcli->stash;
Expand All @@ -607,15 +744,7 @@ static struct command_result *process_getblockhash(struct bitcoin_cli *bcli)
return command_err_bcli_badjson(bcli, "bad blockhash");
}

start_bitcoin_cli(NULL, bcli->cmd, process_getrawblock, false,
BITCOIND_HIGH_PRIO, stash,
"getblock",
stash->block_hash,
/* Non-verbose: raw block. */
"0",
NULL);

return command_still_pending(bcli->cmd);
return getrawblock(bcli);
}

/* Get a raw block given its height.
Expand All @@ -637,6 +766,7 @@ static struct command_result *getrawblockbyheight(struct command *cmd,

stash = tal(cmd, struct getrawblock_stash);
stash->block_height = *height;
stash->peers = NULL;
tal_free(height);

start_bitcoin_cli(NULL, cmd, process_getblockhash, true,
Expand Down Expand Up @@ -872,17 +1002,7 @@ static struct command_result *sendrawtransaction(struct command *cmd,
return command_param_failed();

if (*allowhighfees) {
if (bitcoind->version >= 190001)
/* Starting in 19.0.1, second argument is
* maxfeerate, which when set to 0 means
* no max feerate.
*/
highfeesarg = "0";
else
/* in older versions, second arg is allowhighfees,
* set to true to allow high fees.
*/
highfeesarg = "true";
} else
highfeesarg = NULL;

Expand Down
95 changes: 95 additions & 0 deletions tests/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,101 @@ def test_bitcoin_ibd(node_factory, bitcoind):
assert 'warning_bitcoind_sync' not in l1.rpc.getinfo()


def test_bitcoin_pruned(node_factory, bitcoind):
"""Test that we try to fetch blocks from a peer if we can not find
them on our local bitcoind.
"""
fetched_peerblock = False

def mock_getblock(r):
# Simulate a pruned node that reutrns an error when asked for a block.
nonlocal fetched_peerblock
if fetched_peerblock:
fetched_peerblock = False
conf_file = os.path.join(bitcoind.bitcoin_dir, "bitcoin.conf")
brpc = RawProxy(btc_conf_file=conf_file)
return {
"result": brpc._call(r["method"], *r["params"]),
"error": None,
"id": r["id"],
}
return {
"id": r["id"],
"result": None,
"error": {"code": -1, "message": "Block not available (pruned data)"},
}

def mock_getpeerinfo(r, error=False):
if error:
return {"id": r["id"], "error": {"code": -1, "message": "unknown"}}
return {
"id": r["id"],
"result": [
{
"id": 1,
},
{
"id": 2,
},
{
"id": 3,
},
],
}

def mock_getblockfrompeer(error=False, release_after=0):
getblock_counter = 0

def mock_getblockfrompeer_inner(r):
nonlocal getblock_counter
getblock_counter += 1

if error and getblock_counter < release_after:
return {
"id": r["id"],
"error": {"code": -1, "message": "peer unknown"},
}
if getblock_counter >= release_after:
nonlocal fetched_peerblock
fetched_peerblock = True
return {
"id": r["id"],
"result": {},
}
return mock_getblockfrompeer_inner

l1 = node_factory.get_node(start=False)

l1.daemon.rpcproxy.mock_rpc("getblock", mock_getblock)
l1.daemon.rpcproxy.mock_rpc("getpeerinfo", mock_getpeerinfo)
l1.daemon.rpcproxy.mock_rpc("getblockfrompeer", mock_getblockfrompeer())
l1.start(wait_for_bitcoind_sync=False)

# check that we fetched a block from a peer (1st peer (from the back) in this case).
pruned_block = bitcoind.rpc.getblockhash(bitcoind.rpc.getblockcount())
l1.daemon.wait_for_log(f"failed to fetch block {pruned_block} from the bitcoin backend")
l1.daemon.wait_for_log(rf"try to fetch block {pruned_block} from peer 3")
l1.daemon.wait_for_log(rf"Adding block (\d+): {pruned_block}")

# check that we can also fetch from a peer > 1st (from the back).
l1.daemon.rpcproxy.mock_rpc("getblockfrompeer", mock_getblockfrompeer(error=True, release_after=2))
bitcoind.generate_block(1)

pruned_block = bitcoind.rpc.getblockhash(bitcoind.rpc.getblockcount())
l1.daemon.wait_for_log(f"failed to fetch block {pruned_block} from the bitcoin backend")
l1.daemon.wait_for_log(rf"failed to fetch block {pruned_block} from peer 3")
l1.daemon.wait_for_log(rf"try to fetch block {pruned_block} from peer (\d+)")
l1.daemon.wait_for_log(rf"Adding block (\d+): {pruned_block}")

# check that we retry if we could not fetch any block
l1.daemon.rpcproxy.mock_rpc("getblockfrompeer", mock_getblockfrompeer(error=True, release_after=10))
bitcoind.generate_block(1)

pruned_block = bitcoind.rpc.getblockhash(bitcoind.rpc.getblockcount())
l1.daemon.wait_for_log(f"asked all known peers about block {pruned_block}, retry")
l1.daemon.wait_for_log(rf"Adding block (\d+): {pruned_block}")


@pytest.mark.openchannel('v1')
@pytest.mark.openchannel('v2')
def test_lightningd_still_loading(node_factory, bitcoind, executor):
Expand Down
Loading