From 257e41ebe6ee1cd93ce93e29f873b71981cda01e Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Wed, 6 Nov 2024 13:54:37 +1030 Subject: [PATCH] channeld_fakenet: add capacity information. Start with a random capacity (linear prob), and remember in-progess payments so we can simulate them using capacity properly. Signed-off-by: Rusty Russell --- tests/plugins/channeld_fakenet.c | 83 ++++++++++++++++++++++++++++++ tests/test_askrene.py | 86 ++++++++++++++++++++++---------- 2 files changed, 144 insertions(+), 25 deletions(-) diff --git a/tests/plugins/channeld_fakenet.c b/tests/plugins/channeld_fakenet.c index f32cff8e1..a73179dc4 100644 --- a/tests/plugins/channeld_fakenet.c +++ b/tests/plugins/channeld_fakenet.c @@ -60,6 +60,8 @@ struct info { struct timers timers; /* Seed for channel feature determination (e.g. delay time) */ struct siphash_seed seed; + /* Currently used channels */ + struct reservation **reservations; /* Fake stuff we feed into lightningd */ struct fee_states *fee_states; @@ -98,6 +100,12 @@ struct multi_payment { struct payment **payments; }; +/* We've taken up some part of a channel */ +struct reservation { + struct short_channel_id_dir scidd; + struct amount_msat amount; +}; + static void make_privkey(size_t idx, struct privkey *pk) { /* pyln-testing uses 'lightning-N' then all zeroes as hsm_secret. */ @@ -573,6 +581,72 @@ static void add_mpp(struct info *info, tal_free(mp); } +static void destroy_reservation(struct reservation *r, + struct info *info) +{ + for (size_t i = 0; i < tal_count(info->reservations); i++) { + if (info->reservations[i] == r) { + tal_arr_remove(&info->reservations, i); + return; + } + } + abort(); +} + +static void add_reservation(const tal_t *ctx, + struct info *info, + const struct short_channel_id_dir *scidd, + struct amount_msat amount) +{ + struct reservation *r = tal(ctx, struct reservation); + r->scidd = *scidd; + r->amount = amount; + tal_arr_expand(&info->reservations, r); + tal_add_destructor2(r, destroy_reservation, info); +} + +/* We determine capacity for one side, then we derive the other side. + * Reservations, however, do *not* credit the other side, since + * they're htlcs in flight. (We don't update after payments, either!) */ +static struct amount_msat calc_capacity(struct info *info, + const struct gossmap_chan *c, + const struct short_channel_id_dir *scidd) +{ + struct short_channel_id_dir base_scidd; + struct amount_msat base_capacity, dynamic_capacity; + + base_scidd.scid = scidd->scid; + base_scidd.dir = 0; + base_capacity = gossmap_chan_get_capacity(info->gossmap, c); + dynamic_capacity = amount_msat(channel_range(info, &base_scidd, + 0, base_capacity.millisatoshis)); /* Raw: rand function */ + /* Invert capacity if that is backwards */ + if (scidd->dir != base_scidd.dir) { + if (!amount_msat_sub(&dynamic_capacity, base_capacity, dynamic_capacity)) + abort(); + } + + status_debug("Capacity for %s is %s, dynamic capacity is %s", + fmt_short_channel_id_dir(tmpctx, scidd), + fmt_amount_msat(tmpctx, base_capacity), + fmt_amount_msat(tmpctx, dynamic_capacity)); + + /* Take away any reservations */ + for (size_t i = 0; i < tal_count(info->reservations); i++) { + if (!short_channel_id_dir_eq(&info->reservations[i]->scidd, scidd)) + continue; + /* We should never use more that we have! */ + if (!amount_msat_sub(&dynamic_capacity, + dynamic_capacity, + info->reservations[i]->amount)) + abort(); + status_debug("... minus reservation %s", + fmt_amount_msat(tmpctx, info->reservations[i]->amount)); + } + + return dynamic_capacity; +} + /* Mutual recursion via timer */ struct delayed_forward { struct info *info; @@ -704,6 +778,14 @@ found_next: return; } + if (amount_msat_greater(amount, calc_capacity(info, c, &scidd))) { + fail(info, htlc, payload, WIRE_TEMPORARY_CHANNEL_FAILURE); + return; + } + + /* When we resolve the HTLC, we'll cancel the reservations */ + add_reservation(htlc, info, &scidd, amount); + if (payload->path_key) { struct sha256 sha; blinding_hash_e_and_ss(payload->path_key, @@ -1172,6 +1254,7 @@ int main(int argc, char *argv[]) info->cached_node_idx = tal_arr(info, size_t, 0); info->multi_payments = tal_arr(info, struct multi_payment *, 0); + info->reservations = tal_arr(info, struct reservation *, 0); timers_init(&info->timers, time_mono()); info->commit_num = 1; info->fakesig.sighash_type = SIGHASH_ALL; diff --git a/tests/test_askrene.py b/tests/test_askrene.py index dd8e8e6c2..5045ad806 100644 --- a/tests/test_askrene.py +++ b/tests/test_askrene.py @@ -997,6 +997,7 @@ def test_real_data(node_factory, bitcoind): assert (len(fees[best]), len(improved), total_first_fee, total_final_fee, percent_fee_reduction) == (8, 95, 6007785, 564997, 91) +@pytest.mark.slow_test def test_askrene_fake_channeld(node_factory, bitcoind): outfile = tempfile.NamedTemporaryFile(prefix='gossip-store-') nodeids = subprocess.check_output(['devtools/gossmap-compress', @@ -1024,36 +1025,71 @@ def test_askrene_fake_channeld(node_factory, bitcoind): shaseed = subprocess.check_output(["tools/hsmtool", "dumpcommitments", l1.info['id'], "1", "0", hsmfile]).decode('utf-8').strip().partition(": ")[2] l1.rpc.dev_peer_shachain(l2.info['id'], shaseed) + TEMPORARY_CHANNEL_FAILURE = 0x1007 + MPP_TIMEOUT = 0x17 + + l1.rpc.askrene_create_layer('test_askrene_fake_channeld') for n in range(0, 100): if n in (62, 76, 80, 97): continue - routes = l1.rpc.getroutes(source=l1.info['id'], - destination=nodeids[n], - amount_msat=AMOUNT, - layers=['auto.sourcefree', 'auto.localchans'], - maxfee_msat=AMOUNT, - final_cltv=18) + print(f"PAYING Node #{n}") + success = False + while not success: + routes = l1.rpc.getroutes(source=l1.info['id'], + destination=nodeids[n], + amount_msat=AMOUNT, + layers=['auto.sourcefree', 'auto.localchans'], + maxfee_msat=AMOUNT, + final_cltv=18) - preimage_hex = f'{n:02}' + '00' * 31 - hash_hex = sha256(bytes.fromhex(preimage_hex)).hexdigest() + preimage_hex = f'{n:02}' + '00' * 31 + hash_hex = sha256(bytes.fromhex(preimage_hex)).hexdigest() - # Sendpay wants a different format, so we convert. - for i, r in enumerate(routes['routes']): - hops = [{'id': h['next_node_id'], - 'channel': h['short_channel_id_dir'].split('/')[0]} - for h in r['path']] - # delay and amount_msat for sendpay are amounts at *end* of hop, not start! - with_end = r['path'] + [{'amount_msat': r['amount_msat'], 'delay': r['final_cltv']}] - for n, h in enumerate(hops): - h['delay'] = with_end[n + 1]['delay'] - h['amount_msat'] = with_end[n + 1]['amount_msat'] + paths = {} + # Sendpay wants a different format, so we convert. + for i, r in enumerate(routes['routes']): + paths[i] = [{'id': h['next_node_id'], + 'channel': h['short_channel_id_dir'].split('/')[0], + 'direction': int(h['short_channel_id_dir'].split('/')[1])} + for h in r['path']] - l1.rpc.sendpay(hops, hash_hex, - amount_msat=AMOUNT, - payment_secret='00' * 32, - partid=i + 1, groupid=1) + # delay and amount_msat for sendpay are amounts at *end* of hop, not start! + with_end = r['path'] + [{'amount_msat': r['amount_msat'], 'delay': r['final_cltv']}] + for n, h in enumerate(paths[i]): + h['delay'] = with_end[n + 1]['delay'] + h['amount_msat'] = with_end[n + 1]['amount_msat'] - for i, r in enumerate(routes['routes']): - # Worst-case timeout is 1 second per hop. - assert l1.rpc.waitsendpay(hash_hex, timeout=TIMEOUT + len(r['path']), partid=i + 1, groupid=1)['payment_preimage'] == preimage_hex + l1.rpc.sendpay(paths[i], hash_hex, + amount_msat=AMOUNT, + payment_secret='00' * 32, + partid=i + 1, groupid=1) + + for i, p in paths.items(): + # Worst-case timeout is 1 second per hop, + 60 seconds if MPP timeout! + try: + if l1.rpc.waitsendpay(hash_hex, timeout=TIMEOUT + len(p) + 60, partid=i + 1, groupid=1): + success = True + except RpcError as err: + # Timeout means this one succeeded! + if err.error['data']['failcode'] == MPP_TIMEOUT: + for h in p: + l1.rpc.askrene_inform_channel('test_askrene_fake_channeld', + f"{h['channel']}/{h['direction']}", + h['amount_msat'], + 'unconstrained') + elif err.error['data']['failcode'] == TEMPORARY_CHANNEL_FAILURE: + # We succeeded up to here + failpoint = err.error['data']['erring_index'] + for h in p[:failpoint]: + l1.rpc.askrene_inform_channel('test_askrene_fake_channeld', + f"{h['channel']}/{h['direction']}", + h['amount_msat'], + 'unconstrained') + h = p[failpoint] + l1.rpc.askrene_inform_channel('test_askrene_fake_channeld', + f"{h['channel']}/{h['direction']}", + h['amount_msat'], + 'constrained') + else: + raise err