diff --git a/common/features.c b/common/features.c index e423138eef33..8d41a88b0e35 100644 --- a/common/features.c +++ b/common/features.c @@ -8,6 +8,9 @@ static const u32 our_features[] = { OPTIONAL_FEATURE(OPT_DATA_LOSS_PROTECT), OPTIONAL_FEATURE(OPT_UPFRONT_SHUTDOWN_SCRIPT), OPTIONAL_FEATURE(OPT_GOSSIP_QUERIES), +#if EXPERIMENTAL_FEATURES + OPTIONAL_FEATURE(OPT_VAR_ONION), +#endif OPTIONAL_FEATURE(OPT_GOSSIP_QUERIES_EX), OPTIONAL_FEATURE(OPT_STATIC_REMOTEKEY), }; diff --git a/common/sphinx.c b/common/sphinx.c index 5fe79a2f8bfd..d56a671018da 100644 --- a/common/sphinx.c +++ b/common/sphinx.c @@ -134,9 +134,10 @@ void sphinx_add_raw_hop(struct sphinx_path *path, const struct pubkey *pubkey, assert(sphinx_path_payloads_size(path) <= ROUTING_INFO_SIZE); } -void sphinx_add_v0_hop(struct sphinx_path *path, const struct pubkey *pubkey, - const struct short_channel_id *scid, - struct amount_msat forward, u32 outgoing_cltv) +static void sphinx_add_v0_hop(struct sphinx_path *path, + const struct pubkey *pubkey, + const struct short_channel_id *scid, + struct amount_msat forward, u32 outgoing_cltv) { const u8 padding[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; @@ -149,6 +150,80 @@ void sphinx_add_v0_hop(struct sphinx_path *path, const struct pubkey *pubkey, sphinx_add_raw_hop(path, pubkey, 0, buf); } +static void sphinx_add_tlv_hop(struct sphinx_path *path, + const struct pubkey *pubkey, + const struct tlv_tlv_payload *tlv) +{ + u8 *tlvs = tal_arr(path, u8, 0); + towire_tlvs(&tlvs, tlvs_tlv_payload, TLVS_TLV_PAYLOAD_ARRAY_SIZE, tlv); + sphinx_add_raw_hop(path, pubkey, tal_bytelen(tlvs), tlvs); +} + +void sphinx_add_nonfinal_hop(struct sphinx_path *path, + const struct pubkey *pubkey, + bool use_tlv, + const struct short_channel_id *scid, + struct amount_msat forward, + u32 outgoing_cltv) +{ + if (use_tlv) { + struct tlv_tlv_payload *tlv = tlv_tlv_payload_new(tmpctx); + struct tlv_tlv_payload_amt_to_forward tlv_amt; + struct tlv_tlv_payload_outgoing_cltv_value tlv_cltv; + struct tlv_tlv_payload_short_channel_id tlv_scid; + + /* BOLT #4: + * + * The writer: + * - MUST include `amt_to_forward` and `outgoing_cltv_value` + * for every node. + * - MUST include `short_channel_id` for every non-final node. + */ + tlv_amt.amt_to_forward = forward.millisatoshis; /* Raw: TLV convert */ + tlv_cltv.outgoing_cltv_value = outgoing_cltv; + tlv_scid.short_channel_id = *scid; + tlv->amt_to_forward = &tlv_amt; + tlv->outgoing_cltv_value = &tlv_cltv; + tlv->short_channel_id = &tlv_scid; + + sphinx_add_tlv_hop(path, pubkey, tlv); + } else { + sphinx_add_v0_hop(path, pubkey, scid, forward, outgoing_cltv); + } +} + +void sphinx_add_final_hop(struct sphinx_path *path, + const struct pubkey *pubkey, + bool use_tlv, + struct amount_msat forward, + u32 outgoing_cltv) +{ + if (use_tlv) { + struct tlv_tlv_payload *tlv = tlv_tlv_payload_new(tmpctx); + struct tlv_tlv_payload_amt_to_forward tlv_amt; + struct tlv_tlv_payload_outgoing_cltv_value tlv_cltv; + + /* BOLT #4: + * + * The writer: + * - MUST include `amt_to_forward` and `outgoing_cltv_value` + * for every node. + *... + * - MUST NOT include `short_channel_id` for the final node. + */ + tlv_amt.amt_to_forward = forward.millisatoshis; /* Raw: TLV convert */ + tlv_cltv.outgoing_cltv_value = outgoing_cltv; + tlv->amt_to_forward = &tlv_amt; + tlv->outgoing_cltv_value = &tlv_cltv; + + sphinx_add_tlv_hop(path, pubkey, tlv); + } else { + static struct short_channel_id all_zero_scid; + sphinx_add_v0_hop(path, pubkey, &all_zero_scid, + forward, outgoing_cltv); + } +} + /* Small helper to append data to a buffer and update the position * into the buffer */ @@ -426,7 +501,7 @@ static struct hop_params *generate_hop_params( return params; } -static void deserialize_hop_data(struct hop_data *data, const u8 *src) +static void deserialize_hop_data(struct hop_data_legacy *data, const u8 *src) { const u8 *cursor = src; size_t max = FRAME_SIZE; @@ -477,16 +552,31 @@ static void sphinx_parse_payload(struct route_step *step, const u8 *src) } #endif - /* Legacy hop_data support */ + /* BOLT #4: + * + * The `length` field determines both the length and the format of the + * `hop_payload` field; the following formats are defined: + * + * - Legacy `hop_data` format, identified by a single `0x00` byte for + * length. In this case the `hop_payload_length` is defined to be 32 + * bytes. + * + * - `tlv_payload` format, identified by any length over `1`. In this + * case the `hop_payload_length` is equal to the numeric value of + * `length`. + */ if (src[0] == 0x00) { vsize = 1; raw_size = 32; hop_size = FRAME_SIZE; step->type = SPHINX_V0_PAYLOAD; - } else { + } else if (src[0] > 1) { vsize = bigsize_get(src, 3, &raw_size); hop_size = raw_size + vsize + HMAC_SIZE; step->type = SPHINX_TLV_PAYLOAD; + } else { + step->type = SPHINX_INVALID_PAYLOAD; + return; } /* Copy common pieces over */ @@ -497,6 +587,18 @@ static void sphinx_parse_payload(struct route_step *step, const u8 *src) * later. */ if (step->type == SPHINX_V0_PAYLOAD) deserialize_hop_data(&step->payload.v0, src); + else if (step->type == SPHINX_TLV_PAYLOAD) { + const u8 *tlv = step->raw_payload; + size_t max = tal_bytelen(tlv); + step->payload.tlv = tlv_tlv_payload_new(step); + if (!fromwire_tlvs(&tlv, &max, tlvs_tlv_payload, + TLVS_TLV_PAYLOAD_ARRAY_SIZE, + step->payload.tlv)) { + /* FIXME: record offset of violation for error! */ + step->type = SPHINX_INVALID_PAYLOAD; + return; + } + } } struct onionpacket *create_onionpacket( @@ -781,3 +883,74 @@ struct onionreply *unwrap_onionreply(const tal_t *ctx, return oreply; } + +/** + * Helper to extract fields from ONION_END. + */ +bool route_step_decode_end(const struct route_step *rs, + struct amount_msat *amt_forward, + u32 *outgoing_cltv) +{ + assert(rs->nextcase == ONION_END); + + switch (rs->type) { + case SPHINX_V0_PAYLOAD: + *amt_forward = rs->payload.v0.amt_forward; + *outgoing_cltv = rs->payload.v0.outgoing_cltv; + return true; + case SPHINX_TLV_PAYLOAD: + if (!rs->payload.tlv->amt_to_forward) + return false; + if (!rs->payload.tlv->outgoing_cltv_value) + return false; + amt_forward->millisatoshis /* Raw: tu64 -> millisatoshis */ + = rs->payload.tlv->amt_to_forward->amt_to_forward; + *outgoing_cltv = rs->payload.tlv->outgoing_cltv_value->outgoing_cltv_value; + return true; + case SPHINX_INVALID_PAYLOAD: + return false; + + /* This should probably be removed, as it's just for testing */ + case SPHINX_RAW_PAYLOAD: + abort(); + } + abort(); +} + +/** + * Helper to extract fields from ONION_FORWARD. + */ +bool route_step_decode_forward(const struct route_step *rs, + struct amount_msat *amt_forward, + u32 *outgoing_cltv, + struct short_channel_id *scid) +{ + assert(rs->nextcase == ONION_FORWARD); + + switch (rs->type) { + case SPHINX_V0_PAYLOAD: + *amt_forward = rs->payload.v0.amt_forward; + *outgoing_cltv = rs->payload.v0.outgoing_cltv; + *scid = rs->payload.v0.channel_id; + return true; + case SPHINX_TLV_PAYLOAD: + if (!rs->payload.tlv->amt_to_forward) + return false; + amt_forward->millisatoshis /* Raw: tu64 -> millisatoshis */ + = rs->payload.tlv->amt_to_forward->amt_to_forward; + if (!rs->payload.tlv->outgoing_cltv_value) + return false; + *outgoing_cltv = rs->payload.tlv->outgoing_cltv_value->outgoing_cltv_value; + if (!rs->payload.tlv->short_channel_id) + return false; + *scid = rs->payload.tlv->short_channel_id->short_channel_id; + return true; + case SPHINX_INVALID_PAYLOAD: + return false; + + /* This should probably be removed, as it's just for testing */ + case SPHINX_RAW_PAYLOAD: + abort(); + } + abort(); +} diff --git a/common/sphinx.h b/common/sphinx.h index d6fc81ef96f0..e0478f93f64e 100644 --- a/common/sphinx.h +++ b/common/sphinx.h @@ -61,7 +61,7 @@ struct sphinx_path; * * [`u32`:`outgoing_cltv_value`] * * [`12*byte`:`padding`] */ -struct hop_data { +struct hop_data_legacy { u8 realm; struct short_channel_id channel_id; struct amount_msat amt_forward; @@ -80,7 +80,8 @@ struct route_step { struct onionpacket *next; enum sphinx_payload_type type; union { - struct hop_data v0; + struct hop_data_legacy v0; + struct tlv_tlv_payload *tlv; } payload; u8 *raw_payload; }; @@ -218,16 +219,43 @@ struct sphinx_path *sphinx_path_new_with_key(const tal_t *ctx, const u8 *associated_data, const struct secret *session_key); -/** - * Add a V0 (Realm 0) single frame hop to the path. - */ -void sphinx_add_v0_hop(struct sphinx_path *path, const struct pubkey *pubkey, - const struct short_channel_id *scid, struct amount_msat forward, - u32 outgoing_cltv); /** * Add a raw payload hop to the path. */ void sphinx_add_raw_hop(struct sphinx_path *path, const struct pubkey *pubkey, enum sphinx_payload_type type, const u8 *payload); +/** + * Add a non-final hop to the path. + */ +void sphinx_add_nonfinal_hop(struct sphinx_path *path, + const struct pubkey *pubkey, + bool use_tlv, + const struct short_channel_id *scid, + struct amount_msat forward, + u32 outgoing_cltv); + +/** + * Add a final hop to the path. + */ +void sphinx_add_final_hop(struct sphinx_path *path, + const struct pubkey *pubkey, + bool use_tlv, + struct amount_msat forward, + u32 outgoing_cltv); + +/** + * Helper to extract fields from ONION_END. + */ +bool route_step_decode_end(const struct route_step *rs, + struct amount_msat *amt_forward, + u32 *outgoing_cltv); + +/** + * Helper to extract fields from ONION_FORWARD. + */ +bool route_step_decode_forward(const struct route_step *rs, + struct amount_msat *amt_forward, + u32 *outgoing_cltv, + struct short_channel_id *scid); #endif /* LIGHTNING_COMMON_SPHINX_H */ diff --git a/common/test/Makefile b/common/test/Makefile index 59e2a6415575..3974d2099855 100644 --- a/common/test/Makefile +++ b/common/test/Makefile @@ -11,6 +11,9 @@ $(COMMON_TEST_OBJS): $(COMMON_HEADERS) $(WIRE_HEADERS) $(COMMON_SRC) ALL_TEST_PROGRAMS += $(COMMON_TEST_PROGRAMS) ALL_OBJS += $(COMMON_TEST_PROGRAMS:=.o) +# Sphinx test wants to decode TLVs. +common/test/run-sphinx: wire/gen_onion_wire.o + update-mocks: $(COMMON_TEST_SRC:%=update-mocks/%) check-units: $(COMMON_TEST_PROGRAMS:%=unittest/%) diff --git a/common/test/run-sphinx.c b/common/test/run-sphinx.c index b79d249199c1..41f9c44a0517 100644 --- a/common/test/run-sphinx.c +++ b/common/test/run-sphinx.c @@ -43,13 +43,31 @@ const u8 *fromwire(const u8 **cursor UNNEEDED, size_t *max UNNEEDED, void *copy /* Generated stub for fromwire_amount_msat */ struct amount_msat fromwire_amount_msat(const u8 **cursor UNNEEDED, size_t *max UNNEEDED) { fprintf(stderr, "fromwire_amount_msat called!\n"); abort(); } +/* Generated stub for fromwire_bigsize */ +bigsize_t fromwire_bigsize(const u8 **cursor UNNEEDED, size_t *max UNNEEDED) +{ fprintf(stderr, "fromwire_bigsize called!\n"); abort(); } /* Generated stub for fromwire_fail */ const void *fromwire_fail(const u8 **cursor UNNEEDED, size_t *max UNNEEDED) { fprintf(stderr, "fromwire_fail called!\n"); abort(); } +/* Generated stub for fromwire_sha256 */ +void fromwire_sha256(const u8 **cursor UNNEEDED, size_t *max UNNEEDED, struct sha256 *sha256 UNNEEDED) +{ fprintf(stderr, "fromwire_sha256 called!\n"); abort(); } /* Generated stub for fromwire_short_channel_id */ void fromwire_short_channel_id(const u8 **cursor UNNEEDED, size_t *max UNNEEDED, struct short_channel_id *short_channel_id UNNEEDED) { fprintf(stderr, "fromwire_short_channel_id called!\n"); abort(); } +/* Generated stub for fromwire_tlvs */ +bool fromwire_tlvs(const u8 **cursor UNNEEDED, size_t *max UNNEEDED, + const struct tlv_record_type types[] UNNEEDED, + size_t num_types UNNEEDED, + void *record UNNEEDED) +{ fprintf(stderr, "fromwire_tlvs called!\n"); abort(); } +/* Generated stub for fromwire_tu32 */ +u32 fromwire_tu32(const u8 **cursor UNNEEDED, size_t *max UNNEEDED) +{ fprintf(stderr, "fromwire_tu32 called!\n"); abort(); } +/* Generated stub for fromwire_tu64 */ +u64 fromwire_tu64(const u8 **cursor UNNEEDED, size_t *max UNNEEDED) +{ fprintf(stderr, "fromwire_tu64 called!\n"); abort(); } /* Generated stub for fromwire_u16 */ u16 fromwire_u16(const u8 **cursor UNNEEDED, size_t *max UNNEEDED) { fprintf(stderr, "fromwire_u16 called!\n"); abort(); } @@ -59,16 +77,40 @@ u32 fromwire_u32(const u8 **cursor UNNEEDED, size_t *max UNNEEDED) /* Generated stub for fromwire_u8 */ u8 fromwire_u8(const u8 **cursor UNNEEDED, size_t *max UNNEEDED) { fprintf(stderr, "fromwire_u8 called!\n"); abort(); } +/* Generated stub for fromwire_u8_array */ +void fromwire_u8_array(const u8 **cursor UNNEEDED, size_t *max UNNEEDED, u8 *arr UNNEEDED, size_t num UNNEEDED) +{ fprintf(stderr, "fromwire_u8_array called!\n"); abort(); } /* Generated stub for towire */ void towire(u8 **pptr UNNEEDED, const void *data UNNEEDED, size_t len UNNEEDED) { fprintf(stderr, "towire called!\n"); abort(); } +/* Generated stub for towire_amount_msat */ +void towire_amount_msat(u8 **pptr UNNEEDED, const struct amount_msat msat UNNEEDED) +{ fprintf(stderr, "towire_amount_msat called!\n"); abort(); } +/* Generated stub for towire_bigsize */ +void towire_bigsize(u8 **pptr UNNEEDED, const bigsize_t val UNNEEDED) +{ fprintf(stderr, "towire_bigsize called!\n"); abort(); } /* Generated stub for towire_pad */ void towire_pad(u8 **pptr UNNEEDED, size_t num UNNEEDED) { fprintf(stderr, "towire_pad called!\n"); abort(); } +/* Generated stub for towire_sha256 */ +void towire_sha256(u8 **pptr UNNEEDED, const struct sha256 *sha256 UNNEEDED) +{ fprintf(stderr, "towire_sha256 called!\n"); abort(); } /* Generated stub for towire_short_channel_id */ void towire_short_channel_id(u8 **pptr UNNEEDED, const struct short_channel_id *short_channel_id UNNEEDED) { fprintf(stderr, "towire_short_channel_id called!\n"); abort(); } +/* Generated stub for towire_tlvs */ +void towire_tlvs(u8 **pptr UNNEEDED, + const struct tlv_record_type types[] UNNEEDED, + size_t num_types UNNEEDED, + const void *record UNNEEDED) +{ fprintf(stderr, "towire_tlvs called!\n"); abort(); } +/* Generated stub for towire_tu32 */ +void towire_tu32(u8 **pptr UNNEEDED, u32 v UNNEEDED) +{ fprintf(stderr, "towire_tu32 called!\n"); abort(); } +/* Generated stub for towire_tu64 */ +void towire_tu64(u8 **pptr UNNEEDED, u64 v UNNEEDED) +{ fprintf(stderr, "towire_tu64 called!\n"); abort(); } /* Generated stub for towire_u16 */ void towire_u16(u8 **pptr UNNEEDED, u16 v UNNEEDED) { fprintf(stderr, "towire_u16 called!\n"); abort(); } @@ -78,6 +120,9 @@ void towire_u32(u8 **pptr UNNEEDED, u32 v UNNEEDED) /* Generated stub for towire_u64 */ void towire_u64(u8 **pptr UNNEEDED, u64 v UNNEEDED) { fprintf(stderr, "towire_u64 called!\n"); abort(); } +/* Generated stub for towire_u8_array */ +void towire_u8_array(u8 **pptr UNNEEDED, const u8 *arr UNNEEDED, size_t num UNNEEDED) +{ fprintf(stderr, "towire_u8_array called!\n"); abort(); } /* AUTOGENERATED MOCKS END */ secp256k1_context *secp256k1_ctx; diff --git a/devtools/onion.c b/devtools/onion.c index 27140914190b..a6442f06f623 100644 --- a/devtools/onion.c +++ b/devtools/onion.c @@ -29,7 +29,6 @@ static void do_generate(int argc, char **argv, struct secret session_key; struct secret *shared_secrets; struct sphinx_path *sp; - struct hop_data hops_data[num_hops]; const u8* tmp_assocdata =tal_dup_arr(ctx, u8, assocdata, ASSOC_DATA_SIZE, 0); @@ -59,49 +58,31 @@ static void do_generate(int argc, char **argv, argv[2 + i]); } - memset(&hops_data[i], 0, sizeof(hops_data[i])); - if (argv[2 + i][klen] != '\0') { - /* FIXME: Generic realm support, not this hack! */ - /* FIXME: Multi hop! */ + /* / -> raw hopdata. /tlv -> TLV encoding. */ + if (argv[2 + i][klen] != '\0' && argv[2 + i][klen] != 't') { const char *hopstr = argv[2 + i] + klen + 1; - size_t dsize = hex_data_size(strlen(hopstr)); - be64 scid, msat; - be32 cltv; - u8 padding[12]; - if (dsize != 33) - errx(1, "hopdata expected 33 bytes"); - if (!hex_decode(hopstr, 2, - &hops_data[i].realm, - sizeof(hops_data[i].realm)) - || !hex_decode(hopstr + 2, 16, - &scid, sizeof(scid)) - || !hex_decode(hopstr + 2 + 16, 16, - &msat, sizeof(msat)) - || !hex_decode(hopstr + 2 + 16 + 16, 8, - &cltv, sizeof(cltv)) - || !hex_decode(hopstr + 2 + 16 + 16 + 8, 24, - padding, sizeof(padding))) - errx(1, "hopdata bad hex"); - if (hops_data[i].realm != 0) - errx(1, "FIXME: Only realm 0 supported"); - if (!memeqzero(padding, sizeof(padding))) - errx(1, "FIXME: Only zero padding supported"); - /* Fix endian up */ - hops_data[i].channel_id.u64 - = be64_to_cpu(scid); - hops_data[i].amt_forward.millisatoshis /* Raw: test code */ - = be64_to_cpu(msat); - hops_data[i].outgoing_cltv - = be32_to_cpu(cltv); + u8 *data = tal_hexdata(ctx, hopstr, strlen(hopstr)); + + if (!data) + errx(1, "bad hex after / in %s", argv[1 + i]); + sphinx_add_raw_hop(sp, &path[i], SPHINX_RAW_PAYLOAD, + data); } else { - hops_data[i].realm = i; - memset(&hops_data[i].channel_id, i, - sizeof(hops_data[i].channel_id)); - hops_data[i].amt_forward.millisatoshis = i; /* Raw: test code */ - hops_data[i].outgoing_cltv = i; + struct short_channel_id scid; + struct amount_msat amt; + bool use_tlv = streq(argv[1 + i] + klen, "/tlv"); + + memset(&scid, i, sizeof(scid)); + amt.millisatoshis = i; /* Raw: test code */ + if (i == num_hops - 1) + sphinx_add_final_hop(sp, &path[i], + use_tlv, + amt, i); + else + sphinx_add_nonfinal_hop(sp, &path[i], + use_tlv, + &scid, amt, i); } - fprintf(stderr, "Hopdata %d: %s\n", i, tal_hexstr(NULL, &hops_data[i], sizeof(hops_data[i]))); - sphinx_add_v0_hop(sp, &path[i], &hops_data[i].channel_id, hops_data[i].amt_forward, hops_data[i].outgoing_cltv); } struct onionpacket *res = create_onionpacket(ctx, sp, &shared_secrets); @@ -311,8 +292,8 @@ int main(int argc, char **argv) opt_register_noarg("--help|-h", opt_usage_and_exit, "\n\n\tdecode \n" "\tgenerate ...\n" - "\tgenerate [/hopdata] [/hopdata]\n" - "\tgenerate [/hopdata] [/hopdata]\n" + "\tgenerate [/hopdata|/tlv] [/hopdata|/tlv]\n" + "\tgenerate [/hopdata|/tlv] [/hopdata|/tlv]\n" "\truntest \n\n", "Show this message\n\n" "\texample:\n" "\t> onion generate 02c18e7ff9a319983e85094b8c957da5c1230ecb328c1f1c7e88029f1fec2046f8/00000000000000000000000000000f424000000138000000000000000000000000 --assoc-data 44ee26f01e54665937b892f6afbfdfb88df74bcca52d563f088668cf4490aacd > onion.dat\n" diff --git a/doc/PLUGINS.md b/doc/PLUGINS.md index a6eac78be3ff..e87deaa50ba2 100644 --- a/doc/PLUGINS.md +++ b/doc/PLUGINS.md @@ -588,11 +588,10 @@ The payload of the hook call has the following format: { "onion": { "payload": "", - "per_hop_v0": { - "realm": "00", - "short_channel_id": "1x2x3", - "forward_amount": "42msat", - "outgoing_cltv_value": 500014 + "type": "legacy", + "short_channel_id": "1x2x3", + "forward_amount": "42msat", + "outgoing_cltv_value": 500014 } }, "next_onion": "[1365bytes of serialized onion]", @@ -606,21 +605,16 @@ The payload of the hook call has the following format: } ``` -The `per_hop_v0` will only be present if the per hop payload has format `0x00` -as defined by the specification. If not present an object representing the -type-length-vale (TLV) payload will be added (pending specification). For detailed information about each field please refer to [BOLT 04 of the specification][bolt4], the following is just a brief summary: +For detailed information about each field please refer to [BOLT 04 of the specification][bolt4], the following is just a brief summary: - `onion.payload` contains the unparsed payload that was sent to us from the sender of the payment. - - `onion.per_hop_v0`: - - `realm` will always be `00` since that value determines that we are using - the `per_hop_v0` format. - - `short_channel_id` determines the channel that the sender is hinting - should be used next (set to `0x0x0` if we are the recipient of the - payment). - - `forward_amount` is the amount we should be forwarding to the next hop, + - `onion.type` is `legacy` for realm 0 payments, `tlv` for realm > 1. + - `short_channel_id` determines the channel that the sender is hinting + should be used next. Not present if we're the final destination. + - `forward_amount` is the amount we should be forwarding to the next hop, and should match the incoming funds in case we are the recipient. - - `outgoing_cltv_value` determines what the CLTV value for the HTLC that we + - `outgoing_cltv_value` determines what the CLTV value for the HTLC that we forward to the next hop should be. - `next_onion` is the fully processed onion that we should be sending to the next hop as part of the outgoing HTLC. Processed in this case means that we diff --git a/doc/lightning-getroute.7 b/doc/lightning-getroute.7 index 01b6f6dcaa45..7c245e4485c0 100644 --- a/doc/lightning-getroute.7 +++ b/doc/lightning-getroute.7 @@ -113,8 +113,9 @@ factor for larger amounts, and is basically ignored for tiny ones\. On success, a "route" array is returned\. Each array element contains \fIid\fR (the node being routed through), \fImsatoshi\fR (the millisatoshis -sent), \fIamount_msat\fR (the same, with \fImsat\fR appended), and \fIdelay\fR (the -number of blocks to timeout at this node)\. +sent), \fIamount_msat\fR (the same, with \fImsat\fR appended), \fIdelay\fR (the +number of blocks to timeout at this node), and \fIstyle\fR (indicating +the features which can be used for this hop)\. The final \fIid\fR will be the destination \fIid\fR given in the input\. The diff --git a/doc/lightning-getroute.7.md b/doc/lightning-getroute.7.md index 97a90d987318..84b056e99d21 100644 --- a/doc/lightning-getroute.7.md +++ b/doc/lightning-getroute.7.md @@ -279,8 +279,9 @@ RETURN VALUE On success, a "route" array is returned. Each array element contains *id* (the node being routed through), *msatoshi* (the millisatoshis -sent), *amount\_msat* (the same, with *msat* appended), and *delay* (the -number of blocks to timeout at this node). +sent), *amount\_msat* (the same, with *msat* appended), *delay* (the +number of blocks to timeout at this node), and *style* (indicating +the features which can be used for this hop). The final *id* will be the destination *id* given in the input. The difference between the first *msatoshi* minus the *msatoshi* given in diff --git a/gossipd/routing.c b/gossipd/routing.c index 48aaf5cc28cc..060485444405 100644 --- a/gossipd/routing.c +++ b/gossipd/routing.c @@ -380,6 +380,8 @@ static struct node *new_node(struct routing_state *rstate, n->id = *id; memset(n->chans.arr, 0, sizeof(n->chans.arr)); broadcastable_init(&n->bcast); + /* We don't know, so assume legacy. */ + n->hop_style = ROUTE_HOP_LEGACY; n->tokens = TOKEN_MAX; node_map_add(rstate->nodes, n); tal_add_destructor2(n, destroy_node, rstate); @@ -2510,6 +2512,12 @@ bool routing_add_node_announcement(struct routing_state *rstate, && node->bcast.timestamp < time_now().ts.tv_sec) rstate->last_timestamp = node->bcast.timestamp; + if (feature_offered(features, OPT_VAR_ONION)) + node->hop_style = ROUTE_HOP_TLV; + else + /* Reset it in case they no longer offer the feature */ + node->hop_style = ROUTE_HOP_LEGACY; + if (index) node->bcast.index = index; else { @@ -2707,6 +2715,7 @@ struct route_hop *get_route(const tal_t *ctx, struct routing_state *rstate, hops[i].amount = total_amount; hops[i].delay = total_delay; hops[i].direction = idx; + hops[i].style = n->hop_style; /* Since we calculated this route, it should not overflow! */ if (!amount_msat_add_fee(&total_amount, diff --git a/gossipd/routing.h b/gossipd/routing.h index cf994511e4e6..7c43bcea8088 100644 --- a/gossipd/routing.h +++ b/gossipd/routing.h @@ -133,6 +133,11 @@ HTABLE_DEFINE_TYPE(struct local_chan, local_chan_map_scid, hash_scid, local_chan_eq_scid, local_chan_map); +enum route_hop_style { + ROUTE_HOP_LEGACY = 1, + ROUTE_HOP_TLV = 2, +}; + /* For a small number of channels (by far the most common) we use a simple * array, with empty buckets NULL. For larger, we use a proper hash table, * with the extra allocation that implies. */ @@ -147,6 +152,9 @@ struct node { /* Token bucket */ u8 tokens; + /* route_hop_style */ + enum route_hop_style hop_style; + /* Channels connecting us to other nodes */ union { struct chan_map map; @@ -320,6 +328,7 @@ struct route_hop { struct node_id nodeid; struct amount_msat amount; u32 delay; + enum route_hop_style style; }; enum exclude_entry_type { diff --git a/lightningd/gossip_control.c b/lightningd/gossip_control.c index 927549d0b28e..67be0db43ebd 100644 --- a/lightningd/gossip_control.c +++ b/lightningd/gossip_control.c @@ -303,6 +303,49 @@ static const struct json_command listnodes_command = { }; AUTODATA(json_command, &listnodes_command); +static void json_add_route_hop_style(struct json_stream *response, + const char *fieldname, + enum route_hop_style style) +{ + switch (style) { + case ROUTE_HOP_LEGACY: + json_add_string(response, fieldname, "legacy"); + return; + case ROUTE_HOP_TLV: + json_add_string(response, fieldname, "tlv"); + return; + } + fatal("Unknown route_hop_style %u", style); +} + +/* Output a route hop */ +static void json_add_route_hop(struct json_stream *r, char const *n, + const struct route_hop *h) +{ + /* Imitate what getroute/sendpay use */ + json_object_start(r, n); + json_add_node_id(r, "id", &h->nodeid); + json_add_short_channel_id(r, "channel", + &h->channel_id); + json_add_num(r, "direction", h->direction); + json_add_amount_msat_compat(r, h->amount, "msatoshi", "amount_msat"); + json_add_num(r, "delay", h->delay); + json_add_route_hop_style(r, "style", h->style); + json_object_end(r); +} + +/* Output a route */ +static void json_add_route(struct json_stream *r, char const *n, + const struct route_hop *hops, size_t hops_len) +{ + size_t i; + json_array_start(r, n); + for (i = 0; i < hops_len; ++i) { + json_add_route_hop(r, NULL, &hops[i]); + } + json_array_end(r); +} + static void json_getroute_reply(struct subd *gossip UNUSED, const u8 *reply, const int *fds UNUSED, struct command *cmd) { diff --git a/lightningd/gossip_msg.c b/lightningd/gossip_msg.c index fd57099e614a..9b000f666e09 100644 --- a/lightningd/gossip_msg.c +++ b/lightningd/gossip_msg.c @@ -66,6 +66,7 @@ void fromwire_route_hop(const u8 **pptr, size_t *max, struct route_hop *entry) entry->direction = fromwire_u8(pptr, max); entry->amount = fromwire_amount_msat(pptr, max); entry->delay = fromwire_u32(pptr, max); + entry->style = fromwire_u8(pptr, max); } void towire_route_hop(u8 **pptr, const struct route_hop *entry) @@ -75,6 +76,7 @@ void towire_route_hop(u8 **pptr, const struct route_hop *entry) towire_u8(pptr, entry->direction); towire_amount_msat(pptr, entry->amount); towire_u32(pptr, entry->delay); + towire_u8(pptr, entry->style); } void fromwire_route_info(const u8 **pptr, size_t *max, struct route_info *entry) diff --git a/lightningd/json.c b/lightningd/json.c index fdbbd04371ce..8de71cdbf957 100644 --- a/lightningd/json.c +++ b/lightningd/json.c @@ -27,35 +27,6 @@ #include #include -/* Output a route hop */ -static void -json_add_route_hop(struct json_stream *r, char const *n, - const struct route_hop *h) -{ - /* Imitate what getroute/sendpay use */ - json_object_start(r, n); - json_add_node_id(r, "id", &h->nodeid); - json_add_short_channel_id(r, "channel", - &h->channel_id); - json_add_num(r, "direction", h->direction); - json_add_amount_msat_compat(r, h->amount, "msatoshi", "amount_msat"); - json_add_num(r, "delay", h->delay); - json_object_end(r); -} - -/* Output a route */ -void -json_add_route(struct json_stream *r, char const *n, - const struct route_hop *hops, size_t hops_len) -{ - size_t i; - json_array_start(r, n); - for (i = 0; i < hops_len; ++i) { - json_add_route_hop(r, NULL, &hops[i]); - } - json_array_end(r); -} - void json_add_node_id(struct json_stream *response, const char *fieldname, const struct node_id *id) diff --git a/lightningd/json.h b/lightningd/json.h index 165b821efa59..a47fe8e443ce 100644 --- a/lightningd/json.h +++ b/lightningd/json.h @@ -26,7 +26,6 @@ struct json_escape; struct json_stream; struct pubkey; struct node_id; -struct route_hop; struct sha256; struct short_channel_id; struct wallet_payment; @@ -34,10 +33,6 @@ struct wallet_tx; struct wireaddr; struct wireaddr_internal; -/* Output a route array. */ -void json_add_route(struct json_stream *r, char const *n, - const struct route_hop *hops, size_t hops_len); - /* '"fieldname" : "0289abcdef..."' or "0289abcdef..." if fieldname is NULL */ void json_add_pubkey(struct json_stream *response, const char *fieldname, diff --git a/lightningd/pay.c b/lightningd/pay.c index 83a3404b503e..8a9811c54a3e 100644 --- a/lightningd/pay.c +++ b/lightningd/pay.c @@ -615,6 +615,20 @@ static struct command_result *wait_payment(struct lightningd *ld, abort(); } +static bool should_use_tlv(enum route_hop_style style) +{ + switch (style) { + case ROUTE_HOP_TLV: +#if EXPERIMENTAL_FEATURES + return true; +#endif + /* Otherwise fall thru */ + case ROUTE_HOP_LEGACY: + return false; + } + abort(); +} + /* Returns command_result if cmd was resolved, NULL if not yet called. */ static struct command_result * send_payment(struct lightningd *ld, @@ -632,7 +646,6 @@ send_payment(struct lightningd *ld, struct secret *path_secrets; enum onion_type failcode; size_t i, n_hops = tal_count(route); - struct hop_data *hop_data = tal_arr(tmpctx, struct hop_data, n_hops); struct node_id *ids = tal_arr(tmpctx, struct node_id, n_hops); struct wallet_payment *payment = NULL; struct htlc_out *hout; @@ -640,40 +653,38 @@ send_payment(struct lightningd *ld, struct routing_failure *fail; struct channel *channel; struct sphinx_path *path; - struct short_channel_id finalscid; struct pubkey pubkey; bool ret; /* Expiry for HTLCs is absolute. And add one to give some margin. */ base_expiry = get_block_height(ld->topology) + 1; - memset(&finalscid, 0, sizeof(struct short_channel_id)); path = sphinx_path_new(tmpctx, rhash->u.u8); /* Extract IDs for each hop: create_onionpacket wants array. */ for (i = 0; i < n_hops; i++) ids[i] = route[i].nodeid; - /* Copy hop_data[n] from route[n+1] (ie. where it goes next) */ + /* Create sphinx path */ for (i = 0; i < n_hops - 1; i++) { ret = pubkey_from_node_id(&pubkey, &ids[i]); assert(ret); - hop_data[i].realm = 0; - hop_data[i].channel_id = route[i+1].channel_id; - hop_data[i].amt_forward = route[i+1].amount; - hop_data[i].outgoing_cltv = base_expiry + route[i+1].delay; - sphinx_add_v0_hop(path, &pubkey, &route[i + 1].channel_id, - route[i + 1].amount, - base_expiry + route[i + 1].delay); + + sphinx_add_nonfinal_hop(path, &pubkey, + should_use_tlv(route[i].style), + &route[i + 1].channel_id, + route[i + 1].amount, + base_expiry + route[i + 1].delay); } /* And finally set the final hop to the special values in * BOLT04 */ - memset(&finalscid, 0, sizeof(struct short_channel_id)); ret = pubkey_from_node_id(&pubkey, &ids[i]); assert(ret); - sphinx_add_v0_hop(path, &pubkey, &finalscid, - route[i].amount, - base_expiry + route[i].delay); + + sphinx_add_final_hop(path, &pubkey, + should_use_tlv(route[i].style), + route[i].amount, + base_expiry + route[i].delay); /* Now, do we already have a payment? */ payment = wallet_payment_by_hash(tmpctx, ld->wallet, rhash); @@ -793,6 +804,27 @@ send_payment(struct lightningd *ld, JSON-RPC sendpay interface -----------------------------------------------------------------------------*/ +static struct command_result *param_route_hop_style(struct command *cmd, + const char *name, + const char *buffer, + const jsmntok_t *tok, + enum route_hop_style **style) +{ + *style = tal(cmd, enum route_hop_style); + if (json_tok_streq(buffer, tok, "legacy")) { + **style = ROUTE_HOP_LEGACY; + return NULL; + } else if (json_tok_streq(buffer, tok, "tlv")) { + **style = ROUTE_HOP_TLV; + return NULL; + } + + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "'%s' should be a legacy or tlv, not '%.*s'", + name, json_tok_full_len(tok), + json_tok_full(buffer, tok)); +} + static struct command_result *json_sendpay(struct command *cmd, const char *buffer, const jsmntok_t *obj UNNEEDED, @@ -851,6 +883,7 @@ static struct command_result *json_sendpay(struct command *cmd, struct node_id *id; struct short_channel_id *channel; unsigned *delay, *direction; + enum route_hop_style *style; if (!param(cmd, buffer, t, /* Only *one* of these is required */ @@ -861,6 +894,8 @@ static struct command_result *json_sendpay(struct command *cmd, p_opt("delay", param_number, &delay), p_opt("channel", param_short_channel_id, &channel), p_opt("direction", param_number, &direction), + p_opt_def("style", param_route_hop_style, &style, + ROUTE_HOP_LEGACY), NULL)) return command_param_failed(); @@ -889,6 +924,7 @@ static struct command_result *json_sendpay(struct command *cmd, route[i].nodeid = *id; route[i].delay = *delay; route[i].channel_id = *channel; + route[i].style = *style; /* FIXME: Actually ignored by sending code! */ route[i].direction = direction ? *direction : 0; } diff --git a/lightningd/peer_htlcs.c b/lightningd/peer_htlcs.c index 4c14c31de9b2..20437102aaeb 100644 --- a/lightningd/peer_htlcs.c +++ b/lightningd/peer_htlcs.c @@ -636,6 +636,10 @@ struct htlc_accepted_hook_payload { struct htlc_in *hin; struct channel *channel; struct lightningd *ld; + struct amount_msat amt_to_forward; + u32 outgoing_cltv_value; + /* NULL if this is node is final */ + struct short_channel_id *short_channel_id; u8 *next_onion; }; @@ -726,14 +730,24 @@ static void htlc_accepted_hook_serialize(struct htlc_accepted_hook_payload *p, json_add_hex_talarr (s, "payload", rs->raw_payload); if (rs->type == SPHINX_V0_PAYLOAD) { - json_object_start(s, "per_hop_v0"); - json_add_string(s, "realm", "00"); - json_add_short_channel_id(s, "short_channel_id", &rs->payload.v0.channel_id); - json_add_amount_msat_only(s, "forward_amount", rs->payload.v0.amt_forward); - json_add_u64(s, "outgoing_cltv_value", rs->payload.v0.outgoing_cltv); - json_object_end(s); + if (deprecated_apis) { + json_object_start(s, "per_hop_v0"); + json_add_string(s, "realm", "00"); + json_add_short_channel_id(s, "short_channel_id", &rs->payload.v0.channel_id); + json_add_amount_msat_only(s, "forward_amount", rs->payload.v0.amt_forward); + json_add_u64(s, "outgoing_cltv_value", rs->payload.v0.outgoing_cltv); + json_object_end(s); + } + json_add_string(s, "type", "legacy"); + } else if (rs->type == SPHINX_TLV_PAYLOAD) { + json_add_string(s, "type", "tlv"); } + if (p->short_channel_id) + json_add_short_channel_id(s, "short_channel_id", + p->short_channel_id); + json_add_amount_msat_only(s, "forward_amount", p->amt_to_forward); + json_add_u32(s, "outgoing_cltv_value", p->outgoing_cltv_value); json_add_hex_talarr(s, "next_onion", p->next_onion); json_add_secret(s, "shared_secret", hin->shared_secret); json_object_end(s); @@ -762,19 +776,17 @@ htlc_accepted_hook_callback(struct htlc_accepted_hook_payload *request, enum htlc_accepted_result result; enum onion_type failure_code; u8 *channel_update; - struct hop_data *hop_data; result = htlc_accepted_hook_deserialize(buffer, toks, &payment_preimage, &failure_code, &channel_update); - hop_data = &rs->payload.v0; switch (result) { case htlc_accepted_continue: if (rs->nextcase == ONION_FORWARD) { struct gossip_resolve *gr = tal(ld, struct gossip_resolve); gr->next_onion = serialize_onionpacket(gr, rs->next); - gr->next_channel = hop_data->channel_id; - gr->amt_to_forward = hop_data->amt_forward; - gr->outgoing_cltv_value = hop_data->outgoing_cltv; + gr->next_channel = *request->short_channel_id; + gr->amt_to_forward = request->amt_to_forward; + gr->outgoing_cltv_value = request->outgoing_cltv_value; gr->hin = hin; req = towire_gossip_get_channel_peer(tmpctx, &gr->next_channel); @@ -785,13 +797,19 @@ htlc_accepted_hook_callback(struct htlc_accepted_hook_payload *request, channel_resolve_reply, gr); } else handle_localpay(hin, hin->cltv_expiry, &hin->payment_hash, - hop_data->amt_forward, - hop_data->outgoing_cltv); + request->amt_to_forward, + request->outgoing_cltv_value); break; case htlc_accepted_fail: log_debug(channel->log, "Failing incoming HTLC as instructed by plugin hook"); - fail_in_htlc(hin, failure_code, NULL, &hop_data->channel_id); + if ((failure_code & UPDATE) && rs->nextcase == ONION_END) { + log_broken(channel->log, + "htlc_acccepted hook: Can't return failure %u on last hop!", + failure_code); + failure_code = WIRE_TEMPORARY_NODE_FAILURE; + } + fail_in_htlc(hin, failure_code, NULL, request->short_channel_id); break; case htlc_accepted_resolve: fulfill_htlc(hin, &payment_preimage); @@ -897,16 +915,32 @@ static bool peer_accepted_htlc(struct channel *channel, u64 id, } /* Unknown realm isn't a bad onion, it's a normal failure. */ - /* FIXME: if we want hooks to handle foreign realms we should - * move this check to the hook callback. */ - if (rs->type != SPHINX_V0_PAYLOAD) { + if (rs->type == SPHINX_INVALID_PAYLOAD) { *failcode = WIRE_INVALID_REALM; goto out; } - /* It's time to package up all the information and call the - * hook so plugins can interject if they want */ hook_payload = tal(hin, struct htlc_accepted_hook_payload); + + if (rs->nextcase == ONION_END) { + if (!route_step_decode_end(rs, &hook_payload->amt_to_forward, + &hook_payload->outgoing_cltv_value)) { + *failcode = WIRE_INVALID_ONION_PAYLOAD; + goto out; + } + hook_payload->short_channel_id = NULL; + } else { + hook_payload->short_channel_id + = tal(hook_payload, struct short_channel_id); + if (!route_step_decode_forward(rs, + &hook_payload->amt_to_forward, + &hook_payload->outgoing_cltv_value, + hook_payload->short_channel_id)) { + *failcode = WIRE_INVALID_ONION_PAYLOAD; + goto out; + } + } + hook_payload->route_step = tal_steal(hook_payload, rs); hook_payload->ld = ld; hook_payload->hin = hin; diff --git a/tests/plugins/print_htlc_onion.py b/tests/plugins/print_htlc_onion.py new file mode 100755 index 000000000000..a97cc0ced6a1 --- /dev/null +++ b/tests/plugins/print_htlc_onion.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +"""Plugin that prints out HTLC onions. + +We use this to check whether they're TLV or not + +""" + +from lightning import Plugin + +plugin = Plugin() + + +@plugin.hook("htlc_accepted") +def on_htlc_accepted(htlc, onion, plugin, **kwargs): + plugin.log("Got onion {}".format(onion)) + return {'result': 'continue'} + + +plugin.run() diff --git a/tests/test_connection.py b/tests/test_connection.py index 5cdaad9cc6da..6fdcb173e7c6 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -4,7 +4,7 @@ from fixtures import TEST_NETWORK from flaky import flaky # noqa: F401 from lightning import RpcError -from utils import DEVELOPER, only_one, wait_for, sync_blockheight, VALGRIND, TIMEOUT, SLOW_MACHINE, COMPAT +from utils import DEVELOPER, only_one, wait_for, sync_blockheight, VALGRIND, TIMEOUT, SLOW_MACHINE, COMPAT, expected_features from bitcoin.core import CMutableTransaction, CMutableTxOut import binascii @@ -1564,7 +1564,7 @@ def test_forget_channel(node_factory): def test_peerinfo(node_factory, bitcoind): l1, l2 = node_factory.line_graph(2, fundchannel=False, opts={'may_reconnect': True}) - lfeatures = '28a2' + lfeatures = expected_features() # Gossiping but no node announcement yet assert l1.rpc.getpeer(l2.info['id'])['connected'] assert len(l1.rpc.getpeer(l2.info['id'])['channels']) == 0 @@ -1816,8 +1816,8 @@ def test_dataloss_protection(node_factory, bitcoind): l2 = node_factory.get_node(may_reconnect=True, log_all_io=True, feerates=(7500, 7500, 7500), allow_broken_log=True) - # features 1, 3, 7, 11 and 13 (0x28a2). - lf = "28a2" + lf = expected_features() + l1.rpc.connect(l2.info['id'], 'localhost', l2.port) # l1 should send out WIRE_INIT (0010) l1.daemon.wait_for_log(r"\[OUT\] 0010" diff --git a/tests/test_gossip.py b/tests/test_gossip.py index a041763049bb..b3008efc8280 100644 --- a/tests/test_gossip.py +++ b/tests/test_gossip.py @@ -2,7 +2,7 @@ from fixtures import * # noqa: F401,F403 from fixtures import TEST_NETWORK from lightning import RpcError -from utils import wait_for, TIMEOUT, only_one, sync_blockheight +from utils import wait_for, TIMEOUT, only_one, sync_blockheight, expected_features import json import logging @@ -1031,7 +1031,7 @@ def test_node_reannounce(node_factory, bitcoind): wait_for(lambda: 'alias' in only_one(l2.rpc.listnodes(l1.info['id'])['nodes'])) assert only_one(l2.rpc.listnodes(l1.info['id'])['nodes'])['alias'].startswith('JUNIORBEAM') - lfeatures = '28a2' + lfeatures = expected_features() # Make sure it gets features correct. assert only_one(l2.rpc.listnodes(l1.info['id'])['nodes'])['features'] == lfeatures diff --git a/tests/test_misc.py b/tests/test_misc.py index 54f134355242..b5a3b42f006b 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -5,7 +5,7 @@ from flaky import flaky # noqa: F401 from lightning import RpcError from threading import Event -from utils import DEVELOPER, TIMEOUT, VALGRIND, sync_blockheight, only_one, wait_for, TailableProc +from utils import DEVELOPER, TIMEOUT, VALGRIND, sync_blockheight, only_one, wait_for, TailableProc, EXPERIMENTAL_FEATURES from ephemeral_port_reserve import reserve import json @@ -1632,11 +1632,19 @@ def test_dev_demux(node_factory): def test_list_features_only(node_factory): features = subprocess.check_output(['lightningd/lightningd', '--list-features-only']).decode('utf-8').splitlines() - expected = ['option_data_loss_protect/odd', - 'option_upfront_shutdown_script/odd', - 'option_gossip_queries/odd', - 'option_gossip_queries_ex/odd', - 'option_static_remotekey/odd'] + if EXPERIMENTAL_FEATURES: + expected = ['option_data_loss_protect/odd', + 'option_upfront_shutdown_script/odd', + 'option_gossip_queries/odd', + 'option_var_onion_optin/odd', + 'option_gossip_queries_ex/odd', + 'option_static_remotekey/odd'] + else: + expected = ['option_data_loss_protect/odd', + 'option_upfront_shutdown_script/odd', + 'option_gossip_queries/odd', + 'option_gossip_queries_ex/odd', + 'option_static_remotekey/odd'] assert features == expected diff --git a/tests/test_pay.py b/tests/test_pay.py index a1ef1b0e8c23..0f2d3d7ee7f6 100644 --- a/tests/test_pay.py +++ b/tests/test_pay.py @@ -2,7 +2,7 @@ from fixtures import TEST_NETWORK from flaky import flaky # noqa: F401 from lightning import RpcError, Millisatoshi -from utils import DEVELOPER, wait_for, only_one, sync_blockheight, SLOW_MACHINE, TIMEOUT, VALGRIND +from utils import DEVELOPER, wait_for, only_one, sync_blockheight, SLOW_MACHINE, TIMEOUT, VALGRIND, EXPERIMENTAL_FEATURES import copy @@ -2346,6 +2346,71 @@ def test_error_returns_blockheight(node_factory, bitcoind): == '400f{:016x}{:08x}'.format(100, bitcoind.rpc.getblockcount())) +@unittest.skipIf(not DEVELOPER, 'Needs dev-routes') +def test_tlv_or_legacy(node_factory, bitcoind): + l1, l2, l3 = node_factory.get_nodes(3, + opts={'plugin': os.path.join(os.getcwd(), 'tests/plugins/print_htlc_onion.py')}) + + # Set up a channel from 1->2 first. + l1.rpc.connect(l2.info['id'], 'localhost', l2.port) + scid12 = l1.fund_channel(l2, 1000000) + + # Now set up 2->3. + l2.rpc.connect(l3.info['id'], 'localhost', l3.port) + scid23 = l2.fund_channel(l3, 1000000) + + # We need to force l3 to provide route hint from l2 (it won't normally, + # since it sees l2 as a dead end). + inv = l3.rpc.call('invoice', {"msatoshi": 10000, + "label": "test_tlv1", + "description": "test_tlv1", + "dev-routes": [[{'id': l2.info['id'], + 'short_channel_id': scid23, + 'fee_base_msat': 1, + 'fee_proportional_millionths': 10, + 'cltv_expiry_delta': 6}]]})['bolt11'] + l1.rpc.pay(inv) + + # Since L1 hasn't seen broadcast, it doesn't know they're TLV. + l2.daemon.wait_for_log("Got onion.*'type': 'legacy'") + l3.daemon.wait_for_log("Got onion.*'type': 'legacy'") + + # Turns out we only need 3 more blocks to announce l1->l2 channel. + bitcoind.generate_block(3) + + # Make sure l1 knows about l2 + wait_for(lambda: 'alias' in l1.rpc.listnodes(l2.info['id'])['nodes'][0]) + + # Make sure l3 knows about l1->l2, so it will add route hint now. + wait_for(lambda: len(l3.rpc.listchannels(scid12)['channels']) > 0) + + # Now it should send TLV to l2, but not l3. + inv = l3.rpc.invoice(10000, "test_tlv2", "test_tlv2")['bolt11'] + + l1.rpc.pay(inv) + if EXPERIMENTAL_FEATURES: + l2.daemon.wait_for_log("Got onion.*'type': 'tlv'") + else: + l2.daemon.wait_for_log("Got onion.*'type': 'legacy'") + l3.daemon.wait_for_log("Got onion.*'type': 'legacy'") + + # Now we finally announce l2->l3 channel, so l3 can announce tlv support. + bitcoind.generate_block(2) + + wait_for(lambda: len(l1.rpc.listnodes(l3.info['id'])['nodes']) > 0) + wait_for(lambda: 'alias' in l1.rpc.listnodes(l3.info['id'])['nodes'][0]) + + inv = l3.rpc.invoice(10000, "test_tlv3", "test_tlv3")['bolt11'] + + l1.rpc.pay(inv) + if EXPERIMENTAL_FEATURES: + l2.daemon.wait_for_log("Got onion.*'type': 'tlv'") + l3.daemon.wait_for_log("Got onion.*'type': 'tlv'") + else: + l2.daemon.wait_for_log("Got onion.*'type': 'legacy'") + l3.daemon.wait_for_log("Got onion.*'type': 'legacy'") + + @flaky def test_shadow_routing(node_factory): """ diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 811cc41f2acb..5929c578ecdd 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -2,7 +2,7 @@ from fixtures import * # noqa: F401,F403 from flaky import flaky # noqa: F401 from lightning import RpcError, Millisatoshi -from utils import DEVELOPER, only_one, sync_blockheight, TIMEOUT, wait_for +from utils import DEVELOPER, only_one, sync_blockheight, TIMEOUT, wait_for, EXPERIMENTAL_FEATURES import json import os @@ -534,11 +534,15 @@ def test_htlc_accepted_hook_forward_restart(node_factory, executor): logline = l2.daemon.wait_for_log(r'Onion written to') fname = re.search(r'Onion written to (.*\.json)', logline).group(1) onion = json.load(open(fname)) - assert re.match(r'^00006700000.000100000000000003e8000000..000000000000000000000000$', onion['payload']) - assert len(onion['payload']) == 64 + if EXPERIMENTAL_FEATURES: + assert onion['type'] == 'tlv' + assert re.match(r'^020203e80401..0608................$', onion['payload']) + else: + assert onion['type'] == 'legacy' + assert re.match(r'^00006700000.000100000000000003e8000000..000000000000000000000000$', onion['payload']) + assert len(onion['payload']) == 64 assert len(onion['shared_secret']) == 64 - assert onion['per_hop_v0']['realm'] == "00" - assert onion['per_hop_v0']['forward_amount'] == '1000msat' + assert onion['forward_amount'] == '1000msat' assert len(onion['next_onion']) == 2 * (1300 + 32 + 33 + 1) f1.result() diff --git a/tests/utils.py b/tests/utils.py index 98a7513c57a1..08352b81ffea 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -4,3 +4,13 @@ EXPERIMENTAL_FEATURES = env("EXPERIMENTAL_FEATURES", "0") == "1" COMPAT = env("COMPAT", "1") == "1" + + +def expected_features(): + """Return the expected features hexstring for this configuration""" + if EXPERIMENTAL_FEATURES: + # features 1, 3, 7, 9, 11 and 13 (0x2aa2). + return "2aa2" + else: + # features 1, 3, 7, 11 and 13 (0x28a2). + return "28a2" diff --git a/wallet/test/run-wallet.c b/wallet/test/run-wallet.c index f11d87792a96..6a66f6860bc2 100644 --- a/wallet/test/run-wallet.c +++ b/wallet/test/run-wallet.c @@ -535,6 +535,17 @@ struct route_step *process_onionpacket( const size_t assocdatalen ) { fprintf(stderr, "process_onionpacket called!\n"); abort(); } +/* Generated stub for route_step_decode_end */ +bool route_step_decode_end(const struct route_step *rs UNNEEDED, + struct amount_msat *amt_forward UNNEEDED, + u32 *outgoing_cltv UNNEEDED) +{ fprintf(stderr, "route_step_decode_end called!\n"); abort(); } +/* Generated stub for route_step_decode_forward */ +bool route_step_decode_forward(const struct route_step *rs UNNEEDED, + struct amount_msat *amt_forward UNNEEDED, + u32 *outgoing_cltv UNNEEDED, + struct short_channel_id *scid UNNEEDED) +{ fprintf(stderr, "route_step_decode_forward called!\n"); abort(); } /* Generated stub for serialize_onionpacket */ u8 *serialize_onionpacket( const tal_t *ctx UNNEEDED,