Skip to content

bolt11 decode wraps the amount around u64 for invoices of 2^64 pico-bitcoin or more #9666

Open

Description

@Nuhiat-Arefin

Issue and Steps to Reproduce

bolt11_decode_nosig multiplies the amount by its multiplier in an unchecked u64, so the product wraps for any amount of 2^64 pico-bitcoin or more, about 18,446,744.07 BTC. This happens on master at 1ebc494 and in v26.06.9.

This test goes in common/test/run-bolt11-amount.c. It signs two invoices with the BOLT 11 example key, changes only the amount between them, and decodes each.

#include "config.h"
#include "../amount.c"
#include "../bech32.c"
#include "../bech32_util.c"
#include "../bolt11.c"
#include "../features.c"
#include "../node_id.c"
#include "../hash_u5.c"
#include "../wire/fromwire.c"
#include "../wire/towire.c"
#include <ccan/err/err.h>
#include <ccan/str/str.h>
#include <common/setup.h>
#include <stdio.h>

/* AUTOGENERATED MOCKS START */
/* Generated stub for siphash_seed */
const struct siphash_seed *siphash_seed(void)
{ fprintf(stderr, "siphash_seed called!\n"); abort(); }
/* AUTOGENERATED MOCKS END */

static struct privkey privkey;

/* As in run-bolt11.c: sign with the BOLT #11 example key. */
static bool test_sign(const u5 *u5bytes,
		      const u8 *hrpu8,
		      secp256k1_ecdsa_recoverable_signature *rsig,
		      void *unused UNUSED)
{
	struct hash_u5 hu5;
	char *hrp;
	struct sha256 sha;

	hrp = tal_dup_arr(NULL, char, (char *)hrpu8, tal_count(hrpu8), 1);
	hrp[tal_count(hrpu8)] = '\0';

	hash_u5_init(&hu5, hrp);
	hash_u5(&hu5, u5bytes, tal_count(u5bytes));
	hash_u5_done(&hu5, &sha);
	tal_free(hrp);

	if (!secp256k1_ecdsa_sign_recoverable(secp256k1_ctx, rsig,
					      (const u8 *)&sha,
					      privkey.secret.data,
					      NULL, NULL))
		abort();

	return true;
}

/* BOLT #11:
 *
 * > ### Please send $3 for a cup of coffee to the same peer, within one minute
 *
 * We take that example's fields, sign them with the example key, and change
 * only the amount.  msat is NULL to leave the amount out of struct bolt11 and
 * carry it in the prefix instead, for an amount too large to fit a u64 msat.
 */
static struct bolt11 *encode_then_decode(const struct chainparams *chain,
					 const struct amount_msat *msat,
					 const char **enc, const char **fail)
{
	struct bolt11 *b11 = new_bolt11(tmpctx, msat);

	b11->chain = chain;
	b11->timestamp = 1496314658;
	b11->payment_secret = tal(b11, struct secret);
	memset(b11->payment_secret, 0x11, sizeof(*b11->payment_secret));
	if (!hex_decode("0001020304050607080900010203040506070809000102030405060708090102",
			strlen("0001020304050607080900010203040506070809000102030405060708090102"),
			&b11->payment_hash, sizeof(b11->payment_hash)))
		abort();
	b11->description = "1 cup coffee";
	b11->expiry = 60;
	set_feature_bit(&b11->features, 8);
	set_feature_bit(&b11->features, 14);

	dev_bolt11_omit_c_value = true;
	*enc = bolt11_encode(tmpctx, b11, false, test_sign, NULL);
	dev_bolt11_omit_c_value = false;
	return bolt11_decode(tmpctx, *enc, NULL, NULL, NULL, fail);
}

int main(int argc, char *argv[])
{
	/* A mutable copy of the bitcoin parameters, so the amount can go in the
	 * prefix for the case that does not fit a u64 msat. */
	struct chainparams big = *chainparams_for_network("bitcoin");
	struct amount_msat fits_msat;
	struct bolt11 *toobig, *fits;
	const char *toobig_str, *fits_str, *toobig_fail, *fits_fail;

	common_setup(argv[0]);

	/* BOLT #11:
	 *
	 * NB: all the following examples are signed with `priv_key`=`e126f68f7eafcc8b74f54d269fe206be715000f94dac067d1c04a8ca3b2db734`.
	 */
	if (!hex_decode("e126f68f7eafcc8b74f54d269fe206be715000f94dac067d1c04a8ca3b2db734",
			strlen("e126f68f7eafcc8b74f54d269fe206be715000f94dac067d1c04a8ca3b2db734"),
			&privkey, sizeof(privkey)))
		abort();

	/* Amount 184467441 BTC is 18446744100000000000 msat, which does not fit
	 * in a u64, so a reader MUST fail it.  BOLT #11: a reader, "if the
	 * `multiplier` is present: MUST multiply `amount` by the `multiplier`
	 * value to derive the amount required for payment." */
	big.lightning_hrp = "bc184467441";
	toobig = encode_then_decode(&big, NULL, &toobig_str, &toobig_fail);
	assert(strstarts(toobig_str, "lnbc1844674411"));
	if (toobig)
		warnx("%s decodes with %s", toobig_str,
		      fmt_amount_msat(tmpctx, *toobig->msat));
	else
		warnx("%s rejected: %s", toobig_str, toobig_fail);

	/* Amount 21000000 BTC is 2100000000000000000 msat, which fits in a u64,
	 * so a reader MUST decode it with that amount. */
	fits_msat = amount_msat(21000000ULL * MSAT_PER_BTC);
	fits = encode_then_decode(chainparams_for_network("bitcoin"), &fits_msat,
				  &fits_str, &fits_fail);
	assert(strstarts(fits_str, "lnbc210000001"));
	if (fits)
		warnx("%s decodes with %s", fits_str,
		      fmt_amount_msat(tmpctx, *fits->msat));
	else
		warnx("%s rejected: %s", fits_str, fits_fail);

	assert(!toobig);
	assert(fits && amount_msat_eq(*fits->msat, fits_msat));

	common_shutdown();
}

Build and run it with make unittest/common/test/run-bolt11-amount. The output on master 1ebc494 is:

run-bolt11-amount: lnbc1844674411pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpu9qrsgqn8teqkgvquuadwwv98jhqwstzxxrwqazmmqzx0pq80keejv4mv39shhlf06h3ggwk7kwr0527dyw257jzpw25rl8fk074k6204q8e0spyysvdc decodes with 26290448384msat
run-bolt11-amount: lnbc210000001pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpu9qrsgq9mzumzkvfvf548wuypxkkkw0eaezff0ckq24karyxpwaxv8ya4wznh7v0ual5fqtcrc6dw43jjeuw3aul8l2l00jslleql0zqvkemjcqwjrj3j rejected: Invalid sub-millisatoshi amount '21000000p'
run-bolt11-amount: common/test/run-bolt11-amount.c:129: main: Assertion `!toobig' failed.
make: *** [Makefile:942: unittest/common/test/run-bolt11-amount] Error 134

The product is amount * m10 at bolt11.c:823 and bolt11.c:865-870, and the sub-millisatoshi check runs on the wrapped value, which most wrapped values fail. When the product wraps around 2^64 a multiple of five times, first at 2^63 msat, the wrapped value passes and decode returns it. Then xpay.c:2847 takes it as the amount to pay. The error message also puts a literal p after the amount whatever the multiplier.

decode should fail the first invoice, which asks for 18,446,744,100,000,000,000 msat and does not fit in a u64. The second asks for 2,100,000,000,000,000,000 msat, which fits, and it should decode with that amount.

getinfo output

master 1ebc4943ed37: "version": "1ebc494", "network": "regtest"
v26.06.9, a2176589f235: "version": "a217658", "network": "regtest"

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions