diff --git a/channeld/channel_wire.csv b/channeld/channel_wire.csv index 20e270661d32..c7505f5af338 100644 --- a/channeld/channel_wire.csv +++ b/channeld/channel_wire.csv @@ -208,3 +208,20 @@ msgdata,channel_send_error,reason,wirestring, # Tell master channeld has sent the error message. msgtype,channel_send_error_reply,1108 + +# Tell lightningd we got a onion message (for us, or to fwd) +msgtype,got_onionmsg_to_us,1142 +msgdata,got_onionmsg_to_us,reply_blinding,?pubkey, +msgdata,got_onionmsg_to_us,reply_path_len,u16, +msgdata,got_onionmsg_to_us,reply_path,onionmsg_path,reply_path_len + +msgtype,got_onionmsg_forward,1143 +msgdata,got_onionmsg_forward,next_scid,?short_channel_id, +msgdata,got_onionmsg_forward,next_node_id,?node_id, +msgdata,got_onionmsg_forward,next_blinding,?pubkey, +msgdata,got_onionmsg_forward,next_onion,u8,1366 + +# Lightningd tells us to send a onion message. +msgtype,send_onionmsg,1040 +msgdata,send_onionmsg,onion,u8,1366 +msgdata,send_onionmsg,blinding,?pubkey, diff --git a/channeld/channeld.c b/channeld/channeld.c index c30786a419ca..a6643e9c2706 100644 --- a/channeld/channeld.c +++ b/channeld/channeld.c @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -54,6 +55,7 @@ #include #include #include +#include #include #include #include @@ -1621,6 +1623,281 @@ static bool channeld_handle_custommsg(const u8 *msg) #endif } +#if EXPERIMENTAL_FEATURES +/* H(E(i) || ss(i)) */ +static struct sha256 hash_e_and_ss(const struct pubkey *e, + const struct secret *ss) +{ + u8 der[PUBKEY_CMPR_LEN]; + struct sha256_ctx shactx; + struct sha256 h; + + pubkey_to_der(der, e); + sha256_init(&shactx); + sha256_update(&shactx, der, sizeof(der)); + sha256_update(&shactx, ss->data, sizeof(ss->data)); + sha256_done(&shactx, &h); + + return h; +} + +/* E(i-1) = H(E(i) || ss(i)) * E(i) */ +static struct pubkey next_pubkey(const struct pubkey *pk, + const struct sha256 *h) +{ + struct pubkey ret; + + ret = *pk; + if (secp256k1_ec_pubkey_tweak_mul(secp256k1_ctx, &ret.pubkey, h->u.u8) + != 1) + abort(); + + return ret; +} + +/* Peer sends onion msg. */ +static void handle_onion_message(struct peer *peer, const u8 *msg) +{ + enum onion_type badreason; + struct onionpacket op; + struct secret ss, *blinding_ss; + struct pubkey *blinding_in; + struct route_step *rs; + u8 onion[TOTAL_PACKET_SIZE]; + const u8 *cursor; + size_t max, maxlen; + struct tlv_onionmsg_payload *om; + const struct short_channel_id *next_scid; + struct node_id *next_node; + struct tlv_onion_message_tlvs *tlvs = tlv_onion_message_tlvs_new(msg); + + if (!fromwire_onion_message(msg, onion, tlvs)) + peer_failed(peer->pps, + &peer->channel_id, + "Bad onion_message %s", tal_hex(peer, msg)); + + /* We unwrap the onion now. */ + badreason = parse_onionpacket(onion, TOTAL_PACKET_SIZE, &op); + if (badreason != 0) { + status_debug("onion msg: can't parse onionpacket: %s", + onion_type_name(badreason)); + return; + } + + if (tlvs->blinding) { + struct secret hmac; + + /* E(i) */ + blinding_in = tal(msg, struct pubkey); + *blinding_in = tlvs->blinding->blinding; + status_debug("blinding in = %s", + type_to_string(tmpctx, struct pubkey, blinding_in)); + blinding_ss = tal(msg, struct secret); + msg = hsm_req(tmpctx, towire_hsm_ecdh_req(tmpctx, blinding_in)); + + if (!fromwire_hsm_ecdh_resp(msg, blinding_ss)) + status_failed(STATUS_FAIL_HSM_IO, + "Reading ecdh response for blinding"); + + /* b(i) = HMAC256("blinded_node_id", ss(i)) * k(i) */ + subkey_from_hmac("blinded_node_id", blinding_ss, &hmac); + + /* We instead tweak the *ephemeral* key from the onion and use + * our normal privkey: since hsmd knows only how to ECDH with + * our real key */ + if (secp256k1_ec_pubkey_tweak_mul(secp256k1_ctx, + &op.ephemeralkey.pubkey, + hmac.data) != 1) { + status_debug("onion msg: can't tweak pubkey"); + return; + } + } else { + blinding_ss = NULL; + blinding_in = NULL; + } + + msg = hsm_req(tmpctx, towire_hsm_ecdh_req(tmpctx, &op.ephemeralkey)); + if (!fromwire_hsm_ecdh_resp(msg, &ss)) + status_failed(STATUS_FAIL_HSM_IO, "Reading ecdh response"); + + /* We make sure we can parse onion packet, so we know if shared secret + * is actually valid (this checks hmac). */ + rs = process_onionpacket(tmpctx, &op, &ss, NULL, 0, false); + if (!rs) { + status_debug("onion msg: can't process onionpacket ss=%s", + type_to_string(tmpctx, struct secret, &ss)); + return; + } + + /* The raw payload is prepended with length in the TLV world. */ + cursor = rs->raw_payload; + max = tal_bytelen(rs->raw_payload); + maxlen = fromwire_bigsize(&cursor, &max); + if (!cursor) { + status_debug("onion msg: Invalid hop payload %s", + tal_hex(tmpctx, rs->raw_payload)); + return; + } + if (maxlen > max) { + status_debug("onion msg: overlong hop payload %s", + tal_hex(tmpctx, rs->raw_payload)); + return; + } + + om = tlv_onionmsg_payload_new(msg); + if (!fromwire_onionmsg_payload(&cursor, &maxlen, om)) { + status_debug("onion msg: invalid onionmsg_payload %s", + tal_hex(tmpctx, rs->raw_payload)); + return; + } + + /* If we weren't given a blinding factor, tlv can provide one. */ + if (om->blinding && !blinding_ss) { + /* E(i) */ + blinding_in = tal(msg, struct pubkey); + *blinding_in = om->blinding->blinding; + blinding_ss = tal(msg, struct secret); + + msg = hsm_req(tmpctx, towire_hsm_ecdh_req(tmpctx, blinding_in)); + + if (!fromwire_hsm_ecdh_resp(msg, blinding_ss)) + status_failed(STATUS_FAIL_HSM_IO, + "Reading ecdh response for om blinding"); + } + + if (om->enctlv) { + const unsigned char npub[crypto_aead_chacha20poly1305_ietf_NPUBBYTES] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + u8 *dec; + struct secret rho; + int ret; + + if (!blinding_ss) { + status_debug("enctlv but no blinding?"); + return; + } + + /* We need this to decrypt enctlv */ + subkey_from_hmac("rho", blinding_ss, &rho); + + /* Overrides next_scid / next_node */ + if (tal_bytelen(om->enctlv->enctlv) + < crypto_aead_chacha20poly1305_ietf_ABYTES) { + status_debug("enctlv too short for mac"); + return; + } + + dec = tal_arr(msg, u8, + tal_bytelen(om->enctlv->enctlv) + - crypto_aead_chacha20poly1305_ietf_ABYTES); + ret = crypto_aead_chacha20poly1305_ietf_decrypt(dec, NULL, + NULL, + om->enctlv->enctlv, + tal_bytelen(om->enctlv->enctlv), + NULL, 0, + npub, + rho.data); + if (ret != 0) + errx(1, "Failed to decrypt enctlv field"); + + status_debug("enctlv -> %s", tal_hex(tmpctx, dec)); + + /* Replace onionmsg with one from enctlv */ + cursor = dec; + maxlen = tal_bytelen(dec); + + om = tlv_onionmsg_payload_new(msg); + if (!fromwire_onionmsg_payload(&cursor, &maxlen, om)) { + status_debug("onion msg: invalid enctlv onionmsg_payload %s", + tal_hex(tmpctx, dec)); + return; + } + } else if (blinding_ss && rs->nextcase != ONION_END) { + status_debug("Onion had %s, but not enctlv?", + tlvs->blinding ? "blinding" : "om blinding"); + return; + } + + if (om->next_short_channel_id) + next_scid = &om->next_short_channel_id->short_channel_id; + else + next_scid = NULL; + + if (om->next_node_id) { + next_node = tal(msg, struct node_id); + node_id_from_pubkey(next_node, &om->next_node_id->node_id); + } else + next_node = NULL; + + if (om->enctlv) { + status_broken("FIXME: Handle enctlv!"); + return; + } + + if (rs->nextcase == ONION_END) { + struct pubkey *blinding; + const struct onionmsg_path **path; + + if (om->reply_path) { + blinding = &om->reply_path->blinding; + path = cast_const2(const struct onionmsg_path **, + om->reply_path->path); + } else { + blinding = NULL; + path = NULL; + } + wire_sync_write(MASTER_FD, + take(towire_got_onionmsg_to_us(NULL, + blinding, + path))); + } else { + struct pubkey *next_blinding; + + /* This *MUST* have instructions on where to go next. */ + if (!next_scid && !next_node) { + status_debug("onion msg: no next field in %s", + tal_hex(tmpctx, rs->raw_payload)); + return; + } + + if (blinding_ss) { + /* E(i-1) = H(E(i) || ss(i)) * E(i) */ + struct sha256 h = hash_e_and_ss(blinding_in, blinding_ss); + next_blinding = tal(msg, struct pubkey); + *next_blinding = next_pubkey(blinding_in, &h); + } else + next_blinding = NULL; + + wire_sync_write(MASTER_FD, + take(towire_got_onionmsg_forward(NULL, + next_scid, + next_node, + next_blinding, + serialize_onionpacket(tmpctx, rs->next)))); + } +} + +/* We send onion msg. */ +static void send_onionmsg(struct peer *peer, const u8 *msg) +{ + u8 onion_routing_packet[TOTAL_PACKET_SIZE]; + struct pubkey *blinding; + struct tlv_onion_message_tlvs *tlvs = tlv_onion_message_tlvs_new(msg); + + if (!fromwire_send_onionmsg(msg, msg, onion_routing_packet, &blinding)) + master_badmsg(WIRE_SEND_ONIONMSG, msg); + + if (blinding) { + tlvs->blinding = tal(tlvs, + struct tlv_onion_message_tlvs_blinding); + tlvs->blinding->blinding = *blinding; + } + sync_crypto_write(peer->pps, + take(towire_onion_message(NULL, + onion_routing_packet, + tlvs))); +} +#endif /* EXPERIMENTAL_FEATURES */ + static void peer_in(struct peer *peer, const u8 *msg) { enum wire_type type = fromwire_peektype(msg); @@ -1692,6 +1969,11 @@ static void peer_in(struct peer *peer, const u8 *msg) case WIRE_SHUTDOWN: handle_peer_shutdown(peer, msg); return; +#if EXPERIMENTAL_FEATURES + case WIRE_ONION_MESSAGE: + handle_onion_message(peer, msg); + return; +#endif case WIRE_INIT: case WIRE_OPEN_CHANNEL: @@ -2680,6 +2962,14 @@ static void req_in(struct peer *peer, const u8 *msg) case WIRE_CHANNEL_SEND_ERROR: handle_send_error(peer, msg); return; +#if EXPERIMENTAL_FEATURES + case WIRE_SEND_ONIONMSG: + send_onionmsg(peer, msg); + return; +#else + case WIRE_SEND_ONIONMSG: + break; +#endif /* !EXPERIMENTAL_FEATURES */ #if DEVELOPER case WIRE_CHANNEL_DEV_REENABLE_COMMIT: handle_dev_reenable_commit(peer); @@ -2707,6 +2997,8 @@ static void req_in(struct peer *peer, const u8 *msg) case WIRE_CHANNEL_FAIL_FALLEN_BEHIND: case WIRE_CHANNEL_DEV_MEMLEAK_REPLY: case WIRE_CHANNEL_SEND_ERROR_REPLY: + case WIRE_GOT_ONIONMSG_TO_US: + case WIRE_GOT_ONIONMSG_FORWARD: break; } diff --git a/devtools/Makefile b/devtools/Makefile index 0ca66b4f1579..a272feea5f13 100644 --- a/devtools/Makefile +++ b/devtools/Makefile @@ -1,6 +1,10 @@ DEVTOOLS_SRC := devtools/gen_print_wire.c devtools/gen_print_onion_wire.c devtools/print_wire.c DEVTOOLS_OBJS := $(DEVTOOLS_SRC:.c=.o) DEVTOOLS := devtools/bolt11-cli devtools/decodemsg devtools/onion devtools/dump-gossipstore devtools/gossipwith devtools/create-gossipstore devtools/mkcommit devtools/mkfunding devtools/mkclose devtools/mkgossip devtools/mkencoded devtools/checkchannels devtools/mkquery devtools/lightning-checkmessage +ifeq ($(EXPERIMENTAL_FEATURES),1) +DEVTOOLS += devtools/blindedpath +endif + DEVTOOLS_TOOL_SRC := $(DEVTOOLS:=.c) DEVTOOLS_TOOL_OBJS := $(DEVTOOLS_TOOL_SRC:.c=.o) @@ -66,6 +70,8 @@ devtools/onion.c: ccan/config.h devtools/onion: $(DEVTOOLS_OBJS) $(DEVTOOLS_COMMON_OBJS) $(JSMN_OBJS) $(CCAN_OBJS) $(BITCOIN_OBJS) wire/fromwire.o wire/towire.o devtools/onion.o common/sphinx.o +devtools/blindedpath: $(DEVTOOLS_OBJS) $(DEVTOOLS_COMMON_OBJS) $(JSMN_OBJS) $(CCAN_OBJS) $(BITCOIN_OBJS) wire/fromwire.o wire/towire.o devtools/blindedpath.o common/sphinx.o + devtools/gossipwith: $(DEVTOOLS_OBJS) $(DEVTOOLS_COMMON_OBJS) $(JSMN_OBJS) $(CCAN_OBJS) $(BITCOIN_OBJS) wire/fromwire.o wire/towire.o wire/gen_peer_wire.o devtools/gossipwith.o common/cryptomsg.o common/cryptomsg.o common/crypto_sync.o $(DEVTOOLS_OBJS) $(DEVTOOLS_TOOL_OBJS): wire/wire.h devtools/gen_print_wire.h devtools/gen_print_onion_wire.h diff --git a/devtools/blindedpath.c b/devtools/blindedpath.c new file mode 100644 index 000000000000..469ebebf8f42 --- /dev/null +++ b/devtools/blindedpath.c @@ -0,0 +1,336 @@ +#include "config.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static bool simpleout = false; + +/* Tal wrappers for opt. */ +static void *opt_allocfn(size_t size) +{ + return tal_arr_label(NULL, char, size, TAL_LABEL("opt_allocfn", "")); +} + +static void *tal_reallocfn(void *ptr, size_t size) +{ + if (!ptr) + return opt_allocfn(size); + tal_resize_(&ptr, 1, size, false); + return ptr; +} + +static void tal_freefn(void *ptr) +{ + tal_free(ptr); +} + +/* E(i-1) = H(E(i) || ss(i)) * E(i) */ +static struct sha256 hash_e_and_ss(const struct pubkey *e, + const struct secret *ss) +{ + u8 der[PUBKEY_CMPR_LEN]; + struct sha256_ctx shactx; + struct sha256 h; + + pubkey_to_der(der, e); + sha256_init(&shactx); + sha256_update(&shactx, der, sizeof(der)); + sha256_update(&shactx, ss->data, sizeof(ss->data)); + sha256_done(&shactx, &h); + + return h; +} + +/* E(i-1) = H(E(i) || ss(i)) * E(i) */ +static struct pubkey next_pubkey(const struct pubkey *pk, + const struct sha256 *h) +{ + struct pubkey ret; + + ret = *pk; + if (secp256k1_ec_pubkey_tweak_mul(secp256k1_ctx, &ret.pubkey, h->u.u8) + != 1) + abort(); + + return ret; +} + +/* e(i+1) = H(E(i) || ss(i)) * e(i) */ +static struct privkey next_privkey(const struct privkey *e, + const struct sha256 *h) +{ + struct privkey ret; + + ret = *e; + if (secp256k1_ec_privkey_tweak_mul(secp256k1_ctx, ret.secret.data, + h->u.u8) != 1) + abort(); + + return ret; +} + +int main(int argc, char **argv) +{ + bool first = false; + + setup_locale(); + + secp256k1_ctx = secp256k1_context_create(SECP256K1_CONTEXT_VERIFY | + SECP256K1_CONTEXT_SIGN); + + opt_set_alloc(opt_allocfn, tal_reallocfn, tal_freefn); + opt_register_noarg("--help|-h", opt_usage_and_exit, + "\n\n\tcreate [/]...\n" + "\tunwrap \n", + "Show this message"); + opt_register_noarg("--first-node", opt_set_bool, &first, + "Don't try to tweak key to unwrap onion"); + opt_register_noarg("--simple-output", opt_set_bool, &simpleout, + "Output values without prefixes, one per line"); + opt_register_version(); + + opt_parse(&argc, argv, opt_log_stderr_exit); + setup_tmpctx(); + + if (argc < 2) + errx(1, "You must specify create or unwrap"); + if (streq(argv[1], "create")) { + struct privkey e; + struct pubkey *pk_e, *b, *nodes; + struct secret *rho; + size_t num = argc - 2; + + if (argc < 3) + errx(1, "create requires at least one nodeid"); + + /* P(i) */ + nodes = tal_arr(tmpctx, struct pubkey, num); + /* E(i) */ + pk_e = tal_arr(tmpctx, struct pubkey, num); + /* B(i) */ + b = tal_arr(tmpctx, struct pubkey, num); + /* rho(i) */ + rho = tal_arr(tmpctx, struct secret, num); + + /* Randomness, chosen with a fair dice roll! */ + memset(&e, 6, sizeof(e)); + if (!pubkey_from_privkey(&e, &pk_e[0])) + abort(); + + for (size_t i = 0; i < num; i++) { + struct secret ss; + struct secret hmac; + struct sha256 h; + + if (!pubkey_from_hexstr(argv[2+i], + strcspn(argv[2+i], "/"), + &nodes[i])) + errx(1, "%s not a valid pubkey", argv[2+i]); + + if (secp256k1_ecdh(secp256k1_ctx, ss.data, + &nodes[i].pubkey, e.secret.data, NULL, NULL) != 1) + abort(); + + subkey_from_hmac("blinded_node_id", &ss, &hmac); + b[i] = nodes[i]; + if (i != 0) { + if (secp256k1_ec_pubkey_tweak_mul(secp256k1_ctx, + &b[i].pubkey, hmac.data) != 1) + abort(); + } + subkey_from_hmac("rho", &ss, &rho[i]); + h = hash_e_and_ss(&pk_e[i], &ss); + if (i != num-1) + pk_e[i+1] = next_pubkey(&pk_e[i], &h); + e = next_privkey(&e, &h); + } + + /* Print initial blinding factor */ + if (simpleout) + printf("%s\n", + type_to_string(tmpctx, struct pubkey, &pk_e[0])); + else + printf("Blinding: %s\n", + type_to_string(tmpctx, struct pubkey, &pk_e[0])); + + for (size_t i = 0; i < num - 1; i++) { + u8 *p; + u8 buf[BIGSIZE_MAX_LEN]; + const unsigned char npub[crypto_aead_chacha20poly1305_ietf_NPUBBYTES] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + struct tlv_onionmsg_payload *inner, *outer; + int ret; + + /* Inner is encrypted */ + inner = tlv_onionmsg_payload_new(tmpctx); + /* FIXME: Use /scid for encblob if specified */ + inner->next_node_id = tal(inner, struct tlv_onionmsg_payload_next_node_id); + inner->next_node_id->node_id = nodes[i+1]; + p = tal_arr(tmpctx, u8, 0); + towire_encmsg_tlvs(&p, inner); + + outer = tlv_onionmsg_payload_new(tmpctx); + outer->enctlv = tal(outer, struct tlv_onionmsg_payload_enctlv); + outer->enctlv->enctlv = tal_arr(tmpctx, u8, tal_count(p) + + crypto_aead_chacha20poly1305_ietf_ABYTES); + ret = crypto_aead_chacha20poly1305_ietf_encrypt(outer->enctlv->enctlv, NULL, + p, + tal_bytelen(p), + NULL, 0, + NULL, npub, + rho[i].data); + assert(ret == 0); + + p = tal_arr(tmpctx, u8, 0); + towire_onionmsg_payload(&p, outer); + ret = bigsize_put(buf, tal_bytelen(p)); + + if (simpleout) { + printf("%s\n%s\n", + type_to_string(tmpctx, struct pubkey, + &b[i]), + tal_hex(tmpctx, outer->enctlv->enctlv)); + } else { + /* devtools/onion wants length explicitly prepended */ + printf("%s/%.*s%s ", + type_to_string(tmpctx, struct pubkey, + &b[i]), + ret * 2, + tal_hexstr(tmpctx, buf, ret), + tal_hex(tmpctx, p)); + } + } + /* No payload for last node */ + if (simpleout) + printf("%s\n", + type_to_string(tmpctx, struct pubkey, &b[num-1])); + else + printf("%s/00\n", + type_to_string(tmpctx, struct pubkey, &b[num-1])); + } else if (streq(argv[1], "unwrap")) { + struct privkey privkey; + struct pubkey blinding; + u8 onion[TOTAL_PACKET_SIZE], *dec; + struct onionpacket op; + struct secret ss, onion_ss; + struct secret hmac, rho; + struct route_step *rs; + const u8 *cursor; + struct tlv_onionmsg_payload *outer; + size_t max, len; + struct pubkey res; + struct sha256 h; + int ret; + const unsigned char npub[crypto_aead_chacha20poly1305_ietf_NPUBBYTES] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + + if (argc != 5) + errx(1, "unwrap requires privkey, onion and blinding"); + + if (!hex_decode(argv[2], strlen(argv[2]), &privkey, + sizeof(privkey))) + errx(1, "Invalid private key hex '%s'", argv[2]); + + if (!hex_decode(argv[3], strlen(argv[3]), onion, + sizeof(onion))) + errx(1, "Invalid onion %s", argv[3]); + + if (!pubkey_from_hexstr(argv[4], strlen(argv[4]), &blinding)) + errx(1, "Invalid blinding %s", argv[4]); + + if (parse_onionpacket(onion, sizeof(onion), &op) != 0) + errx(1, "Unparsable onion"); + + /* ss(r) = H(k(r) * E(r)) */ + if (secp256k1_ecdh(secp256k1_ctx, ss.data, &blinding.pubkey, + privkey.secret.data, NULL, NULL) != 1) + abort(); + + subkey_from_hmac("rho", &ss, &rho); + + /* b(i) = HMAC256("blinded_node_id", ss(i)) * k(i) */ + subkey_from_hmac("blinded_node_id", &ss, &hmac); + + /* We instead tweak the *ephemeral* key from the onion + * and use our raw privkey: this models how lightningd + * will do it, since hsmd knows only how to ECDH with + * our real key */ + res = op.ephemeralkey; + if (!first) { + if (secp256k1_ec_pubkey_tweak_mul(secp256k1_ctx, + &res.pubkey, + hmac.data) != 1) + abort(); + } + + if (secp256k1_ecdh(secp256k1_ctx, onion_ss.data, + &res.pubkey, + privkey.secret.data, NULL, NULL) != 1) + abort(); + + rs = process_onionpacket(tmpctx, &op, &onion_ss, NULL, 0, false); + if (!rs) + errx(1, "Could not process onionpacket"); + + cursor = rs->raw_payload; + max = tal_bytelen(cursor); + len = fromwire_bigsize(&cursor, &max); + + /* Always true since we're non-legacy */ + assert(len == max); + outer = tlv_onionmsg_payload_new(tmpctx); + if (!fromwire_onionmsg_payload(&cursor, &max, outer)) + errx(1, "Invalid payload %s", + tal_hex(tmpctx, rs->raw_payload)); + + if (rs->nextcase == ONION_END) { + printf("TERMINAL\n"); + return 0; + } + + /* Look for enctlv */ + if (!outer->enctlv) + errx(1, "No enctlv field"); + + if (tal_bytelen(outer->enctlv->enctlv) + < crypto_aead_chacha20poly1305_ietf_ABYTES) + errx(1, "enctlv field too short"); + + dec = tal_arr(tmpctx, u8, + tal_bytelen(outer->enctlv->enctlv) + - crypto_aead_chacha20poly1305_ietf_ABYTES); + ret = crypto_aead_chacha20poly1305_ietf_decrypt(dec, NULL, + NULL, + outer->enctlv->enctlv, + tal_bytelen(outer->enctlv->enctlv), + NULL, 0, + npub, + rho.data); + if (ret != 0) + errx(1, "Failed to decrypt enctlv field"); + + printf("Contents: %s\n", tal_hex(tmpctx, dec)); + + /* E(i-1) = H(E(i) || ss(i)) * E(i) */ + h = hash_e_and_ss(&blinding, &ss); + res = next_pubkey(&blinding, &h); + printf("Next blinding: %s\n", + type_to_string(tmpctx, struct pubkey, &res)); + printf("Next onion: %s\n", tal_hex(tmpctx, serialize_onionpacket(tmpctx, rs->next))); + } else + errx(1, "Either create or unwrap!"); +} diff --git a/gossipd/gossipd.c b/gossipd/gossipd.c index dddb19f0b120..73d55187de21 100644 --- a/gossipd/gossipd.c +++ b/gossipd/gossipd.c @@ -488,6 +488,9 @@ static struct io_plan *peer_msg_in(struct io_conn *conn, case WIRE_CHANNEL_REESTABLISH: case WIRE_ANNOUNCEMENT_SIGNATURES: case WIRE_GOSSIP_TIMESTAMP_FILTER: +#if EXPERIMENTAL_FEATURES + case WIRE_ONION_MESSAGE: +#endif status_broken("peer %s: relayed unexpected msg of type %s", type_to_string(tmpctx, struct node_id, &peer->id), wire_type_name(fromwire_peektype(msg))); diff --git a/lightningd/Makefile b/lightningd/Makefile index 69d6e0d65ac4..2ed6c9fa917b 100644 --- a/lightningd/Makefile +++ b/lightningd/Makefile @@ -75,6 +75,7 @@ LIGHTNINGD_SRC := \ lightningd/channel_control.c \ lightningd/closing_control.c \ lightningd/connect_control.c \ + lightningd/onion_message.c \ lightningd/gossip_control.c \ lightningd/gossip_msg.c \ lightningd/hsm_control.c \ diff --git a/lightningd/channel_control.c b/lightningd/channel_control.c index aeac9f14a8a5..01abc330351f 100644 --- a/lightningd/channel_control.c +++ b/lightningd/channel_control.c @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -319,7 +320,17 @@ static unsigned channel_msg(struct subd *sd, const u8 *msg, const int *fds) case WIRE_CHANNEL_SEND_ERROR_REPLY: handle_error_channel(sd->channel, msg); break; - +#if EXPERIMENTAL_FEATURES + case WIRE_GOT_ONIONMSG_TO_US: + handle_onionmsg_to_us(sd->channel, msg); + break; + case WIRE_GOT_ONIONMSG_FORWARD: + handle_onionmsg_forward(sd->channel, msg); + break; +#else + case WIRE_GOT_ONIONMSG_TO_US: + case WIRE_GOT_ONIONMSG_FORWARD: +#endif /* And we never get these from channeld. */ case WIRE_CHANNEL_INIT: case WIRE_CHANNEL_FUNDING_DEPTH: @@ -334,7 +345,8 @@ static unsigned channel_msg(struct subd *sd, const u8 *msg, const int *fds) case WIRE_CHANNEL_FEERATES: case WIRE_CHANNEL_SPECIFIC_FEERATES: case WIRE_CHANNEL_DEV_MEMLEAK: - /* Replies go to requests. */ + case WIRE_SEND_ONIONMSG: + /* Replies go to requests. */ case WIRE_CHANNEL_OFFER_HTLC_REPLY: case WIRE_CHANNEL_DEV_REENABLE_COMMIT_REPLY: case WIRE_CHANNEL_DEV_MEMLEAK_REPLY: diff --git a/lightningd/onion_message.c b/lightningd/onion_message.c new file mode 100644 index 000000000000..efb7c303c38e --- /dev/null +++ b/lightningd/onion_message.c @@ -0,0 +1,386 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#if EXPERIMENTAL_FEATURES +struct onion_message_hook_payload { + /* Optional */ + struct pubkey *reply_blinding; + struct onionmsg_path **reply_path; + + /* FIXME: Include other TLV fields here! */ +}; + +static void +onion_message_serialize(struct onion_message_hook_payload *payload, + struct json_stream *stream) +{ + json_object_start(stream, "onion_message"); + if (payload->reply_path) { + json_array_start(stream, "reply_path"); + for (size_t i = 0; i < tal_count(payload->reply_path); i++) { + json_object_start(stream, NULL); + json_add_pubkey(stream, "id", + &payload->reply_path[i]->node_id); + if (payload->reply_path[i]->enctlv) + json_add_hex_talarr(stream, "enctlv", + payload->reply_path[i]->enctlv); + if (i == 0) + json_add_pubkey(stream, "blinding", + payload->reply_blinding); + json_object_end(stream); + } + json_array_end(stream); + } + json_object_end(stream); +} + +static void +onion_message_hook_cb(struct onion_message_hook_payload *payload, + const char *buffer, + const jsmntok_t *toks) +{ + /* The core infra checks the "result"; anything other than continue + * just stops. */ + tal_free(payload); +} + +REGISTER_PLUGIN_HOOK(onion_message, + PLUGIN_HOOK_CHAIN, + onion_message_hook_cb, + struct onion_message_hook_payload *, + onion_message_serialize, + struct onion_message_hook_payload *); + +/* Returns false if we can't tell it */ +static bool make_peer_send(struct lightningd *ld, + struct channel *dst, const u8 *msg TAKES) +{ + /* Take ownership of msg (noop if it's taken) */ + msg = tal_dup_talarr(tmpctx, u8, msg); + + if (!dst) { + log_debug(ld->log, "Can't send %s: no channel", + channel_wire_type_name(fromwire_peektype(msg))); + return false; + } + + if (!dst->owner) { + log_debug(ld->log, "Can't send %s: not connected", + channel_wire_type_name(fromwire_peektype(msg))); + return false; + } + + /* FIXME: We should allow this for closingd too, and we should + * allow incoming via openingd!. */ + if (!streq(dst->owner->name, "channeld")) { + log_debug(ld->log, "Can't send %s: owned by %s", + channel_wire_type_name(fromwire_peektype(msg)), + dst->owner->name); + return false; + } + subd_send_msg(dst->owner, take(msg)); + return true; +} + +void handle_onionmsg_to_us(struct channel *channel, const u8 *msg) +{ + struct lightningd *ld = channel->peer->ld; + struct onion_message_hook_payload *payload; + + payload = tal(ld, struct onion_message_hook_payload); + + if (!fromwire_got_onionmsg_to_us(payload, msg, + &payload->reply_blinding, + &payload->reply_path)) { + channel_internal_error(channel, "bad got_onionmsg_tous: %s", + tal_hex(tmpctx, msg)); + return; + } + + if (payload->reply_path && !payload->reply_blinding) { + log_broken(channel->log, + "No reply blinding, ignoring reply path"); + payload->reply_path = tal_free(payload->reply_path); + } + + log_debug(channel->log, "Got onionmsg%s%s", + payload->reply_blinding ? " reply_blinding": "", + payload->reply_path ? " reply_path": ""); + plugin_hook_call_onion_message(ld, payload, payload); +} + +void handle_onionmsg_forward(struct channel *channel, const u8 *msg) +{ + struct lightningd *ld = channel->peer->ld; + struct short_channel_id *next_scid; + struct node_id *next_node; + struct pubkey *next_blinding; + u8 onion[TOTAL_PACKET_SIZE]; + struct channel *outchan; + + if (!fromwire_got_onionmsg_forward(msg, msg, &next_scid, &next_node, + &next_blinding, onion)) { + channel_internal_error(channel, "bad got_onionmsg_forward: %s", + tal_hex(tmpctx, msg)); + return; + } + + if (next_scid) + outchan = active_channel_by_scid(ld, next_scid); + else if (next_node) { + struct peer *p = peer_by_id(ld, next_node); + if (p) + outchan = peer_active_channel(p); + else + outchan = NULL; + } else + outchan = NULL; + + make_peer_send(ld, outchan, + take(towire_send_onionmsg(NULL, onion, next_blinding))); +} + +struct hop { + struct pubkey id; + struct short_channel_id *scid; + struct pubkey *blinding; + u8 *enctlv; + u8 *rawtlv; +}; + +static struct command_result *param_hops(struct command *cmd, + const char *name, + const char *buffer, + const jsmntok_t *tok, + struct hop **hops) +{ + size_t i; + const jsmntok_t *t; + + if (tok->type != JSMN_ARRAY || tok->size == 0) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "%s must be an (non-empty) array", name); + + *hops = tal_arr(cmd, struct hop, tok->size); + json_for_each_arr(i, t, tok) { + const jsmntok_t *tid, *tscid, *tblinding, *tenctlv, *trawtlv; + + tid = json_get_member(buffer, t, "id"); + if (!tid) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "%s[%zu] does not have 'id'", + name, i); + tscid = json_get_member(buffer, t, "short_channel_id"); + tblinding = json_get_member(buffer, t, "blinding"); + tenctlv = json_get_member(buffer, t, "enctlv"); + trawtlv = json_get_member(buffer, t, "rawtlv"); + + if (trawtlv && (tscid || tblinding || tenctlv)) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "%s[%zu] has 'rawtlv' with other fields", + name, i); + + if (tblinding) { + (*hops)[i].blinding = tal(*hops, struct pubkey); + if (!json_to_pubkey(buffer, tblinding, + (*hops)[i].blinding)) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "%s[%zu] 'blinding' is invalid", name, i); + } else + (*hops)[i].blinding = NULL; + + if (!json_to_pubkey(buffer, tid, &(*hops)[i].id)) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "%s[%zu] 'id' is invalid", name, i); + if (tscid) { + (*hops)[i].scid = tal(*hops, struct short_channel_id); + if (!json_to_short_channel_id(buffer, tscid, + (*hops)[i].scid)) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "%s[%zu] 'short_channel_id' is invalid", name, i); + } else + (*hops)[i].scid = NULL; + + if (tenctlv) { + (*hops)[i].enctlv = + json_tok_bin_from_hex(*hops, buffer, tenctlv); + if (!(*hops)[i].enctlv) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "%s[%zu] 'enctlv' is invalid", name, i); + } else + (*hops)[i].enctlv = NULL; + + if (trawtlv) { + (*hops)[i].rawtlv = + json_tok_bin_from_hex(*hops, buffer, trawtlv); + if (!(*hops)[i].rawtlv) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "%s[%zu] 'rawtlv' is invalid", name, i); + } else + (*hops)[i].rawtlv = NULL; + } + return NULL; +} + +static struct command_result *param_reply_path(struct command *cmd, + const char *name, + const char *buffer, + const jsmntok_t *tok, + struct tlv_onionmsg_payload_reply_path **reply_path) +{ + const jsmntok_t *tblinding, *tpath, *t; + size_t i; + + *reply_path = tal(cmd, struct tlv_onionmsg_payload_reply_path); + tblinding = json_get_member(buffer, tok, "blinding"); + if (!tblinding) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "%s has no 'blinding'", name); + if (!json_to_pubkey(buffer, tblinding, &(*reply_path)->blinding)) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "%s 'blinding' invalid pubkey", name); + + tpath = json_get_member(buffer, tok, "path"); + if (!tpath || tpath->type != JSMN_ARRAY) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "%s has no 'path' array", name); + + (*reply_path)->path = tal_arr(*reply_path, struct onionmsg_path *, + tpath->size); + json_for_each_arr(i, t, tpath) { + const jsmntok_t *tid, *tenctlv; + struct onionmsg_path *path; + + path = (*reply_path)->path[i] = tal((*reply_path)->path, + struct onionmsg_path); + tid = json_get_member(buffer, t, "id"); + if (!tid) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "%s path[%zu] 'id' is missing", + name, i); + if (!json_to_pubkey(buffer, tid, &path->node_id)) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "%s path[%zu] 'id' is invalid", + name, i); + + tenctlv = json_get_member(buffer, t, "enctlv"); + if (!tenctlv) { + /* Optional for final destination */ + if (i != tpath->size - 1) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "%s path[%zu] 'enctlv' is missing", + name, i); + path->enctlv = NULL; + } else { + path->enctlv = json_tok_bin_from_hex(path, + buffer, tenctlv); + if (!path->enctlv) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "%s path[%zu] 'enctlv' is invalid", + name, i); + } + } + + return NULL; +} + +/* Generate ->rawtlv if not already supplied. */ +static void populate_tlvs(struct hop *hops, + struct tlv_onionmsg_payload_reply_path *reply_path) +{ + for (size_t i = 0; i < tal_count(hops); i++) { + struct tlv_onionmsg_payload *tlv; + + if (hops[i].rawtlv) + continue; + + tlv = tlv_onionmsg_payload_new(tmpctx); + /* If they don't give scid, use next node id */ + if (hops[i].scid) { + tlv->next_short_channel_id = tal(tlv, struct tlv_onionmsg_payload_next_short_channel_id); + tlv->next_short_channel_id->short_channel_id = *hops[i].scid; + } else if (i != tal_count(hops)-1) { + tlv->next_node_id = tal(tlv, struct tlv_onionmsg_payload_next_node_id); + tlv->next_node_id->node_id = hops[i+1].id; + } + if (hops[i].blinding) { + tlv->blinding = tal(tlv, struct tlv_onionmsg_payload_blinding); + tlv->blinding->blinding = *hops[i].blinding; + } + if (hops[i].enctlv) { + tlv->enctlv = tal(tlv, struct tlv_onionmsg_payload_enctlv); + tlv->enctlv->enctlv = hops[i].enctlv; + } + + if (i == tal_count(hops)-1 && reply_path) + tlv->reply_path = reply_path; + + hops[i].rawtlv = tal_arr(hops, u8, 0); + towire_onionmsg_payload(&hops[i].rawtlv, tlv); + } +} + +static struct command_result *json_send_onion_message(struct command *cmd, + const char *buffer, + const jsmntok_t *obj UNNEEDED, + const jsmntok_t *params) +{ + struct hop *hops; + struct tlv_onionmsg_payload_reply_path *reply_path; + struct sphinx_path *sphinx_path; + struct onionpacket *op; + struct secret *path_secrets; + struct channel *first_hop; + struct node_id first_id; + + if (!param(cmd, buffer, params, + p_req("hops", param_hops, &hops), + p_opt("reply_path", param_reply_path, &reply_path), + NULL)) + return command_param_failed(); + + /* FIXME: Allow sending to non-channel peers! */ + node_id_from_pubkey(&first_id, &hops[0].id); + first_hop = active_channel_by_id(cmd->ld, &first_id, NULL); + if (!first_hop) + return command_fail(cmd, LIGHTNINGD, "Unknown first peer"); + + /* Create an onion which encodes this. */ + populate_tlvs(hops, reply_path); + sphinx_path = sphinx_path_new(cmd, NULL); + for (size_t i = 0; i < tal_count(hops); i++) { + /* FIXME: Remove legacy, then length prefix can be removed! */ + u8 *tlv_with_len = tal_arr(NULL, u8, 0); + towire_bigsize(&tlv_with_len, tal_bytelen(hops[i].rawtlv)); + towire_u8_array(&tlv_with_len, + hops[i].rawtlv, tal_bytelen(hops[i].rawtlv)); + sphinx_add_hop(sphinx_path, &hops[i].id, take(tlv_with_len)); + } + op = create_onionpacket(tmpctx, sphinx_path, &path_secrets); + if (!op) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Creating onion failed (tlvs too long?)"); + + if (!make_peer_send(cmd->ld, first_hop, + take(towire_send_onionmsg(NULL, + serialize_onionpacket(tmpctx, op), + NULL)))) + return command_fail(cmd, LIGHTNINGD, "First peer not ready"); + + return command_success(cmd, json_stream_success(cmd)); +} + +static const struct json_command send_onion_message_command = { + "sendonionmessage", + "utility", + json_send_onion_message, + "Send message over {hops} (id, [short_channel_id], [blinding], [enctlv], [rawtlv]) with optional {reply_path} (blinding, path[id, enctlv])" +}; +AUTODATA(json_command, &send_onion_message_command); +#endif /* EXPERIMENTAL_FEATURES */ diff --git a/lightningd/onion_message.h b/lightningd/onion_message.h new file mode 100644 index 000000000000..c2a2099a8ebb --- /dev/null +++ b/lightningd/onion_message.h @@ -0,0 +1,11 @@ +#ifndef LIGHTNING_LIGHTNINGD_ONION_MESSAGE_H +#define LIGHTNING_LIGHTNINGD_ONION_MESSAGE_H +#include "config.h" +#include + +struct channel; + +void handle_onionmsg_to_us(struct channel *channel, const u8 *msg); +void handle_onionmsg_forward(struct channel *channel, const u8 *msg); + +#endif /* LIGHTNING_LIGHTNINGD_ONION_MESSAGE_H */ diff --git a/lightningd/pay.c b/lightningd/pay.c index 28980e038a0a..cce8c641f68d 100644 --- a/lightningd/pay.c +++ b/lightningd/pay.c @@ -1195,38 +1195,21 @@ static struct command_result *param_route_hop_style(struct command *cmd, json_tok_full(buffer, tok)); } -static struct command_result *json_sendpay(struct command *cmd, - const char *buffer, - const jsmntok_t *obj UNNEEDED, - const jsmntok_t *params) +static struct command_result *param_route_hops(struct command *cmd, + const char *name, + const char *buffer, + const jsmntok_t *tok, + struct route_hop **hops) { - const jsmntok_t *routetok; - const jsmntok_t *t; size_t i; - struct sha256 *rhash; - struct route_hop *route; - struct amount_msat *msat; - const char *b11str, *label; - u64 *partid; - struct secret *payment_secret; - - /* For generating help, give new-style. */ - if (!param(cmd, buffer, params, - p_req("route", param_array, &routetok), - p_req("payment_hash", param_sha256, &rhash), - p_opt("label", param_escaped_string, &label), - p_opt("msatoshi", param_msat, &msat), - p_opt("bolt11", param_string, &b11str), - p_opt("payment_secret", param_secret, &payment_secret), - p_opt_def("partid", param_u64, &partid, 0), - NULL)) - return command_param_failed(); + const jsmntok_t *t; - if (routetok->size == 0) - return command_fail(cmd, JSONRPC2_INVALID_PARAMS, "Empty route"); + if (tok->type != JSMN_ARRAY || tok->size == 0) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "%s must be an (non-empty) array", name); - route = tal_arr(cmd, struct route_hop, routetok->size); - json_for_each_arr(i, t, routetok) { + *hops = tal_arr(cmd, struct route_hop, tok->size); + json_for_each_arr(i, t, tok) { struct amount_msat *msat, *amount_msat; struct node_id *id; struct short_channel_id *channel; @@ -1249,16 +1232,16 @@ static struct command_result *json_sendpay(struct command *cmd, if (!msat && !amount_msat) return command_fail(cmd, JSONRPC2_INVALID_PARAMS, - "route[%zi]: must have msatoshi" - " or amount_msat", i); + "%s[%zi]: must have msatoshi" + " or amount_msat", name, i); if (!id || !channel || !delay) return command_fail(cmd, JSONRPC2_INVALID_PARAMS, - "route[%zi]: must have id, channel" - " and delay", i); + "%s[%zi]: must have id, channel" + " and delay", name, i); if (msat && amount_msat && !amount_msat_eq(*msat, *amount_msat)) return command_fail(cmd, JSONRPC2_INVALID_PARAMS, - "route[%zi]: msatoshi %s != amount_msat %s", - i, + "%s[%zi]: msatoshi %s != amount_msat %s", + name, i, type_to_string(tmpctx, struct amount_msat, msat), @@ -1268,20 +1251,47 @@ static struct command_result *json_sendpay(struct command *cmd, if (!msat) msat = amount_msat; - route[i].amount = *msat; - route[i].nodeid = *id; - route[i].delay = *delay; - route[i].channel_id = *channel; - route[i].style = *style; + (*hops)[i].amount = *msat; + (*hops)[i].nodeid = *id; + (*hops)[i].delay = *delay; + (*hops)[i].channel_id = *channel; + (*hops)[i].style = *style; /* FIXME: Actually ignored by sending code! */ - route[i].direction = direction ? *direction : 0; + (*hops)[i].direction = direction ? *direction : 0; } + return NULL; +} + +static struct command_result *json_sendpay(struct command *cmd, + const char *buffer, + const jsmntok_t *obj UNNEEDED, + const jsmntok_t *params) +{ + struct sha256 *rhash; + struct route_hop *route; + struct amount_msat *msat; + const char *b11str, *label; + u64 *partid; + struct secret *payment_secret; + + /* For generating help, give new-style. */ + if (!param(cmd, buffer, params, + p_req("route", param_route_hops, &route), + p_req("payment_hash", param_sha256, &rhash), + p_opt("label", param_escaped_string, &label), + p_opt("msatoshi", param_msat, &msat), + p_opt("bolt11", param_string, &b11str), + p_opt("payment_secret", param_secret, &payment_secret), + p_opt_def("partid", param_u64, &partid, 0), + NULL)) + return command_param_failed(); + if (*partid && !msat) return command_fail(cmd, JSONRPC2_INVALID_PARAMS, "Must specify msatoshi with partid"); - const struct amount_msat final_amount = route[routetok->size-1].amount; + const struct amount_msat final_amount = route[tal_count(route)-1].amount; if (msat && !*partid && !amount_msat_eq(*msat, final_amount)) return command_fail(cmd, JSONRPC2_INVALID_PARAMS, diff --git a/tests/plugins/onionmessage-reply.py b/tests/plugins/onionmessage-reply.py new file mode 100755 index 000000000000..66ad0611c84a --- /dev/null +++ b/tests/plugins/onionmessage-reply.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +""" +This plugin is used to test the `onion_message` hook. +""" +from lightning import Plugin + +plugin = Plugin() + + +@plugin.hook("onion_message") +def on_onion_message(plugin, onion_message, **kwargs): + if 'reply_path' not in onion_message: + plugin.log("no reply path") + return + + plugin.rpc.call('sendonionmessage', [onion_message['reply_path']]) + plugin.log("Sent reply via {}".format(onion_message['reply_path'])) + return {"result": "continue"} + + +plugin.run() diff --git a/tests/test_misc.py b/tests/test_misc.py index 5a34aaeeb504..66eba7485565 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -10,6 +10,7 @@ TailableProc, env ) from ephemeral_port_reserve import reserve +from utils import EXPERIMENTAL_FEATURES import json import os @@ -2177,6 +2178,75 @@ def test_sendcustommsg(node_factory): serialized=serialized, peer_id=l2.info['id'])) +@unittest.skipIf(not EXPERIMENTAL_FEATURES, "Needs sendonionmessage") +def test_sendonionmessage(node_factory): + l1, l2, l3 = node_factory.line_graph(3) + + blindedpathtool = os.path.join(os.path.dirname(__file__), "..", "devtools", "blindedpath") + + l1.rpc.call('sendonionmessage', + {'hops': + [{'id': l2.info['id']}, + {'id': l3.info['id']}]}) + assert l3.daemon.wait_for_log('Got onionmsg') + + # Now by SCID. + l1.rpc.call('sendonionmessage', + {'hops': + [{'id': l2.info['id'], + 'short_channel_id': l2.get_channel_scid(l3)}, + {'id': l3.info['id']}]}) + assert l3.daemon.wait_for_log('Got onionmsg') + + # Now test blinded path. + output = subprocess.check_output( + [blindedpathtool, '--simple-output', 'create', l2.info['id'], l3.info['id']] + ).decode('ASCII').strip() + + # First line is blinding, then then . + blinding, p1, p1enc, p2 = output.split('\n') + # First hop can't be blinded! + assert p1 == l2.info['id'] + + l1.rpc.call('sendonionmessage', + {'hops': + [{'id': l2.info['id'], + 'blinding': blinding, + 'enctlv': p1enc}, + {'id': p2}]}) + assert l3.daemon.wait_for_log('Got onionmsg') + + +@unittest.skipIf(not EXPERIMENTAL_FEATURES, "Needs sendonionmessage") +def test_sendonionmessage_reply(node_factory): + blindedpathtool = os.path.join(os.path.dirname(__file__), "..", "devtools", "blindedpath") + + plugin = os.path.join(os.path.dirname(__file__), "plugins", "onionmessage-reply.py") + l1, l2, l3 = node_factory.line_graph(3, opts={'plugin': plugin}) + + # Make reply path + output = subprocess.check_output( + [blindedpathtool, '--simple-output', 'create', l2.info['id'], l1.info['id']] + ).decode('ASCII').strip() + + # First line is blinding, then then . + blinding, p1, p1enc, p2 = output.split('\n') + # First hop can't be blinded! + assert p1 == l2.info['id'] + + l1.rpc.call('sendonionmessage', + {'hops': + [{'id': l2.info['id']}, + {'id': l3.info['id']}], + 'reply_path': + {'blinding': blinding, + 'path': [{'id': p1, 'enctlv': p1enc}, {'id': p2}]}}) + + assert l3.daemon.wait_for_log('Got onionmsg reply_blinding reply_path') + assert l3.daemon.wait_for_log('Sent reply via') + assert l1.daemon.wait_for_log('Got onionmsg') + + @unittest.skipIf(not DEVELOPER, "needs --dev-force-privkey") def test_getsharedsecret(node_factory): """ diff --git a/tools/check-spelling.sh b/tools/check-spelling.sh index a9f88697eeb0..d82a77b2e47c 100755 --- a/tools/check-spelling.sh +++ b/tools/check-spelling.sh @@ -6,7 +6,7 @@ if git --no-pager grep -nHiE 'l[ightn]{6}g|l[ightn]{8}g|ilghtning|lgihtning|lihg exit 1 fi -if git --no-pager grep -nHiE 'ctlv' -- . ':!tools/check-spelling.sh'; then +if git --no-pager grep -nHiE 'ctlv' | grep -v 'enctlv' -- . ':!tools/check-spelling.sh'; then echo "It's check lock time verify, not check time lock verify!" >&2 exit 1 fi diff --git a/tools/generate-wire.py b/tools/generate-wire.py index 547c769efb28..2c2958e19b9f 100755 --- a/tools/generate-wire.py +++ b/tools/generate-wire.py @@ -226,6 +226,7 @@ class Type(FieldSet): 'onionreply', 'witscript', 'feature_set', + 'onionmsg_path', ] # Some BOLT types are re-typed based on their field name diff --git a/wire/extracted_onion_experimental_1b1c9a71038bd72453cabefd3ace17ce8b8dd96e b/wire/extracted_onion_experimental_1b1c9a71038bd72453cabefd3ace17ce8b8dd96e new file mode 100644 index 000000000000..3b8ebbec89a1 --- /dev/null +++ b/wire/extracted_onion_experimental_1b1c9a71038bd72453cabefd3ace17ce8b8dd96e @@ -0,0 +1,28 @@ +--- wire/extracted_onion_wire_csv 2020-03-25 10:24:12.861645774 +1030 ++++ - 2020-03-26 13:47:13.498294435 +1030 +@@ -8,6 +8,25 @@ + tlvtype,tlv_payload,payment_data,8 + tlvdata,tlv_payload,payment_data,payment_secret,byte,32 + tlvdata,tlv_payload,payment_data,total_msat,tu64, ++tlvtype,onionmsg_payload,next_node_id,4 ++tlvdata,onionmsg_payload,next_node_id,node_id,point, ++tlvtype,onionmsg_payload,next_short_channel_id,6 ++tlvdata,onionmsg_payload,next_short_channel_id,short_channel_id,short_channel_id, ++tlvtype,onionmsg_payload,reply_path,8 ++tlvdata,onionmsg_payload,reply_path,blinding,point, ++tlvdata,onionmsg_payload,reply_path,path,onionmsg_path,... ++tlvtype,onionmsg_payload,enctlv,10 ++tlvdata,onionmsg_payload,enctlv,enctlv,byte,... ++tlvtype,onionmsg_payload,blinding,12 ++tlvdata,onionmsg_payload,blinding,blinding,point, ++tlvtype,encmsg_tlvs,next_node_id,4 ++tlvdata,encmsg_tlvs,next_node_id,node_id,point, ++tlvtype,encmsg_tlvs,next_short_channel_id,6 ++tlvdata,encmsg_tlvs,next_short_channel_id,short_channel_id,short_channel_id, ++subtype,onionmsg_path ++subtypedata,onionmsg_path,node_id,point, ++subtypedata,onionmsg_path,enclen,u16, ++subtypedata,onionmsg_path,enctlv,byte,enclen + msgtype,invalid_realm,PERM|1 + msgtype,temporary_node_failure,NODE|2 + msgtype,permanent_node_failure,PERM|NODE|2 diff --git a/wire/extracted_peer_experimental_1b1c9a71038bd72453cabefd3ace17ce8b8dd96e b/wire/extracted_peer_experimental_1b1c9a71038bd72453cabefd3ace17ce8b8dd96e new file mode 100644 index 000000000000..2cb91f2f9691 --- /dev/null +++ b/wire/extracted_peer_experimental_1b1c9a71038bd72453cabefd3ace17ce8b8dd96e @@ -0,0 +1,11 @@ +--- wire/extracted_peer_wire_csv 2020-03-11 10:30:35.744376417 +1030 ++++ - 2020-03-26 13:47:13.409755567 +1030 +@@ -211,3 +211,8 @@ + msgdata,gossip_timestamp_filter,chain_hash,chain_hash, + msgdata,gossip_timestamp_filter,first_timestamp,u32, + msgdata,gossip_timestamp_filter,timestamp_range,u32, ++msgtype,onion_message,385,option_onion_messages ++msgdata,onion_message,onionmsg,byte,1366 ++msgdata,onion_message,onion_message_tlvs,onion_message_tlvs, ++tlvtype,onion_message_tlvs,blinding,2 ++tlvdata,onion_message_tlvs,blinding,blinding,point, diff --git a/wire/peer_wire.c b/wire/peer_wire.c index e9210d7d8434..737e2fbb6caa 100644 --- a/wire/peer_wire.c +++ b/wire/peer_wire.c @@ -31,6 +31,9 @@ static bool unknown_type(enum wire_type t) case WIRE_QUERY_CHANNEL_RANGE: case WIRE_REPLY_CHANNEL_RANGE: case WIRE_GOSSIP_TIMESTAMP_FILTER: +#if EXPERIMENTAL_FEATURES + case WIRE_ONION_MESSAGE: +#endif return false; } return true; @@ -68,6 +71,9 @@ bool is_msg_for_gossipd(const u8 *cursor) case WIRE_CHANNEL_REESTABLISH: case WIRE_ANNOUNCEMENT_SIGNATURES: case WIRE_GOSSIP_TIMESTAMP_FILTER: +#if EXPERIMENTAL_FEATURES + case WIRE_ONION_MESSAGE: +#endif break; } return false; diff --git a/wire/wire.h b/wire/wire.h index ea380a937419..a9bc8e89d9d7 100644 --- a/wire/wire.h +++ b/wire/wire.h @@ -153,4 +153,18 @@ struct witscript *fromwire_witscript(const tal_t *ctx, void fromwire_chainparams(const u8 **cursor, size_t *max, const struct chainparams **chainparams); +#if !EXPERIMENTAL_FEATURES +/* Stubs, as this subtype is only defined when EXPERIMENTAL_FEATURES */ +struct onionmsg_path; + +static inline void towire_onionmsg_path(u8 **p, const struct onionmsg_path *onionmsg_path) +{ +} + +static inline struct onionmsg_path * +fromwire_onionmsg_path(const tal_t *ctx, const u8 **cursor, size_t *plen) +{ + return NULL; +} +#endif /* EXPERIMENTAL_FEATURES */ #endif /* LIGHTNING_WIRE_WIRE_H */