Skip to content

Commit adf7663

Browse files
committed
chanbackup: only store backups for peers with current/previous channels.
This seems fair. Signed-off-by: Rusty Russell <[email protected]>
1 parent cdce113 commit adf7663

File tree

3 files changed

+144
-3
lines changed

3 files changed

+144
-3
lines changed

plugins/chanbackup.c

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -388,9 +388,12 @@ static struct command_result *peer_after_listdatastore(struct command *cmd,
388388
const u8 *hexdata,
389389
struct node_id *nodeid)
390390
{
391+
struct out_req *req;
392+
393+
/* We use an empty record to indicate we *would* store data
394+
* for this peer, until we actually get data from them */
391395
if (tal_bytelen(hexdata) == 0)
392396
return command_hook_success(cmd);
393-
struct out_req *req;
394397

395398
if (!chanbackup(cmd->plugin)->peer_backup)
396399
return command_hook_success(cmd);
@@ -582,6 +585,15 @@ static struct command_result *after_staticbackup(struct command *cmd,
582585
return send_outreq(req);
583586
}
584587

588+
static struct command_result *datastore_create_done(struct command *cmd,
589+
const char *method,
590+
const char *buf,
591+
const jsmntok_t *result,
592+
void *unused)
593+
{
594+
return notification_or_hook_done(cmd);
595+
}
596+
585597
static struct command_result *json_state_changed(struct command *cmd,
586598
const char *buf,
587599
const jsmntok_t *params)
@@ -604,6 +616,32 @@ static struct command_result *json_state_changed(struct command *cmd,
604616
return send_outreq(req);
605617
}
606618

619+
/* Once it has a normal channel, we will start storing its
620+
* backups: put an empty record in place. */
621+
if (json_tok_streq(buf, statetok, "CHANNELD_NORMAL")) {
622+
const jsmntok_t *nodeid_tok;
623+
struct node_id node_id;
624+
625+
nodeid_tok = json_get_member(buf, notiftok, "peer_id");
626+
if (!json_to_node_id(buf, nodeid_tok, &node_id))
627+
plugin_err(cmd->plugin, "Invalid peer_id in %.*s",
628+
json_tok_full_len(notiftok),
629+
json_tok_full(buf, notiftok));
630+
631+
/* Might already exist, that's OK, just don't overwrite! */
632+
return jsonrpc_set_datastore_binary(cmd,
633+
tal_fmt(cmd,
634+
"chanbackup/peers/%s",
635+
fmt_node_id(tmpctx,
636+
&node_id)),
637+
/* Empty record */
638+
tal_arr(tmpctx, u8, 0),
639+
"must-create",
640+
datastore_create_done,
641+
datastore_create_done,
642+
NULL);
643+
}
644+
607645
return notification_or_hook_done(cmd);
608646
}
609647

@@ -723,7 +761,10 @@ static struct command_result *handle_your_peer_storage(struct command *cmd,
723761
* - MUST store the message.
724762
* - MAY store the message anyway.
725763
*/
726-
/* FIXME: Only store peers we want to! */
764+
/* We store if we have a datastore slot for it
765+
* (otherwise, this fails). We create those once it
766+
* has a channel, though the user could also create an
767+
* empty one if they wanted to */
727768

728769
/* BOLT #1:
729770
* - If it does store the message:
@@ -737,7 +778,7 @@ static struct command_result *handle_your_peer_storage(struct command *cmd,
737778
fmt_node_id(tmpctx,
738779
&node_id)),
739780
payload_deserialise,
740-
"create-or-replace",
781+
"must-replace",
741782
datastore_success,
742783
datastore_failed,
743784
"Saving chanbackup/peers/");

tests/test_misc.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3171,6 +3171,15 @@ def test_restorefrompeer(node_factory, bitcoind):
31713171
assert l1.daemon.is_in_log('Peer storage sent!')
31723172
assert l2.daemon.is_in_log('Peer storage sent!')
31733173

3174+
# Note: each node may or may not send peer_storage_retrieval: if it
3175+
# receives storage fast enough, it will, otherwise not.
3176+
l1.rpc.disconnect(l2.info['id'], force=True)
3177+
l1.rpc.connect(l2.info['id'], 'localhost', l2.port)
3178+
l1.daemon.wait_for_logs(['peer_out WIRE_PEER_STORAGE',
3179+
'peer_in WIRE_PEER_STORAGE'])
3180+
l2.daemon.wait_for_logs(['peer_out WIRE_PEER_STORAGE',
3181+
'peer_in WIRE_PEER_STORAGE'])
3182+
31743183
l1.stop()
31753184
os.unlink(os.path.join(l1.daemon.lightning_dir, TEST_NETWORK, "lightningd.sqlite3"))
31763185

tests/test_plugin.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4558,3 +4558,94 @@ def write_all(fd, bytestr):
45584558

45594559
with pytest.raises(RpcError, match="maybe encrypted"):
45604560
l1.rpc.exposesecret(passphrase=password)
4561+
4562+
4563+
def test_peer_storage(node_factory, bitcoind):
4564+
"""Test that we offer and re-xmit peer storage for our peers if they have a channel or are explicitly enabled"""
4565+
l1, l2, l3 = node_factory.get_nodes(3,
4566+
opts={'experimental-peer-storage': None,
4567+
'may_reconnect': True,
4568+
'dev-no-reconnect': None})
4569+
4570+
# Connect them, no peer storage yet anyway.
4571+
l1.rpc.connect(l2.info['id'], 'localhost', l2.port)
4572+
4573+
assert not l1.daemon.is_in_log(r'WIRE_PEER_STORAGE_RETRIEVAL')
4574+
assert not l2.daemon.is_in_log(r'WIRE_PEER_STORAGE_RETRIEVAL')
4575+
4576+
# And they won't store.
4577+
assert l1.rpc.listdatastore(['chanbackup', 'peers', l2.info['id']]) == {'datastore': []}
4578+
assert l2.rpc.listdatastore(['chanbackup', 'peers', l1.info['id']]) == {'datastore': []}
4579+
4580+
# Reconnect, still no xmit.
4581+
l1.rpc.disconnect(l2.info['id'])
4582+
l1.rpc.connect(l2.info['id'], 'localhost', l2.port)
4583+
4584+
# Give it time to make sure we *would* catch it if it happened.
4585+
time.sleep(10)
4586+
assert not l1.daemon.is_in_log(r'WIRE_PEER_STORAGE_RETRIEVAL')
4587+
assert not l2.daemon.is_in_log(r'WIRE_PEER_STORAGE_RETRIEVAL')
4588+
4589+
# But we can force it manually by creating an empty one.
4590+
l1.rpc.datastore(['chanbackup', 'peers', l2.info['id']], hex='')
4591+
assert l1.rpc.listdatastore(['chanbackup', 'peers', l2.info['id']])['datastore'][0]['hex'] == ''
4592+
l1.restart()
4593+
l1.rpc.connect(l2.info['id'], 'localhost', l2.port)
4594+
4595+
# Note: l1 may or may not send peer_storage_retrieval: if it
4596+
# receives storage fast enough, it will, otherwise not.
4597+
wait_for(lambda: l1.rpc.listdatastore(['chanbackup', 'peers', l2.info['id']])['datastore'][0]['hex'] != '')
4598+
4599+
# Next reconnect, l1 will send it back.
4600+
l1.rpc.disconnect(l2.info['id'])
4601+
l1.rpc.connect(l2.info['id'], 'localhost', l2.port)
4602+
4603+
l1.daemon.wait_for_log(r'peer_out WIRE_PEER_STORAGE_RETRIEVAL')
4604+
l2.daemon.wait_for_log(r'peer_in WIRE_PEER_STORAGE_RETRIEVAL')
4605+
assert not l1.daemon.is_in_log(r'peer_in WIRE_PEER_STORAGE_RETRIEVAL')
4606+
assert not l2.daemon.is_in_log(r'peer_out WIRE_PEER_STORAGE_RETRIEVAL')
4607+
4608+
# It must be valid.
4609+
l2.daemon.wait_for_log(r'Received peer_storage from peer')
4610+
assert not l2.daemon.is_in_log(r'PeerStorageFailed')
4611+
4612+
# l2 will only store if we have an ESTABLISHED channel.
4613+
l1.openchannel(l2, confirm=False, wait_for_announce=False)
4614+
assert l2.rpc.listdatastore(['chanbackup', 'peers', l1.info['id']]) == {'datastore': []}
4615+
4616+
# But it will create an entry once it hits CHANNELD_NORMAL.
4617+
bitcoind.generate_block(1, wait_for_mempool=1)
4618+
wait_for(lambda: l2.rpc.listdatastore(['chanbackup', 'peers', l1.info['id']]) != {'datastore': []})
4619+
4620+
# Now it will store l1's backup when channels change.
4621+
l1.openchannel(l3, wait_for_announce=False)
4622+
wait_for(lambda: l2.rpc.listdatastore(['chanbackup', 'peers', l1.info['id']])['datastore'][0]['hex'] != '')
4623+
4624+
# Now every time we reconnect, both sides restore.
4625+
l1.restart()
4626+
l1.rpc.connect(l2.info['id'], 'localhost', l2.port)
4627+
4628+
l1.daemon.wait_for_logs([r'peer_out WIRE_PEER_STORAGE_RETRIEVAL',
4629+
r'peer_in WIRE_PEER_STORAGE_RETRIEVAL'])
4630+
l2.daemon.wait_for_logs([r'peer_out WIRE_PEER_STORAGE_RETRIEVAL',
4631+
r'peer_in WIRE_PEER_STORAGE_RETRIEVAL'])
4632+
4633+
# Even if we close channel, and it's long forgotten, we will store for
4634+
# them as a courtesy.
4635+
l1.rpc.close(l2.info['id'])
4636+
assert len(l1.rpc.listpeerchannels()['channels']) == 2
4637+
bitcoind.generate_block(100, wait_for_mempool=1)
4638+
wait_for(lambda: len(l1.rpc.listpeerchannels()['channels']) == 1)
4639+
4640+
# Now try restarting l2 and connecting that way instead.
4641+
l2.restart()
4642+
l2.rpc.connect(l1.info['id'], 'localhost', l1.port)
4643+
4644+
l1.daemon.wait_for_logs([r'peer_out WIRE_PEER_STORAGE_RETRIEVAL',
4645+
r'peer_in WIRE_PEER_STORAGE_RETRIEVAL'])
4646+
l2.daemon.wait_for_logs([r'peer_out WIRE_PEER_STORAGE_RETRIEVAL',
4647+
r'peer_in WIRE_PEER_STORAGE_RETRIEVAL'])
4648+
4649+
# This should never happen
4650+
assert not l1.daemon.is_in_log(r'PeerStorageFailed')
4651+
assert not l2.daemon.is_in_log(r'PeerStorageFailed')

0 commit comments

Comments
 (0)