From 49a55108a9a1ae4c4f86fc16933eb6070a9190b1 Mon Sep 17 00:00:00 2001 From: Aditya Sharma Date: Fri, 16 May 2025 10:44:59 +0530 Subject: [PATCH 1/5] Add attribution data to onionreply and update_fail_htlc Adding the optional TLVs to update_fail_htlc and struct onionreply so that we can parse and store it. Key Changes: - Add tlvtype 'attribution_data' to 'update_fail_htlc' msgtype. - Add 'htlc_hold_time' & 'truncated_hmac' to struct onionreply. --- common/onionreply.c | 28 +++++++++++++++++++++++++++- common/onionreply.h | 9 ++++++++- wire/peer_wire.csv | 3 +++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/common/onionreply.c b/common/onionreply.c index 6a6e58ff96b2..52c607b4b23b 100644 --- a/common/onionreply.c +++ b/common/onionreply.c @@ -8,6 +8,10 @@ void towire_onionreply(u8 **cursor, const struct onionreply *r) { towire_u16(cursor, tal_count(r->contents)); towire_u8_array(cursor, r->contents, tal_count(r->contents)); + if (r->attr_data) { + towire_u8_array(cursor, r->attr_data->htlc_hold_time, 80); + towire_u8_array(cursor, r->attr_data->truncated_hmac, 840); + } } struct onionreply *fromwire_onionreply(const tal_t *ctx, @@ -16,6 +20,14 @@ struct onionreply *fromwire_onionreply(const tal_t *ctx, struct onionreply *r = tal(ctx, struct onionreply); r->contents = fromwire_tal_arrn(r, cursor, max, fromwire_u16(cursor, max)); + if (*max >= 80 + 840) { + r->attr_data = tal(ctx, struct attribution_data); + r->attr_data->htlc_hold_time = fromwire_tal_arrn(r, cursor, max, 80); + r->attr_data->truncated_hmac = fromwire_tal_arrn(r, cursor, max, 840); + } else { + r->attr_data = NULL; + } + if (!*cursor) return tal_free(r); return r; @@ -31,12 +43,26 @@ struct onionreply *dup_onionreply(const tal_t *ctx, n = tal(ctx, struct onionreply); n->contents = tal_dup_talarr(n, u8, r->contents); + if (r->attr_data) { + n->attr_data = tal(ctx, struct attribution_data); + n->attr_data->htlc_hold_time = tal_dup_talarr(r, u8, r->attr_data->htlc_hold_time); + n->attr_data->truncated_hmac = tal_dup_talarr(r, u8, r->attr_data->truncated_hmac); + } else { + n->attr_data = NULL; + } return n; } -struct onionreply *new_onionreply(const tal_t *ctx, const u8 *contents TAKES) +struct onionreply *new_onionreply(const tal_t *ctx, const u8 *contents TAKES, const struct attribution_data *attr_data TAKES) { struct onionreply *r = tal(ctx, struct onionreply); r->contents = tal_dup_talarr(r, u8, contents); + if (attr_data) { + r->attr_data = tal(ctx, struct attribution_data); + r->attr_data->htlc_hold_time = tal_dup_talarr(r, u8, attr_data->htlc_hold_time); + r->attr_data->truncated_hmac = tal_dup_talarr(r, u8, attr_data->truncated_hmac); + } else { + r->attr_data = NULL; + } return r; } diff --git a/common/onionreply.h b/common/onionreply.h index 759a4df18ecd..848994139a51 100644 --- a/common/onionreply.h +++ b/common/onionreply.h @@ -4,9 +4,16 @@ #include #include +/* A separate type for attribution data. */ +struct attribution_data { + u8 *htlc_hold_time; + u8 *truncated_hmac; +}; + /* A separate type for an onion reply, to differentiate from a wire msg. */ struct onionreply { u8 *contents; + struct attribution_data *attr_data; }; /** @@ -20,5 +27,5 @@ struct onionreply *fromwire_onionreply(const tal_t *ctx, struct onionreply *dup_onionreply(const tal_t *ctx, const struct onionreply *r TAKES); -struct onionreply *new_onionreply(const tal_t *ctx, const u8 *contents TAKES); +struct onionreply *new_onionreply(const tal_t *ctx, const u8 *contents TAKES, const struct attribution_data *attr_data TAKES); #endif /* LIGHTNING_COMMON_ONIONREPLY_H */ diff --git a/wire/peer_wire.csv b/wire/peer_wire.csv index b4b8230aff23..222744dc7cd1 100644 --- a/wire/peer_wire.csv +++ b/wire/peer_wire.csv @@ -288,6 +288,9 @@ msgdata,update_fail_htlc,channel_id,channel_id, msgdata,update_fail_htlc,id,u64, msgdata,update_fail_htlc,len,u16, msgdata,update_fail_htlc,reason,byte,len +tlvtype,update_fail_htlc_tlvs,attribution_data,1 +tlvdata,update_fail_htlc_tlvs,attribution_data,htlc_hold_times,u8,80 +tlvdata,update_fail_htlc_tlvs,attribution_data,truncated_hmacs,u8,840 msgtype,update_fail_malformed_htlc,135 msgdata,update_fail_malformed_htlc,channel_id,channel_id, msgdata,update_fail_malformed_htlc,id,u64, From 9eefe7929d68ce1df5c77fe3235077bc1b896114 Mon Sep 17 00:00:00 2001 From: Aditya Sharma Date: Fri, 16 May 2025 11:13:39 +0530 Subject: [PATCH 2/5] Calculate hold_time for attribution data We initialise time of creating htlc_out and calculate the time when this htlc_out is failed. Key Changes: - Add 'send_timestamp' to htlc_out. - Calculate hold_time when htlc_out is failed. --- lightningd/htlc_end.c | 4 +++- lightningd/htlc_end.h | 5 ++++- lightningd/peer_htlcs.c | 7 +++---- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/lightningd/htlc_end.c b/lightningd/htlc_end.c index 46083353877c..af7c3b3fa33b 100644 --- a/lightningd/htlc_end.c +++ b/lightningd/htlc_end.c @@ -269,7 +269,8 @@ struct htlc_out *new_htlc_out(const tal_t *ctx, struct amount_msat final_msat, u64 partid, u64 groupid, - struct htlc_in *in) + struct htlc_in *in, + struct timeabs send_timestamp) { struct htlc_out *hout = tal(ctx, struct htlc_out); @@ -305,6 +306,7 @@ struct htlc_out *new_htlc_out(const tal_t *ctx, } hout->in = NULL; + hout->send_timestamp = send_timestamp; if (in) { htlc_out_connect_htlc_in(hout, in); diff --git a/lightningd/htlc_end.h b/lightningd/htlc_end.h index 6b106caca9bf..d4a65cb635be 100644 --- a/lightningd/htlc_end.h +++ b/lightningd/htlc_end.h @@ -106,6 +106,8 @@ struct htlc_out { /* Timer we use in case they don't add an HTLC in a timely manner. */ struct oneshot *timeout; + + struct timeabs send_timestamp; }; static inline const struct htlc_key *keyof_htlc_in(const struct htlc_in *in) @@ -172,7 +174,8 @@ struct htlc_out *new_htlc_out(const tal_t *ctx, struct amount_msat final_msat, u64 partid, u64 groupid, - struct htlc_in *in); + struct htlc_in *in, + struct timeabs send_timestamp); void connect_htlc_in(struct htlc_in_map *map, struct htlc_in *hin); void connect_htlc_out(struct htlc_out_map *map, struct htlc_out *hout); diff --git a/lightningd/peer_htlcs.c b/lightningd/peer_htlcs.c index fc4a7063dcde..f183e0a76ede 100644 --- a/lightningd/peer_htlcs.c +++ b/lightningd/peer_htlcs.c @@ -313,8 +313,7 @@ static void fail_out_htlc(struct htlc_out *hout, const char *localfail) hout->failmsg, localfail); } else if (hout->in) { - const struct onionreply *failonion; - + const u32 hold_times = time_between(time_now(), hout->send_timestamp).ts.tv_nsec/1000000; /* If we have an onion, simply copy it. */ if (hout->failonion) failonion = hout->failonion; @@ -731,7 +730,7 @@ const u8 *send_htlc_out(const tal_t *ctx, payment_hash, onion_routing_packet, path_key, in == NULL, final_msat, - partid, groupid, in); + partid, groupid, in, time_now()); tal_add_destructor(*houtp, destroy_hout_subd_died); /* Give channel 30 seconds to commit this htlc. */ @@ -1074,7 +1073,7 @@ static bool htlc_accepted_hook_deserialize(struct htlc_accepted_hook_payload *re " Ignoring 'failure_message'."); fail_in_htlc(hin, take(new_onionreply(NULL, - failonion))); + failonion, NULL))); return false; } if (!failmsgtok) { From 7bf121f48bf82a267b3e01b248cd0617fa834660 Mon Sep 17 00:00:00 2001 From: Aditya Sharma Date: Fri, 16 May 2025 11:21:33 +0530 Subject: [PATCH 3/5] Add helpers for serializing and deserializing attr data Implemented methods to wrap attribution data, update the attribution data. Key Changes: - Use 'ammagext' subkey to crypt the attr data. - Write method to add HMACs to the attr data. - Update attribution data to a given onionreply. - Write method to verify attr data to find erring node. - Use all these helpers inside unwrap_onionreply(). --- common/sphinx.c | 137 +++++++++++++++++++- common/sphinx.h | 6 + common/test/run-blindedpath_onion.c | 2 +- common/test/run-onion-message-test.c | 2 +- common/test/run-onion-test-vector.c | 2 +- common/test/run-route_blinding_onion_test.c | 2 +- common/test/run-sphinx-xor_cipher_stream.c | 2 +- plugins/xpay/xpay.c | 2 +- wallet/test/run-wallet.c | 5 +- wire/test/run-peer-wire.c | 24 +++- 10 files changed, 169 insertions(+), 15 deletions(-) diff --git a/common/sphinx.c b/common/sphinx.c index c254e63170bc..1ec5f31fbd6d 100644 --- a/common/sphinx.c +++ b/common/sphinx.c @@ -818,6 +818,7 @@ struct onionreply *create_onionreply(const tal_t *ctx, towire(&reply->contents, payload, tal_count(payload)); tal_free(payload); + reply->attr_data = NULL; return reply; } @@ -825,7 +826,7 @@ struct onionreply *wrap_onionreply(const tal_t *ctx, const struct secret *shared_secret, const struct onionreply *reply) { - struct secret key; + struct secret key, attr_key; struct onionreply *result = tal(ctx, struct onionreply); /* BOLT #4: @@ -839,9 +840,109 @@ struct onionreply *wrap_onionreply(const tal_t *ctx, subkey_from_hmac("ammag", shared_secret, &key); result->contents = tal_dup_talarr(result, u8, reply->contents); xor_cipher_stream(result->contents, &key, tal_bytelen(result->contents)); + + if (reply->attr_data) { + result->attr_data = tal(ctx, struct attribution_data); + subkey_from_hmac("ammagext", shared_secret, &attr_key); + result->attr_data->htlc_hold_time = tal_dup_talarr(result, u8, reply->attr_data->htlc_hold_time); + xor_cipher_stream_off(&attr_key, 0, result->attr_data->htlc_hold_time, tal_bytelen(result->attr_data->htlc_hold_time)); + result->attr_data->truncated_hmac = tal_dup_talarr(result, u8, reply->attr_data->truncated_hmac); + xor_cipher_stream_off(&attr_key, + tal_bytelen(result->attr_data->htlc_hold_time), + result->attr_data->truncated_hmac, + tal_bytelen(result->attr_data->truncated_hmac)); + } else { + result->attr_data = NULL; + } + return result; } +static void write_downstream_hmacs(struct crypto_auth_hmacsha256_state *state, int pos, u8 *truncated_hmac) { + int hmac_idx = MAX_HOPS + MAX_HOPS - pos - 1; + for (int i = 0; i < pos; i++) { + hmac_update(state, truncated_hmac + (hmac_idx * TRUNC_HMAC_LEN), TRUNC_HMAC_LEN); + int block_size = MAX_HOPS - i - 1; + hmac_idx += block_size; + } +} + +static void add_hmacs_to_attribution_data(struct onionreply *failonion, struct secret *shared_secret) { + struct secret um_key; + subkey_from_hmac("um", shared_secret, &um_key); + for (int i = 0; i < MAX_HOPS; i++) { + struct crypto_auth_hmacsha256_state state; + struct hmac hmac; + int pos = MAX_HOPS - i - 1; + hmac_start(&state, um_key.data, sizeof(um_key.data)); + hmac_update(&state, failonion->contents, tal_bytelen(failonion->contents)); + hmac_update(&state, failonion->attr_data->htlc_hold_time, (pos + 1) * HOLD_TIME_LEN); + write_downstream_hmacs(&state, pos, failonion->attr_data->truncated_hmac); + hmac_done(&state, &hmac); + memcpy(failonion->attr_data->truncated_hmac + (i * TRUNC_HMAC_LEN), hmac.bytes, TRUNC_HMAC_LEN); + } + +} + +void update_attributable_data(struct onionreply *failonion, u32 hold_times, struct secret *shared_secret) { + if (!failonion->attr_data) { + failonion->attr_data = tal(failonion, struct attribution_data); + failonion->attr_data->htlc_hold_time = tal_arrz(failonion, u8, 80); + failonion->attr_data->truncated_hmac = tal_arrz(failonion, u8, 840); + } else { + /* Right shift */ + memmove(&failonion->attr_data->htlc_hold_time[HOLD_TIME_LEN], failonion->attr_data->htlc_hold_time, HOLD_TIME_LEN * (MAX_HOPS - 1)); + int src_index = HMAC_COUNT - 2; + int dest_index = HMAC_COUNT - 1; + int copy_len = 1; + + for (int i = 0; i < MAX_HOPS - 1; i++) { + memmove(&failonion->attr_data->truncated_hmac[dest_index * TRUNC_HMAC_LEN], + &failonion->attr_data->truncated_hmac[src_index * TRUNC_HMAC_LEN], + copy_len * TRUNC_HMAC_LEN); + if (i == MAX_HOPS - 2) + break; + + copy_len += 1; + src_index -= copy_len + 1; + dest_index -= copy_len; + } + } + u8 *hold_times_be = tal_arr(failonion, u8, 0); + towire_u32(&hold_times_be, hold_times); + memcpy(failonion->attr_data->htlc_hold_time, hold_times_be, HOLD_TIME_LEN); + add_hmacs_to_attribution_data(failonion, shared_secret); +} + +static u8 *verify_attr_data(struct onionreply *reply, + int pos, + const struct secret *shared_secret) +{ + struct secret um_key; + struct hmac hmac; + struct crypto_auth_hmacsha256_state state; + u8 expected_hmac[4], actual_hmac[4]; + + subkey_from_hmac("um", shared_secret, &um_key); + + hmac_start(&state, um_key.data, sizeof(um_key.data)); + hmac_update(&state, reply->contents, tal_bytelen(reply->contents)); + hmac_update(&state, reply->attr_data->htlc_hold_time, (pos + 1) * HOLD_TIME_LEN); + write_downstream_hmacs(&state, pos, reply->attr_data->truncated_hmac); + hmac_done(&state, &hmac); + memcpy(expected_hmac, hmac.bytes, TRUNC_HMAC_LEN); + + /* Compare with actual index. */ + int hmac_idx = MAX_HOPS - pos - 1; + memcpy(actual_hmac, reply->attr_data->truncated_hmac + hmac_idx * TRUNC_HMAC_LEN, TRUNC_HMAC_LEN); + + if (memcmp(actual_hmac, expected_hmac, 4) != 0) { + return NULL; + } + + return tal_dup_arr(reply, u8, reply->attr_data->htlc_hold_time, 4, 0); +} + u8 *unwrap_onionreply(const tal_t *ctx, const struct secret *shared_secrets, const int numhops, @@ -852,9 +953,9 @@ u8 *unwrap_onionreply(const tal_t *ctx, const u8 *cursor; size_t max; u16 msglen; - - r = new_onionreply(tmpctx, reply->contents); + r = new_onionreply(tmpctx, reply->contents, reply->attr_data); *origin_index = -1; + int attr_hop_count = numhops > 20 ? 20 : numhops; for (int i = 0; i < numhops; i++) { struct secret key; @@ -871,6 +972,36 @@ u8 *unwrap_onionreply(const tal_t *ctx, cursor = r->contents; max = tal_count(r->contents); + /* Check attribution error HMACs, If present. */ + if (r->attr_data) { + if (i < attr_hop_count) { + int pos = attr_hop_count - i - 1; + u8 *hop_time = verify_attr_data(r, pos, &shared_secrets[i]); + if (hop_time) { + /* Shift Left */ + memmove(r->attr_data->htlc_hold_time, &r->attr_data->htlc_hold_time[HOLD_TIME_LEN], HOLD_TIME_LEN * (MAX_HOPS - 1)); + + int src_index = MAX_HOPS; + int dest_index = 1; + int copy_len = MAX_HOPS - 1; + + for (int i = 0; i < MAX_HOPS - 1; i++) { + memmove(&r->attr_data->truncated_hmac[dest_index * TRUNC_HMAC_LEN], + &r->attr_data->truncated_hmac[src_index * TRUNC_HMAC_LEN], + copy_len * TRUNC_HMAC_LEN); + + src_index += copy_len; + dest_index += copy_len + 1; + copy_len -= 1; + } + // printf("Hop time at position %d: %s \n", i, tal_hexstr(tmpctx, hop_time, 4)); + } else { + /* FIXME: Add logging */ + // printf("Invalid HMAC at pos: %d", i); + } + } + } + fromwire_hmac(&cursor, &max, &hmac); /* Too short. */ if (!cursor) diff --git a/common/sphinx.h b/common/sphinx.h index 933e82627bb4..1d7878828d15 100644 --- a/common/sphinx.h +++ b/common/sphinx.h @@ -18,6 +18,11 @@ struct node_id; #define ROUTING_INFO_SIZE 1300 #define TOTAL_PACKET_SIZE(payload) (VERSION_SIZE + PUBKEY_SIZE + (payload) + HMAC_SIZE) +#define HOLD_TIME_LEN 4 +#define MAX_HOPS 20 +#define HMAC_COUNT 210 +#define TRUNC_HMAC_LEN 4 + struct onionpacket { /* Cleartext information */ u8 version; @@ -241,6 +246,7 @@ struct onionpacket *sphinx_decompress(const tal_t *ctx, const struct sphinx_compressed_onion *src, const struct secret *shared_secret); +void update_attributable_data(struct onionreply *failonion, u32 hold_times, struct secret *shared_secret); /** * Use ECDH to generate a shared secret from a privkey and a pubkey. * diff --git a/common/test/run-blindedpath_onion.c b/common/test/run-blindedpath_onion.c index fc6d015cc4d2..eb3370623c99 100644 --- a/common/test/run-blindedpath_onion.c +++ b/common/test/run-blindedpath_onion.c @@ -76,7 +76,7 @@ void fromwire_sciddir_or_pubkey(const u8 **cursor UNNEEDED, size_t *max UNNEEDED struct sciddir_or_pubkey *sciddpk UNNEEDED) { fprintf(stderr, "fromwire_sciddir_or_pubkey called!\n"); abort(); } /* Generated stub for new_onionreply */ -struct onionreply *new_onionreply(const tal_t *ctx UNNEEDED, const u8 *contents TAKES UNNEEDED) +struct onionreply *new_onionreply(const tal_t *ctx UNNEEDED, const u8 *contents TAKES UNNEEDED, const struct attribution_data *attr_data TAKES UNNEEDED) { fprintf(stderr, "new_onionreply called!\n"); abort(); } /* Generated stub for pubkey_from_node_id */ bool pubkey_from_node_id(struct pubkey *key UNNEEDED, const struct node_id *id UNNEEDED) diff --git a/common/test/run-onion-message-test.c b/common/test/run-onion-message-test.c index 7dba2c5cc2c6..6465f0a84b93 100644 --- a/common/test/run-onion-message-test.c +++ b/common/test/run-onion-message-test.c @@ -31,7 +31,7 @@ bool fromwire_channel_id(const u8 **cursor UNNEEDED, size_t *max UNNEEDED, void fromwire_node_id(const u8 **cursor UNNEEDED, size_t *max UNNEEDED, struct node_id *id UNNEEDED) { fprintf(stderr, "fromwire_node_id called!\n"); abort(); } /* Generated stub for new_onionreply */ -struct onionreply *new_onionreply(const tal_t *ctx UNNEEDED, const u8 *contents TAKES UNNEEDED) +struct onionreply *new_onionreply(const tal_t *ctx UNNEEDED, const u8 *contents TAKES UNNEEDED, const struct attribution_data *attr_data TAKES UNNEEDED) { fprintf(stderr, "new_onionreply called!\n"); abort(); } /* Generated stub for pubkey_from_node_id */ bool pubkey_from_node_id(struct pubkey *key UNNEEDED, const struct node_id *id UNNEEDED) diff --git a/common/test/run-onion-test-vector.c b/common/test/run-onion-test-vector.c index 130de79bb01d..54210783ea97 100644 --- a/common/test/run-onion-test-vector.c +++ b/common/test/run-onion-test-vector.c @@ -86,7 +86,7 @@ bool fromwire_tlv(const u8 **cursor UNNEEDED, size_t *max UNNEEDED, const char *mvt_tag_str(enum mvt_tag tag UNNEEDED) { fprintf(stderr, "mvt_tag_str called!\n"); abort(); } /* Generated stub for new_onionreply */ -struct onionreply *new_onionreply(const tal_t *ctx UNNEEDED, const u8 *contents TAKES UNNEEDED) +struct onionreply *new_onionreply(const tal_t *ctx UNNEEDED, const u8 *contents TAKES UNNEEDED, const struct attribution_data *attr_data TAKES UNNEEDED) { fprintf(stderr, "new_onionreply called!\n"); abort(); } /* Generated stub for node_id_from_hexstr */ bool node_id_from_hexstr(const char *str UNNEEDED, size_t slen UNNEEDED, struct node_id *id UNNEEDED) diff --git a/common/test/run-route_blinding_onion_test.c b/common/test/run-route_blinding_onion_test.c index 9c554ebd1bfa..fe22bf33b57a 100644 --- a/common/test/run-route_blinding_onion_test.c +++ b/common/test/run-route_blinding_onion_test.c @@ -33,7 +33,7 @@ void fromwire_sciddir_or_pubkey(const u8 **cursor UNNEEDED, size_t *max UNNEEDED const char *mvt_tag_str(enum mvt_tag tag UNNEEDED) { fprintf(stderr, "mvt_tag_str called!\n"); abort(); } /* Generated stub for new_onionreply */ -struct onionreply *new_onionreply(const tal_t *ctx UNNEEDED, const u8 *contents TAKES UNNEEDED) +struct onionreply *new_onionreply(const tal_t *ctx UNNEEDED, const u8 *contents TAKES UNNEEDED, const struct attribution_data *attr_data TAKES UNNEEDED) { fprintf(stderr, "new_onionreply called!\n"); abort(); } /* Generated stub for node_id_from_hexstr */ bool node_id_from_hexstr(const char *str UNNEEDED, size_t slen UNNEEDED, struct node_id *id UNNEEDED) diff --git a/common/test/run-sphinx-xor_cipher_stream.c b/common/test/run-sphinx-xor_cipher_stream.c index b5a30ef72699..25f6cc0b0a7d 100644 --- a/common/test/run-sphinx-xor_cipher_stream.c +++ b/common/test/run-sphinx-xor_cipher_stream.c @@ -98,7 +98,7 @@ void hmac_update(crypto_auth_hmacsha256_state *state UNNEEDED, const void *src UNNEEDED, size_t slen UNNEEDED) { fprintf(stderr, "hmac_update called!\n"); abort(); } /* Generated stub for new_onionreply */ -struct onionreply *new_onionreply(const tal_t *ctx UNNEEDED, const u8 *contents TAKES UNNEEDED) +struct onionreply *new_onionreply(const tal_t *ctx UNNEEDED, const u8 *contents TAKES UNNEEDED, const struct attribution_data *attr_data TAKES UNNEEDED) { fprintf(stderr, "new_onionreply called!\n"); abort(); } /* Generated stub for pubkey_from_node_id */ bool pubkey_from_node_id(struct pubkey *key UNNEEDED, const struct node_id *id UNNEEDED) diff --git a/plugins/xpay/xpay.c b/plugins/xpay/xpay.c index 2bf1c787d96f..6a033b6d20af 100644 --- a/plugins/xpay/xpay.c +++ b/plugins/xpay/xpay.c @@ -580,7 +580,7 @@ static void update_knowledge_from_error(struct command *aux_cmd, if (!tok) plugin_err(aux_cmd->plugin, "Invalid injectpaymentonion result '%.*s'", json_tok_full_len(error), json_tok_full(buf, error)); - reply = new_onionreply(tmpctx, take(json_tok_bin_from_hex(NULL, buf, tok))); + reply = new_onionreply(tmpctx, take(json_tok_bin_from_hex(NULL, buf, tok)), NULL); replymsg = unwrap_onionreply(tmpctx, attempt->shared_secrets, diff --git a/wallet/test/run-wallet.c b/wallet/test/run-wallet.c index 5787f640c25f..616dc404b91b 100644 --- a/wallet/test/run-wallet.c +++ b/wallet/test/run-wallet.c @@ -1218,6 +1218,9 @@ u8 *unwrap_onionreply(const tal_t *ctx UNNEEDED, const struct onionreply *reply UNNEEDED, int *origin_index UNNEEDED) { fprintf(stderr, "unwrap_onionreply called!\n"); abort(); } +/* Generated stub for update_attributable_data */ +void update_attributable_data(struct onionreply *failonion UNNEEDED, u32 hold_times UNNEEDED, struct secret *shared_secret UNNEEDED) +{ fprintf(stderr, "update_attributable_data called!\n"); abort(); } /* Generated stub for watch_opening_inflight */ void watch_opening_inflight(struct lightningd *ld UNNEEDED, struct channel_inflight *inflight UNNEEDED) @@ -2229,7 +2232,7 @@ static bool test_htlc_crud(struct lightningd *ld, const tal_t *ctx) CHECK_MSG( transaction_wrap(w->db, wallet_htlc_update(w, in.dbid, SENT_REMOVE_HTLC, &payment_key, 0, 0, NULL, NULL, &we_filled, in.key.id, in.key.channel, REMOTE, &in.payment_hash, in.cltv_expiry, in.msat)), "Update HTLC with payment_key failed"); - onionreply = new_onionreply(tmpctx, tal_arrz(tmpctx, u8, 100)); + onionreply = new_onionreply(tmpctx, tal_arrz(tmpctx, u8, 100), NULL); CHECK_MSG( transaction_wrap(w->db, wallet_htlc_update(w, in.dbid, SENT_REMOVE_HTLC, NULL, 0, 0, onionreply, NULL, &we_filled, in.key.id, in.key.channel, REMOTE, &in.payment_hash, in.cltv_expiry, in.msat)), "Update HTLC with failonion failed"); diff --git a/wire/test/run-peer-wire.c b/wire/test/run-peer-wire.c index cbb457ac27bb..57bc5d98cdf3 100644 --- a/wire/test/run-peer-wire.c +++ b/wire/test/run-peer-wire.c @@ -202,6 +202,8 @@ struct msg_update_fail_htlc { struct channel_id channel_id; u64 id; u8 *reason; + + struct tlv_update_fail_htlc_tlvs *tlvs; }; struct msg_channel_announcement { secp256k1_ecdsa_signature node_signature_1; @@ -488,7 +490,8 @@ static void *towire_struct_update_fail_htlc(const tal_t *ctx, return towire_update_fail_htlc(ctx, &s->channel_id, s->id, - s->reason); + s->reason, + s->tlvs); } static struct msg_update_fail_htlc *fromwire_struct_update_fail_htlc(const tal_t *ctx, const void *p) @@ -498,7 +501,8 @@ static struct msg_update_fail_htlc *fromwire_struct_update_fail_htlc(const tal_t if (!fromwire_update_fail_htlc(ctx, p, &s->channel_id, &s->id, - &s->reason)) + &s->reason, + &s->tlvs)) return tal_free(s); return s; @@ -784,12 +788,19 @@ static bool announcement_signatures_eq(const struct msg_announcement_signatures return eq_upto(a, b, short_channel_id) && short_channel_id_eq(a->short_channel_id, b->short_channel_id); } +static bool tlv_update_fail_htlc_eq(const struct tlv_update_fail_htlc_tlvs_attribution_data *a, + const struct tlv_update_fail_htlc_tlvs_attribution_data *b) +{ + return eq_field(a, b, htlc_hold_times) + && eq_field(a, b, truncated_hmacs); +} static bool update_fail_htlc_eq(const struct msg_update_fail_htlc *a, const struct msg_update_fail_htlc *b) { return eq_with(a, b, id) - && eq_var(a, b, reason); + && eq_var(a, b, reason) + && eq_tlv(a, b, attribution_data, tlv_update_fail_htlc_eq); } static bool tlv_splice_info_eq(const struct tlv_commitment_signed_tlvs_splice_info *a, @@ -1015,12 +1026,15 @@ int main(int argc, char *argv[]) memset(&ufh, 2, sizeof(ufh)); ufh.reason = tal_arr(ctx, u8, 2); - memset(ufh.reason, 2, 2); + ufh.tlvs = tlv_update_fail_htlc_tlvs_new(tmpctx); + ufh.tlvs->attribution_data = tal(ctx, struct tlv_update_fail_htlc_tlvs_attribution_data); + memset(ufh.tlvs->attribution_data, 3, sizeof(struct tlv_update_fail_htlc_tlvs_attribution_data)); + memset(ufh.reason, 2, 2); msg = towire_struct_update_fail_htlc(ctx, &ufh); ufh2 = fromwire_struct_update_fail_htlc(ctx, msg); assert(update_fail_htlc_eq(&ufh, ufh2)); - test_corruption(&ufh, ufh2, update_fail_htlc); + test_corruption_tlv(&ufh, ufh2, update_fail_htlc); memset(&cs, 2, sizeof(cs)); cs.htlc_signature = tal_arr(ctx, secp256k1_ecdsa_signature, 2); From 8152a0abfeb7eeee26c93b29785d613e821f689a Mon Sep 17 00:00:00 2001 From: Aditya Sharma Date: Fri, 16 May 2025 11:25:27 +0530 Subject: [PATCH 4/5] Serialize and deserialize attr data Sending attr data while sending update_fail_htlc and processing the attr data when received a update_fail_htlc msg. Key Changes: - Serializing updated attr data inside send_fail_or_fulfill. - Deserializing and processing the TLVs while parsing recvd update_fail_htlc msg. --- channeld/channeld.c | 16 +++++++++++++--- lightningd/peer_htlcs.c | 12 ++++++++++-- plugins/renepay/json.c | 2 +- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/channeld/channeld.c b/channeld/channeld.c index 37452adfe966..43cf55f19651 100644 --- a/channeld/channeld.c +++ b/channeld/channeld.c @@ -2577,10 +2577,11 @@ static void handle_peer_fail_htlc(struct peer *peer, const u8 *msg) u8 *reason; struct htlc *htlc; struct failed_htlc *f; + struct tlv_update_fail_htlc_tlvs *tlvs_attr_data; /* reason is not an onionreply because spec doesn't know about that */ if (!fromwire_update_fail_htlc(msg, msg, - &channel_id, &id, &reason)) { + &channel_id, &id, &reason, &tlvs_attr_data)) { peer_failed_warn(peer->pps, &peer->channel_id, "Bad update_fail_htlc %s", tal_hex(msg, msg)); } @@ -2591,7 +2592,12 @@ static void handle_peer_fail_htlc(struct peer *peer, const u8 *msg) htlc->failed = f = tal(htlc, struct failed_htlc); f->id = id; f->sha256_of_onion = NULL; - f->onion = new_onionreply(f, take(reason)); + struct attribution_data *attr = tal(f, struct attribution_data); + attr->htlc_hold_time = tal_dup_arr(f, u8, tlvs_attr_data->attribution_data->htlc_hold_times, 80, 0); + attr->truncated_hmac = tal_dup_arr(f, u8, tlvs_attr_data->attribution_data->truncated_hmacs, 840, 0); + f->onion = new_onionreply(f, take(reason), + attr); + start_commit_timer(peer); return; } @@ -4964,8 +4970,12 @@ static void send_fail_or_fulfill(struct peer *peer, const struct htlc *h) f->sha256_of_onion, f->badonion); } else { + struct tlv_update_fail_htlc_tlvs *attr_data_tlv = tlv_update_fail_htlc_tlvs_new(peer); + attr_data_tlv->attribution_data = tal(peer, struct tlv_update_fail_htlc_tlvs_attribution_data); + memcpy(attr_data_tlv->attribution_data->htlc_hold_times, f->onion->attr_data->htlc_hold_time, 80); + memcpy(attr_data_tlv->attribution_data->truncated_hmacs, f->onion->attr_data->truncated_hmac, 840); msg = towire_update_fail_htlc(peer, &peer->channel_id, h->id, - f->onion->contents); + f->onion->contents, attr_data_tlv); } } else if (h->r) { msg = towire_update_fulfill_htlc(NULL, &peer->channel_id, h->id, diff --git a/lightningd/peer_htlcs.c b/lightningd/peer_htlcs.c index f183e0a76ede..324a32880bbf 100644 --- a/lightningd/peer_htlcs.c +++ b/lightningd/peer_htlcs.c @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -10,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -313,16 +315,22 @@ static void fail_out_htlc(struct htlc_out *hout, const char *localfail) hout->failmsg, localfail); } else if (hout->in) { + const struct onionreply *final_failonion; + struct onionreply *failonion; const u32 hold_times = time_between(time_now(), hout->send_timestamp).ts.tv_nsec/1000000; /* If we have an onion, simply copy it. */ if (hout->failonion) - failonion = hout->failonion; + failonion = dup_onionreply(hout, hout->failonion); /* Otherwise, we need to onionize this local error. */ else failonion = create_onionreply(hout, hout->in->shared_secret, hout->failmsg); - fail_in_htlc(hout->in, failonion); + + update_attributable_data(failonion, hold_times, hout->in->shared_secret); + + final_failonion = failonion; + fail_in_htlc(hout->in, final_failonion); } else { log_broken(hout->key.channel->log, "Neither origin nor in?"); } diff --git a/plugins/renepay/json.c b/plugins/renepay/json.c index e48388fb308e..383dfe24184c 100644 --- a/plugins/renepay/json.c +++ b/plugins/renepay/json.c @@ -90,7 +90,7 @@ static bool get_data_details_onionreply(struct payment_result *result, goto fail; onionreply = new_onionreply( this_ctx, - take(json_tok_bin_from_hex(this_ctx, buffer, onionreplytok))); + take(json_tok_bin_from_hex(this_ctx, buffer, onionreplytok)), NULL); assert(onionreply); /* FIXME: It seems that lightningd will unwrap top portion of the * onionreply for us before serializing it, while unwrap_onionreply will From 5368e11a99e486e0598f5f6ab451001ad11d8bff Mon Sep 17 00:00:00 2001 From: Aditya Sharma Date: Fri, 16 May 2025 11:29:05 +0530 Subject: [PATCH 5/5] Add test to check Attr data processing Key changes: - Add test to check correct serialization and deserialization of attr data inside onionreply. --- common/test/run-sphinx.c | 44 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/common/test/run-sphinx.c b/common/test/run-sphinx.c index b44101f69e1e..82294ee49f09 100644 --- a/common/test/run-sphinx.c +++ b/common/test/run-sphinx.c @@ -233,11 +233,55 @@ static void run_unit_tests(void) assert(origin_index == 4); } +static void run_unit_tests_from_bolt(void) { + struct onionreply *reply; + u8 *oreply; + char *s = "400f0000000000000064000c3500fd84d1fd012c8080808080808080808"; + u8 *raw = tal_hexdata(tmpctx, s, strlen(s)); + int origin_index; + + /* Shared secrets we already have from the forward path */ + char *secrets[] = { + "53eb63ea8a3fec3b3cd433b85cd62a4b145e1dda09391b348c4e1cd36a03ea66", + "a6519e98832a0b179f62123b3567c106db99ee37bef036e783263602f3488fae", + "3a6b412548762f0dbccce5c7ae7bb8147d1caf9b5471c34120b30bc9c04891cc", + "21e13c2d7cfe7e18836df50872466117a295783ab8aab0e7ecc8c725503ad02d", + "b5756b9b542727dbafc6765a49488b023a725d631af688fc031217e90770c328", + }; + struct secret ss[] = { + secret_from_hex(secrets[0]), + secret_from_hex(secrets[1]), + secret_from_hex(secrets[2]), + secret_from_hex(secrets[3]), + secret_from_hex(secrets[4]) + }; + + reply = create_onionreply(tmpctx, &ss[4], raw); + update_attributable_data(reply, 1, &ss[4]); + reply = wrap_onionreply(tmpctx, &ss[4], reply); + + for (int i = 3; i >= 0; i--) { + u32 hold_time = 5 - i; + update_attributable_data(reply, hold_time, &ss[i]); + reply = wrap_onionreply(tmpctx, &ss[i], reply); + + printf("obfuscated packet %s\n", tal_hex(tmpctx, reply->contents)); + printf("obfuscated hold_times %s\n", tal_hex(tmpctx, reply->attr_data->htlc_hold_time)); + printf("obfuscated trunHMACs %s\n", tal_hex(tmpctx, reply->attr_data->truncated_hmac)); + } + + oreply = unwrap_onionreply(tmpctx, ss, 5, reply, &origin_index); + + assert(tal_arr_eq(oreply, raw)); + assert(origin_index == 4); +} + int main(int argc, char **argv) { common_setup(argv[0]); run_unit_tests(); + run_unit_tests_from_bolt(); common_shutdown(); return 0; }