diff --git a/examples/silentpayments.c b/examples/silentpayments.c index f1a5de34f7..7f062403ca 100644 --- a/examples/silentpayments.c +++ b/examples/silentpayments.c @@ -324,6 +324,7 @@ int main(void) { /*** Receiving ***/ { + unsigned char light_client_data33[33]; { /*** Scanning as a full node (Bob) *** * @@ -332,6 +333,16 @@ int main(void) { * 1. Collect the relevant prevouts from the transaction and call * `secp256k1_silentpayments_recipient_prevouts_summary_create` * 2. Call `secp256k1_silentpayments_recipient_scan_outputs` + * Bob collects the prevouts data from the transaction inputs and + * creates a `secp256k1_silentpayments_prevouts_summary` object. He uses + * this for his own scanning and also serializes the `prevouts_summary` + * object to send to light clients. We will use this later for + * Carol, who is scanning as a light client. Note, anyone can create + * and provide these `prevouts_summary` objects, i.e. you don't need to be + * a Silent Payments wallet, just someone interested in providing this + * data to light clients, e.g. a wallet service provider. In our + * example, Bob is scanning for himself but also sharing this data + * with light clients. */ ret = secp256k1_silentpayments_recipient_prevouts_summary_create(ctx, &prevouts_summary, @@ -346,6 +357,12 @@ int main(void) { printf("This transaction is not valid for Silent Payments, skipping.\n"); return EXIT_SUCCESS; } + /* Serialize the prevouts_summary data object for later use. */ + ret = secp256k1_silentpayments_recipient_prevouts_summary_serialize(ctx, + light_client_data33, 33, + &prevouts_summary + ); + assert(ret); /* Scan the transaction */ n_found_outputs = 0; @@ -414,8 +431,23 @@ int main(void) { } } { - /*** Scanning as a full node (Carol) ***/ - /* TODO: switch this part to light client scanning once it is supported */ + /*** Scanning as a light client (Carol) *** + * Being a light client, Carol likely does not have access to the + * transaction inputs and prevout information, so she uses the + * `prevouts_summary` object created by Bob's full node earlier. This + * serialized `prevouts_summary` object contains everything she needs for + * generating the shared secret, i.e., `input_hash * prevouts_pubkey_sum`. + * + * Additionally, she likely does not have access to the transaction outputs. + * This means she will need to first generate outputs, check if they exist + * in the UTXO set (e.g. BIP158 or some other means of querying), and + * proceed to download the full transaction if there is a match. + * + * Once she has the full transaction, she scans with + * `secp256k1_silentpayments_recipient_scan_outputs` to find all of + * the outputs and extract the tweaks needed for spending later. + */ + int found; /* Load Carol's spend public key. */ ret = secp256k1_ec_pubkey_parse(ctx, @@ -424,37 +456,88 @@ int main(void) { 33 ); assert(ret); - - n_found_outputs = 0; - ret = secp256k1_silentpayments_recipient_scan_outputs(ctx, - found_output_ptrs, &n_found_outputs, - (const secp256k1_xonly_pubkey**)tx_output_ptrs, N_OUTPUTS, - carol_scan_key, + /* Parse the serialized prevouts_summary object. */ + ret = secp256k1_silentpayments_recipient_prevouts_summary_parse(ctx, &prevouts_summary, - &unlabeled_spend_pubkey, - NULL, NULL /* NULL, NULL for no labels */ + light_client_data33, 33 ); if (!ret) { + printf("\n"); printf("This transaction is not valid for Silent Payments, skipping.\n"); return EXIT_SUCCESS; } - if (n_found_outputs > 0) { - /* Carol would spend these outputs the same as Bob, by tweaking her - * spend key with the tweak corresponding to the found output. See above - * for an example for Bob's outputs. */ - printf("\n"); - printf("Carol found the following outputs: \n"); - for (i = 0; i < n_found_outputs; i++) { - printf(" "); - ret = secp256k1_xonly_pubkey_serialize(ctx, - serialized_xonly, - &found_outputs[i].output - ); - assert(ret); - print_hex(serialized_xonly, sizeof(serialized_xonly)); + + { + secp256k1_xonly_pubkey *potential_outputs_ptrs[1]; + secp256k1_xonly_pubkey potential_outputs[1]; + const secp256k1_pubkey *spend_pubkeys_ptrs[1]; + + potential_outputs_ptrs[0] = &potential_outputs[0]; + spend_pubkeys_ptrs[0] = &unlabeled_spend_pubkey; + ret = secp256k1_silentpayments_recipient_create_output_pubkeys(ctx, + potential_outputs_ptrs, + carol_scan_key, + &prevouts_summary, + spend_pubkeys_ptrs, 1 + ); + if (!ret) { + printf("This transaction is not valid for Silent Payments, skipping.\n"); + return EXIT_SUCCESS; + } + /* At this point, we check that the UTXO exists with a light + * client protocol. For this example, we'll just iterate + * through the list of transaction outputs. + * + * If we generate an output and it does not exist in the + * UTXO set, we are done scanning this transaction. It is + * sufficient to stop after the first match, since we will be + * doing a full scan of the transaction once we have access to + * all of the outputs. + */ + found = 0; + for (i = 0; i < N_OUTPUTS; i++) { + if (secp256k1_xonly_pubkey_cmp(ctx, &potential_outputs[0], &tx_outputs[i]) == 0) { + found = 1; + break; + } + } + } + + if (found) { + /* Carol now needs to request the full transaction to do a complete scan. */ + n_found_outputs = 0; + ret = secp256k1_silentpayments_recipient_scan_outputs(ctx, + found_output_ptrs, &n_found_outputs, + (const secp256k1_xonly_pubkey**)tx_output_ptrs, N_OUTPUTS, + carol_scan_key, + &prevouts_summary, + &unlabeled_spend_pubkey, + NULL, NULL /* NULL, NULL for no labels */ + ); + if (!ret) { + printf("This transaction is not valid for Silent Payments, skipping.\n"); + return EXIT_SUCCESS; + } + if (n_found_outputs > 0) { + /* Carol would spend these outputs the same as Bob, by tweaking her + * spend key with the tweak corresponding to the found output. See above + * for an example for Bob's outputs. */ + printf("\n"); + printf("Carol found the following outputs: \n"); + for (i = 0; i < n_found_outputs; i++) { + printf(" "); + ret = secp256k1_xonly_pubkey_serialize(ctx, + serialized_xonly, + &found_outputs[i].output + ); + assert(ret); + print_hex(serialized_xonly, sizeof(serialized_xonly)); + } + } else { + printf("Carol did not find any outputs in this transaction.\n"); } } else { - printf("Carol did not find any outputs in this transaction.\n"); + printf("Carol did not find any outputs in this transaction (light client prefilter).\n"); } } } diff --git a/include/secp256k1_silentpayments.h b/include/secp256k1_silentpayments.h index 54581457ff..db1f028992 100644 --- a/include/secp256k1_silentpayments.h +++ b/include/secp256k1_silentpayments.h @@ -235,12 +235,58 @@ SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_silentpayments_recipien * guaranteed to be portable between different platforms or versions. It is * however guaranteed to be 101 bytes in size, and can be safely copied/moved. * This structure does not contain secret data. It can be created with - * `secp256k1_silentpayments_recipient_prevouts_summary_create`. + * `secp256k1_silentpayments_recipient_prevouts_summary_create`. Serialized and + * parsed with `secp256k1_silentpayments_recipient_prevouts_summary_serialize` + * and `secp256k1_silentpayments_recipient_prevouts_summary_parse`. */ typedef struct secp256k1_silentpayments_prevouts_summary { unsigned char data[101]; } secp256k1_silentpayments_prevouts_summary; +/** Parse a 33-byte or 65-byte sequence into a silentpayments_prevouts_summary object. + * + * Both sizes are accepted; see `secp256k1_silentpayments_recipient_prevouts_summary_serialize` + * for the size-vs-parse-speed tradeoff. + * + * Returns: 1 when the prevouts_summary could be parsed, 0 otherwise. + * + * Args: ctx: pointer to a context object. + * Out: prevouts_summary: pointer to a silentpayments_prevouts_summary object. + * In: input: pointer to a serialized silentpayments_prevouts_summary. + * inputlen: size of the serialized input. Must be either 33 or 65. + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_silentpayments_recipient_prevouts_summary_parse( + const secp256k1_context *ctx, + secp256k1_silentpayments_prevouts_summary *prevouts_summary, + const unsigned char *input, + size_t inputlen +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3); + +/** Serialize a silentpayments_prevouts_summary object into a 33-byte or 65-byte sequence. + * + * The 33-byte variant saves bandwidth and is preferred in general. The 65-byte variant + * is slightly faster to parse, at the cost of about double the size. + * + * Serializing a prevouts_summary object created with `_recipient_prevouts_summary_create` + * will result in an EC multiplication. This allows for a more compact serialization, but + * also means a serialized prevouts_summary will not parse back to the same + * prevouts_summary object (due to the EC multiplication). + * + * Returns: 1 always. + * + * Args: ctx: pointer to a context object + * Out: output: pointer to a byte array to store the serialized + * `silentpayments_prevouts_summary`. + * In: outputlen: size of the byte array. Must be either 33 or 65. + * prevouts_summary: pointer to an initialized `silentpayments_prevouts_summary` object + */ +SECP256K1_API int secp256k1_silentpayments_recipient_prevouts_summary_serialize( + const secp256k1_context *ctx, + unsigned char *output, + size_t outputlen, + const secp256k1_silentpayments_prevouts_summary *prevouts_summary +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(4); + /** Compute Silent Payments prevouts summary from prevout public keys and transaction * inputs. * @@ -389,6 +435,43 @@ SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_silentpayments_recipien const void *label_context ) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4) SECP256K1_ARG_NONNULL(6) SECP256K1_ARG_NONNULL(7) SECP256K1_ARG_NONNULL(8); +/** Create Silent Payments output public keys. + * + * Given a scan key, a prevouts_summary object, and an array of recipient + * spend public keys, create the Silent Payments output public keys. + * + * This function is used by the recipient when scanning for outputs without + * access to the transaction outputs (e.g., using BIP158 block filters). It will + * create the first output (i.e. with k=0) for each of the spend public keys provided. + * It is the caller's responsibility to determine if the created outputs exist. + * + * If a match is found, the caller must download the full transaction and call + * `secp256k1_silentpayments_recipient_scan_outputs` to check if there are additional + * outputs for the recipient and get the full output tweak needed to spend the outputs. + * + * Returns: 1 if output creation was successful, 0 otherwise. + * + * Args: ctx: pointer to a context object + * Out: outputs_xonly: pointer to an array of pointers to the resulting + * output x-only public keys. The outputs_xonly array + * MUST have the same size as the spend_pubkeys array. + * In: scan_key32: pointer to the recipient's 32 byte scan key. + * The scan key is valid if it passes secp256k1_ec_seckey_verify. + * prevouts_summary: pointer to the transaction prevouts summary data + * (see `secp256k1_silentpayments_recipient_prevouts_summary_create`). + * spend_pubkeys: pointer to an array of pointers to the recipient's spend public keys + * (labeled or unlabeled). + * n_spend_pubkeys: the size of the spend_pubkeys array. + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_silentpayments_recipient_create_output_pubkeys( + const secp256k1_context *ctx, + secp256k1_xonly_pubkey **outputs_xonly, + const unsigned char *scan_key32, + const secp256k1_silentpayments_prevouts_summary *prevouts_summary, + const secp256k1_pubkey * const *spend_pubkeys, + size_t n_spend_pubkeys +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4) SECP256K1_ARG_NONNULL(5); + #ifdef __cplusplus } #endif diff --git a/src/bench.c b/src/bench.c index ac2054a52e..ec15055f8d 100644 --- a/src/bench.c +++ b/src/bench.c @@ -73,7 +73,8 @@ static void help(const char *executable_path, int default_iters) { #endif #ifdef ENABLE_MODULE_SILENTPAYMENTS - printf(" silentpayments : all Silent payments benchmarks (scan_nomatch, scan_worstcase)\n"); + printf(" silentpayments : all Silent payments benchmarks (create_pubkeys, scan_nomatch, scan_worstcase)\n"); + printf(" silentpayments_create_pubkeys : Silent payments k=0 output pubkey creation (light client filter scenario)\n"); printf(" silentpayments_scan_nomatch : Silent payments scanning common case (no match)\n"); printf(" silentpayments_scan_worstcase : Silent payments scanning worst case (block-sized tx, all match)\n"); #endif @@ -197,7 +198,8 @@ int main(int argc, char** argv) { "ecdsa_recover", "schnorrsig", "schnorrsig_verify", "schnorrsig_sign", "ec", "keygen", "ec_keygen", "ellswift", "encode", "ellswift_encode", "decode", "ellswift_decode", "ellswift_keygen", "ellswift_ecdh", "silentpayments", - "silentpayments_scan_nomatch", "silentpayments_scan_worstcase"}; + "silentpayments_create_pubkeys", "silentpayments_scan_nomatch", + "silentpayments_scan_worstcase"}; int invalid_args = have_invalid_args(argc, argv, valid_args, ARRAY_SIZE(valid_args)); int default_iters = 20000; @@ -256,8 +258,8 @@ int main(int argc, char** argv) { #endif #ifndef ENABLE_MODULE_SILENTPAYMENTS - if (have_flag(argc, argv, "silentpayments") || have_flag(argc, argv, "silentpayments_scan_nomatch") || - have_flag(argc, argv, "silentpayments_scan_worstcase")) { + if (have_flag(argc, argv, "silentpayments") || have_flag(argc, argv, "silentpayments_create_pubkeys") || + have_flag(argc, argv, "silentpayments_scan_nomatch") || have_flag(argc, argv, "silentpayments_scan_worstcase")) { fprintf(stderr, "./bench: silentpayments module not enabled.\n"); fprintf(stderr, "See README.md for configuration instructions.\n\n"); return EXIT_FAILURE; diff --git a/src/ctime_tests.c b/src/ctime_tests.c index 0d201d20ad..e026c78436 100644 --- a/src/ctime_tests.c +++ b/src/ctime_tests.c @@ -123,6 +123,11 @@ static void run_tests(secp256k1_context *ctx, unsigned char *key) { const secp256k1_xonly_pubkey *sp_xonly_pubkeys[1]; secp256k1_pubkey sp_pubkey; const secp256k1_pubkey *sp_pubkeys[1]; + secp256k1_silentpayments_prevouts_summary parsed_prevouts_summary; + unsigned char prevouts_summary_ser33[33]; + secp256k1_xonly_pubkey lc_outputs[2]; + secp256k1_xonly_pubkey *lc_outputs_ptrs[2]; + const secp256k1_pubkey *lc_spend_pubkeys[2]; #endif for (i = 0; i < 32; i++) { @@ -346,6 +351,31 @@ static void run_tests(secp256k1_context *ctx, unsigned char *key) { */ CHECK(secp256k1_silentpayments_recipient_scan_outputs(ctx, found_outputs_ptrs, &n_found_outputs, tx_outputs, 1, key, &prevouts_summary, &recipient.spend_pubkey, NULL, NULL)); + /* Test the light client scanning API. + * + * The prevouts_summary (de)serialization functions only ever touch public data (the + * summed prevout public keys and the input hash), so there is no secret input to mark + * as such. They are still run here, since the parsed object is what a light client + * feeds into _recipient_create_output_pubkeys below. + */ + CHECK(secp256k1_silentpayments_recipient_prevouts_summary_serialize(ctx, prevouts_summary_ser33, sizeof(prevouts_summary_ser33), &prevouts_summary) == 1); + CHECK(secp256k1_silentpayments_recipient_prevouts_summary_parse(ctx, &parsed_prevouts_summary, prevouts_summary_ser33, sizeof(prevouts_summary_ser33)) == 1); + + lc_outputs_ptrs[0] = &lc_outputs[0]; + lc_outputs_ptrs[1] = &lc_outputs[1]; + lc_spend_pubkeys[0] = &recipient.spend_pubkey; + lc_spend_pubkeys[1] = &sp_pubkey; + /* `key` is still secret at this point and is used as the scan key. Both prevouts_summary + * variants are checked, since the scan key is treated differently in each: for an object + * created from transaction data (combined = 0) it is multiplied with the input hash, + * whereas for one parsed from a serialization (combined = 1) it is used as is. More than + * one spend public key is passed to also cover the loop over the spend public keys. + */ + ret = secp256k1_silentpayments_recipient_create_output_pubkeys(ctx, lc_outputs_ptrs, key, &prevouts_summary, lc_spend_pubkeys, 2); + CHECK(ret == 1); + ret = secp256k1_silentpayments_recipient_create_output_pubkeys(ctx, lc_outputs_ptrs, key, &parsed_prevouts_summary, lc_spend_pubkeys, 2); + CHECK(ret == 1); + #endif } diff --git a/src/modules/silentpayments/bench_impl.h b/src/modules/silentpayments/bench_impl.h index cf0d789a44..249ce11a54 100644 --- a/src/modules/silentpayments/bench_impl.h +++ b/src/modules/silentpayments/bench_impl.h @@ -24,6 +24,7 @@ typedef struct { secp256k1_context *ctx; secp256k1_pubkey spend_pubkey; unsigned char scan_key[32]; + const secp256k1_pubkey **pubkeys_ptrs; secp256k1_xonly_pubkey *tx_outputs; secp256k1_xonly_pubkey **tx_outputs_ptrs; secp256k1_xonly_pubkey tx_inputs[SP_BENCH_MAX_INPUTS]; @@ -33,6 +34,8 @@ typedef struct { unsigned char smallest_outpoint[36]; unsigned char label[33]; unsigned char label_tweak[32]; + unsigned char light_client_data[33]; + int num_pubkeys; int num_outputs; int num_matches; } bench_silentpayments_data; @@ -127,8 +130,10 @@ static void bench_silentpayments_scan_setup(void* arg) { } recipients[i].index = i; } - CHECK(secp256k1_silentpayments_sender_create_outputs(data->ctx, data->tx_outputs_ptrs, recipients_ptrs, - data->num_outputs, data->smallest_outpoint, keypairs_ptrs, SP_BENCH_MAX_INPUTS, NULL, 0)); + if (data->num_pubkeys == 0) { /* for the create_pubkeys benchmarks, we don't need any actual tx outputs (only the memory) */ + CHECK(secp256k1_silentpayments_sender_create_outputs(data->ctx, data->tx_outputs_ptrs, recipients_ptrs, + data->num_outputs, data->smallest_outpoint, keypairs_ptrs, SP_BENCH_MAX_INPUTS, NULL, 0)); + } for (i = 0; i < data->num_outputs; i++) { data->found_outputs_ptrs[i] = &data->found_outputs[i]; @@ -144,18 +149,47 @@ static void bench_silentpayments_scan_setup(void* arg) { free(recipients_ptrs); free(recipients); } + + /* Prepare serialized prevouts_summary for light client scanning. */ + { + secp256k1_silentpayments_prevouts_summary prevouts_summary; + data->pubkeys_ptrs = malloc(sizeof(secp256k1_pubkey*) * data->num_pubkeys); + for (i = 0; i < data->num_pubkeys; i++) { + data->pubkeys_ptrs[i] = &data->spend_pubkey; /* use same spend pubkey repeatedly to keep it simple for now */ + } + CHECK(secp256k1_silentpayments_recipient_prevouts_summary_create(data->ctx, &prevouts_summary, + data->smallest_outpoint, data->tx_inputs_ptrs, SP_BENCH_MAX_INPUTS, NULL, 0)); + CHECK(secp256k1_silentpayments_recipient_prevouts_summary_serialize(data->ctx, data->light_client_data, 33, &prevouts_summary)); + } } static void bench_silentpayments_scan_teardown(void* arg, int iters) { bench_silentpayments_data *data = (bench_silentpayments_data*)arg; (void)iters; + free(data->pubkeys_ptrs); free(data->tx_outputs); free(data->tx_outputs_ptrs); free(data->found_outputs); free(data->found_outputs_ptrs); } +static void bench_silentpayments_create_pubkeys(void* arg, int iters) { + bench_silentpayments_data *data = (bench_silentpayments_data*)arg; + secp256k1_silentpayments_prevouts_summary prevouts_summary; + int i; + + for (i = 0; i < iters; i++) { + CHECK(secp256k1_silentpayments_recipient_prevouts_summary_parse(data->ctx, &prevouts_summary, data->light_client_data, 33)); + CHECK(secp256k1_silentpayments_recipient_create_output_pubkeys(data->ctx, + data->tx_outputs_ptrs, + data->scan_key, + &prevouts_summary, + data->pubkeys_ptrs, data->num_pubkeys) + ); + } +} + static void bench_silentpayments_scan(void* arg, int iters) { bench_silentpayments_data *data = (bench_silentpayments_data*)arg; secp256k1_silentpayments_prevouts_summary prevouts_summary; @@ -185,12 +219,32 @@ static void run_silentpayments_bench(int iters, int argc, char** argv) { data.ctx = secp256k1_context_create(SECP256K1_CONTEXT_NONE); + if (d || have_flag(argc, argv, "silentpayments") || have_flag(argc, argv, "silentpayments_create_pubkeys")) { + const int num_pubkeys_bench[] = {1, 2, 5, 10, 100, 1000, 10000}; + size_t p; + for (p = 0; p < ARRAY_SIZE(num_pubkeys_bench); p++) { + const int num_pubkeys = num_pubkeys_bench[p]; + char str[64]; + data.num_pubkeys = num_pubkeys; + data.num_outputs = num_pubkeys; + data.num_matches = 0; + sprintf(str, "silentpayments_create_pubkeys_P=%i", num_pubkeys); + /* Don't run these slow benchmarks with low iterations (as used e.g. in CI) to prevent slow down */ + if (iters <= 2 && num_pubkeys > 10) { + printf("Skipping benchmark \"%s\" due to SECP256K1_BENCH_ITERS <= 2\n", str); + } else { + run_benchmark(str, bench_silentpayments_create_pubkeys, bench_silentpayments_scan_setup, bench_silentpayments_scan_teardown, &data, 10, num_pubkeys <= 10 ? iters : 1); + } + } + } + if (d || have_flag(argc, argv, "silentpayments") || have_flag(argc, argv, "silentpayments_scan_nomatch")) { const int num_outputs_bench[] = {2, 5, 10, 100, 1000, 2323, MAX_P2TR_OUTPUTS_PER_BLOCK}; size_t o; for (o = 0; o < ARRAY_SIZE(num_outputs_bench); o++) { const int num_outputs = num_outputs_bench[o]; char str[64]; + data.num_pubkeys = 0; data.num_outputs = num_outputs; data.num_matches = 0; sprintf(str, "silentpayments_scan_nomatch_N=%i", num_outputs); @@ -209,6 +263,7 @@ static void run_silentpayments_bench(int iters, int argc, char** argv) { for (k = 0; k < ARRAY_SIZE(num_matches_bench); k++) { const int num_matches = num_matches_bench[k]; char str[64]; + data.num_pubkeys = 0; data.num_outputs = MAX_P2TR_OUTPUTS_PER_BLOCK; data.num_matches = num_matches; sprintf(str, "silentpayments_scan_worstcase_K=%i", num_matches); diff --git a/src/modules/silentpayments/main_impl.h b/src/modules/silentpayments/main_impl.h index 9b595c17d2..08f985c32b 100644 --- a/src/modules/silentpayments/main_impl.h +++ b/src/modules/silentpayments/main_impl.h @@ -148,9 +148,9 @@ static int secp256k1_silentpayments_create_output_tweak(const secp256k1_context return (!secp256k1_scalar_is_zero(t_k_scalar)) & (!overflow); } -static int secp256k1_silentpayments_create_output_pubkey(const secp256k1_context *ctx, secp256k1_xonly_pubkey *output_xonly, const unsigned char *shared_secret33, const secp256k1_pubkey *spend_pubkey, uint32_t k) { - secp256k1_ge output_ge; +static int secp256k1_silentpayments_create_output_pubkeys(const secp256k1_context *ctx, secp256k1_xonly_pubkey **outputs_xonly, const unsigned char *shared_secret33, const secp256k1_pubkey * const *spend_pubkeys, size_t n_spend_pubkeys, uint32_t k) { secp256k1_scalar t_k_scalar; + size_t i; /* Calculate the output tweak t_k and convert it to a scalar. * * Note: _create_output_tweak can only fail if the output of the hash function is zero or greater than or equal to @@ -162,28 +162,41 @@ static int secp256k1_silentpayments_create_output_pubkey(const secp256k1_context return 0; } - if (!secp256k1_pubkey_load(ctx, &output_ge, spend_pubkey)) { - secp256k1_scalar_clear(&t_k_scalar); - return 0; - } - /* `tweak_add` only fails if t_k_scalar * G = -spend_pubkey. Considering t_k is the output of a hash function, this - * will happen only with negligible probability for honestly created spend_pubkey, but we handle this error anyway - * to protect against this function being called with malicious inputs, i.e., - * spend_pubkey = -(_create_output_tweak(shared_secret33, k))*G - */ - if (!secp256k1_eckey_pubkey_tweak_add(&output_ge, &t_k_scalar)) { - secp256k1_scalar_clear(&t_k_scalar); - return 0; + for (i = 0; i < n_spend_pubkeys; i++) { + secp256k1_ge output_ge; + if (!secp256k1_pubkey_load(ctx, &output_ge, spend_pubkeys[i])) { + secp256k1_scalar_clear(&t_k_scalar); + return 0; + } + /* `tweak_add` only fails if t_k_scalar * G = -spend_pubkey. Considering t_k is the output of a hash function, this + * will happen only with negligible probability for honestly created spend_pubkey, but we handle this error anyway + * to protect against this function being called with malicious inputs, i.e., + * spend_pubkey = -(_create_output_tweak(shared_secret33, k))*G + */ + if (!secp256k1_eckey_pubkey_tweak_add(&output_ge, &t_k_scalar)) { + secp256k1_scalar_clear(&t_k_scalar); + return 0; + } + secp256k1_fe_normalize_var(&output_ge.y); + secp256k1_extrakeys_ge_even_y(&output_ge); + secp256k1_xonly_pubkey_save(outputs_xonly[i], &output_ge); } - secp256k1_fe_normalize_var(&output_ge.y); - secp256k1_extrakeys_ge_even_y(&output_ge); - secp256k1_xonly_pubkey_save(output_xonly, &output_ge); /* Leaking this value would break indistinguishability of the transaction, so clear it. */ secp256k1_scalar_clear(&t_k_scalar); return 1; } +SECP256K1_INLINE static int secp256k1_silentpayments_create_output_pubkey(const secp256k1_context *ctx, secp256k1_xonly_pubkey *output_xonly, const unsigned char *shared_secret33, const secp256k1_pubkey *spend_pubkey, uint32_t k) { + secp256k1_xonly_pubkey *outputs_xonly[1]; + const secp256k1_pubkey *spend_pubkeys[1]; + + outputs_xonly[0] = output_xonly; + spend_pubkeys[0] = spend_pubkey; + + return secp256k1_silentpayments_create_output_pubkeys(ctx, outputs_xonly, shared_secret33, spend_pubkeys, 1, k); +} + int secp256k1_silentpayments_sender_create_outputs( const secp256k1_context *ctx, secp256k1_xonly_pubkey **generated_outputs, @@ -475,16 +488,90 @@ int secp256k1_silentpayments_recipient_create_labeled_spend_pubkey(const secp256 * curve multiplication can be avoided when creating the shared secret, i.e., * (recipient_scan_key * input_hash) * prevouts_pubkey_sum. * - * But when storing the prevouts_summary object (not supported yet), either to send to - * light clients or for wallet rescans, we can save 32-bytes by combining the input_hash - * and prevouts_pubkey_sum and saving the resulting point serialized as a compressed - * public key, i.e., input_hash * prevouts_pubkey_sum. + * But when serializing the prevouts_summary object, either to send to light clients or for + * wallet rescans, we can save 32-bytes by combining the input_hash and prevouts_pubkey_sum + * and saving the resulting point serialized as a compressed public key, i.e., + * input_hash * prevouts_pubkey_sum. * * For each function: * + * - `_recipient_prevouts_summary_parse` assumes the input represents a previously serialized + * prevouts_summary object and always deserializes into a prevouts_summary object with combined = true + * (and the input_hash portion zeroed out). + * - `_recipient_prevouts_summary_serialize` multiplies the input_hash into the summed public key before + * serializing, if combined = false. If combined = true, the point is directly serialized. * - `_recipient_prevouts_summary_create` always creates a prevouts_summary object with combined = false */ +int secp256k1_silentpayments_recipient_prevouts_summary_parse(const secp256k1_context *ctx, secp256k1_silentpayments_prevouts_summary *prevouts_summary, const unsigned char *input, size_t inputlen) { + secp256k1_ge pk; + + VERIFY_CHECK(ctx != NULL); + ARG_CHECK(prevouts_summary != NULL); + memset(prevouts_summary, 0, sizeof(*prevouts_summary)); + ARG_CHECK(input != NULL); + ARG_CHECK(inputlen == 33 || inputlen == 65); + /* Since an attacker can send us malicious data that looks like a serialized public key but is not, fail early. */ + /* TODO: remove manual hybrid pubkey rejection and replace with `secp256k1_ge_parse33` and + * `secp256k1_ge_parse65` calls (depending on inputlen) once PR #1918 gets merged */ + if (inputlen == 65 && input[0] != SECP256K1_TAG_PUBKEY_UNCOMPRESSED) { /* enforce uncompressed format (0x04 prefix) */ + return 0; + } + if (!secp256k1_ge_parse(&pk, input, inputlen)) { + return 0; + } + /* A serialized prevouts_summary will always have the input_hash multiplied in, so we set + * combined = true. The 32 bytes used to represent the input_hash stay zeroed out by the + * memset above. + */ + memcpy(&prevouts_summary->data[0], secp256k1_silentpayments_prevouts_summary_magic, 4); + prevouts_summary->data[4] = 1; + secp256k1_ge_to_bytes(&prevouts_summary->data[5], &pk); + return 1; +} + +int secp256k1_silentpayments_recipient_prevouts_summary_serialize(const secp256k1_context *ctx, unsigned char *output, size_t outputlen, const secp256k1_silentpayments_prevouts_summary *prevouts_summary) { + secp256k1_ge ge; + int combined; + + VERIFY_CHECK(ctx != NULL); + ARG_CHECK(output != NULL); + ARG_CHECK(outputlen == 33 || outputlen == 65); + memset(output, 0, outputlen); + ARG_CHECK(prevouts_summary != NULL); + ARG_CHECK(secp256k1_memcmp_var(&prevouts_summary->data[0], secp256k1_silentpayments_prevouts_summary_magic, 4) == 0); + /* `_ge_from_bytes` has no failure return, but in VERIFY builds it asserts that the stored + * coordinates are canonical field elements. That holds for every prevouts_summary object + * produced by `_recipient_prevouts_summary_create` or `_recipient_prevouts_summary_parse`, so + * it can only fire if the caller tampered with the object manually. */ + secp256k1_ge_from_bytes(&ge, &prevouts_summary->data[5]); + combined = (int)prevouts_summary->data[4]; + if (!combined) { + int ret; + secp256k1_scalar input_hash_scalar; + /* `_tweak_mul` can only fail if input_hash_scalar is zero, but assuming the prevouts_summary + * object was created correctly, this is impossible because input_hash_scalar is the output of + * a hash function. + * + * Note: we don't verify that the input hash is less than the curve order since this is + * verified when the prevouts_summary object is created. + */ + secp256k1_scalar_set_b32(&input_hash_scalar, &prevouts_summary->data[5 + 64], NULL); + ret = secp256k1_eckey_pubkey_tweak_mul(&ge, &input_hash_scalar); +#ifdef VERIFY + VERIFY_CHECK(ret == 1); +#else + (void)ret; +#endif + } + if (outputlen == 33) { + secp256k1_ge_serialize33(&ge, output); + } else { + secp256k1_ge_serialize65(&ge, output); + } + return 1; +} + int secp256k1_silentpayments_recipient_prevouts_summary_create( const secp256k1_context *ctx, secp256k1_silentpayments_prevouts_summary *prevouts_summary, @@ -651,8 +738,9 @@ int secp256k1_silentpayments_recipient_scan_outputs( } secp256k1_ge_from_bytes(&prevouts_pubkey_sum_ge, &prevouts_summary->data[5]); combined = (int)prevouts_summary->data[4]; - /* Note that the "combined" flag can currently only be 0, as we only have support for full nodes, i.e., - * the following branch is always taken. "combined" can also be 1 once we add light client support. */ + /* The following branch is only taken for prevouts_summary objects that were created from + * transaction input data. If a prevouts_summary was parsed from a serialization (relevant + * for light clients), the EC multiplication has already been done and "combined" is thus 1. */ if (!combined) { secp256k1_scalar input_hash_scalar; secp256k1_scalar_set_b32(&input_hash_scalar, &prevouts_summary->data[5 + 64], NULL); @@ -802,4 +890,54 @@ int secp256k1_silentpayments_recipient_scan_outputs( return 1; } +int secp256k1_silentpayments_recipient_create_output_pubkeys( + const secp256k1_context *ctx, + secp256k1_xonly_pubkey **outputs_xonly, + const unsigned char *scan_key32, + const secp256k1_silentpayments_prevouts_summary *prevouts_summary, + const secp256k1_pubkey * const *spend_pubkeys, size_t n_spend_pubkeys +) { + secp256k1_scalar scan_key_scalar; + secp256k1_ge prevouts_pubkey_sum_ge, spend_pubkey_ge; + size_t i; + int ret, combined, valid_scan_key; + unsigned char shared_secret[33]; + + /* Sanity check inputs */ + VERIFY_CHECK(ctx != NULL); + ARG_CHECK(outputs_xonly != NULL); + ARG_CHECK(scan_key32 != NULL); + ARG_CHECK(prevouts_summary != NULL); + ARG_CHECK(secp256k1_memcmp_var(&prevouts_summary->data[0], secp256k1_silentpayments_prevouts_summary_magic, 4) == 0); + ARG_CHECK(spend_pubkeys != NULL); + ARG_CHECK(n_spend_pubkeys > 0); + for (i = 0; i < n_spend_pubkeys; i++) { + ARG_CHECK(outputs_xonly[i] != NULL); + ARG_CHECK(spend_pubkeys[i] != NULL); + /* Validate each spend pubkey object early so malformed pubkeys are always rejected. */ + if (!secp256k1_pubkey_load(ctx, &spend_pubkey_ge, spend_pubkeys[i])) { + return 0; + } + } + valid_scan_key = secp256k1_scalar_set_b32_seckey(&scan_key_scalar, scan_key32); + secp256k1_declassify(ctx, &valid_scan_key, sizeof(valid_scan_key)); + if (!valid_scan_key) { + secp256k1_scalar_clear(&scan_key_scalar); + return 0; + } + + secp256k1_ge_from_bytes(&prevouts_pubkey_sum_ge, &prevouts_summary->data[5]); + combined = (int)prevouts_summary->data[4]; + if (!combined) { + secp256k1_scalar input_hash_scalar; + secp256k1_scalar_set_b32(&input_hash_scalar, &prevouts_summary->data[5 + 64], NULL); + secp256k1_scalar_mul(&scan_key_scalar, &scan_key_scalar, &input_hash_scalar); + } + secp256k1_silentpayments_create_shared_secret(shared_secret, &prevouts_pubkey_sum_ge, &scan_key_scalar); + secp256k1_scalar_clear(&scan_key_scalar); + ret = secp256k1_silentpayments_create_output_pubkeys(ctx, outputs_xonly, shared_secret, spend_pubkeys, n_spend_pubkeys, 0); + secp256k1_memclear_explicit(shared_secret, sizeof(shared_secret)); + return ret; +} + #endif diff --git a/src/modules/silentpayments/tests_impl.h b/src/modules/silentpayments/tests_impl.h index 8a60a95bb1..9eac8e62fe 100644 --- a/src/modules/silentpayments/tests_impl.h +++ b/src/modules/silentpayments/tests_impl.h @@ -517,6 +517,68 @@ static void test_recipient_api(void) { CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_prevouts_summary_create(CTX, &ps, SMALLEST_OUTPOINT, NULL, 0, NULL, 0)); CHECK(secp256k1_silentpayments_recipient_prevouts_summary_create(CTX, &ps, SMALLEST_OUTPOINT, tp, 1, pp, 1)); + /* Test prevouts_summary _serialize and _parse. */ + { + secp256k1_silentpayments_prevouts_summary malformed_ps; /* not created by us, must be rejected */ + secp256k1_silentpayments_prevouts_summary ps_parsed; /* _parse target, so that ps stays valid */ + unsigned char o33[33]; /* serialized prevouts_summary, compressed */ + unsigned char o65[65]; /* serialized prevouts_summary, uncompressed */ + unsigned char malformed33[33] = { 0x01 }; /* invalid header byte */ + unsigned char malformed65[65] = { 0x04 }; /* valid header byte, but (0, 0) is not on the curve */ + + /* Check that a prevouts_summary that was not created by us (missing magic bytes) is rejected. */ + memset(&malformed_ps, 0, sizeof(malformed_ps)); + malformed_ps.data[4] = 1; /* combined = true, but magic is still zero */ + CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_prevouts_summary_serialize(CTX, o33, 33, &malformed_ps)); + CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_prevouts_summary_serialize(CTX, o65, 65, &malformed_ps)); + + /* Check that NULL and invalid outputlen arguments are handled by _serialize. */ + CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_prevouts_summary_serialize(CTX, NULL, 33, &ps)); + CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_prevouts_summary_serialize(CTX, o33, 33, NULL)); + CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_prevouts_summary_serialize(CTX, o33, 0, &ps)); + CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_prevouts_summary_serialize(CTX, o33, 32, &ps)); + CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_prevouts_summary_serialize(CTX, o33, 64, &ps)); + CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_prevouts_summary_serialize(CTX, o33, 66, &ps)); + + /* Check that NULL and invalid inputlen arguments are handled by _parse. */ + CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_prevouts_summary_parse(CTX, NULL, o33, 33)); + CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_prevouts_summary_parse(CTX, &ps_parsed, NULL, 33)); + CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_prevouts_summary_parse(CTX, &ps_parsed, o33, 0)); + CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_prevouts_summary_parse(CTX, &ps_parsed, o33, 32)); + CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_prevouts_summary_parse(CTX, &ps_parsed, o33, 64)); + CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_prevouts_summary_parse(CTX, &ps_parsed, o33, 66)); + + /* Check that malformed serializations (valid length but not a valid pubkey) are rejected, + * for both the compressed and the uncompressed length. */ + CHECK(secp256k1_silentpayments_recipient_prevouts_summary_parse(CTX, &ps_parsed, malformed33, 33) == 0); + CHECK(secp256k1_silentpayments_recipient_prevouts_summary_parse(CTX, &ps_parsed, malformed65, 65) == 0); + + /* Round-trip: parse a valid 33-byte pubkey, then serialize compressed and uncompressed, then parse both back. + * Since a serialized prevouts_summary is just (input_hash * prevouts_pubkey_sum) represented as a pubkey, + * BOB_ADDRESS[0] (a valid compressed pubkey) is itself a valid serialized prevouts_summary. */ + CHECK(secp256k1_silentpayments_recipient_prevouts_summary_parse(CTX, &ps_parsed, BOB_ADDRESS[0], 33)); + CHECK(secp256k1_silentpayments_recipient_prevouts_summary_serialize(CTX, o33, 33, &ps_parsed)); + CHECK(secp256k1_silentpayments_recipient_prevouts_summary_serialize(CTX, o65, 65, &ps_parsed)); + CHECK(secp256k1_silentpayments_recipient_prevouts_summary_parse(CTX, &ps_parsed, o33, 33)); + CHECK(secp256k1_silentpayments_recipient_prevouts_summary_parse(CTX, &ps_parsed, o65, 65)); + + /* Round-trip the actually created prevouts_summary (combined=0): serialize, parse, then re-serialize. + * After _parse the object has combined=1, so re-serializing must yield the same bytes as the first + * serialization (the input hash is already absorbed into the pubkey). */ + CHECK(secp256k1_silentpayments_recipient_prevouts_summary_serialize(CTX, o33, 33, &ps)); + CHECK(secp256k1_silentpayments_recipient_prevouts_summary_serialize(CTX, o65, 65, &ps)); + { + unsigned char o33_2[33]; + unsigned char o65_2[65]; + CHECK(secp256k1_silentpayments_recipient_prevouts_summary_parse(CTX, &ps_parsed, o33, 33)); + CHECK(secp256k1_silentpayments_recipient_prevouts_summary_serialize(CTX, o33_2, 33, &ps_parsed)); + CHECK(secp256k1_silentpayments_recipient_prevouts_summary_parse(CTX, &ps_parsed, o65, 65)); + CHECK(secp256k1_silentpayments_recipient_prevouts_summary_serialize(CTX, o65_2, 65, &ps_parsed)); + CHECK(secp256k1_memcmp_var(o33, o33_2, 33) == 0); + CHECK(secp256k1_memcmp_var(o65, o65_2, 65) == 0); + } + } + /* check the _recipient_scan_outputs cornercase where internal tweaking would fail; this is the case if the recipient spend public key is P = -(create_output_tweak(shared_secret, k))*G */ { @@ -599,6 +661,54 @@ static void test_recipient_api(void) { CHECK(secp256k1_silentpayments_recipient_scan_outputs(CTX, fp, &n_f, tp, 1, MALFORMED_SECKEY, &ps, &p, NULL, NULL) == 0); memset(&ps, 0, sizeof(ps)); CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_scan_outputs(CTX, fp, &n_f, tp, 1, ALICE_SECKEY, &ps, &p, NULL, NULL)); + /* Reset ps to a valid prevouts_summary object */ + CHECK(secp256k1_silentpayments_recipient_prevouts_summary_parse(CTX, &ps, BOB_ADDRESS[0], 33)); + + /* Test recipient light client API */ + { + secp256k1_xonly_pubkey outputs[2]; /* array of generated xonly pks */ + secp256k1_xonly_pubkey *output_ptrs[2]; /* array of pointers to generated xonly pks */ + secp256k1_pubkey spend_pubkeys[2]; /* array of spend public keys */ + secp256k1_pubkey const *spend_pubkey_ptrs[2]; /* array of pointers to spend public keys */ + size_t i; + + CHECK(secp256k1_ec_pubkey_parse(CTX, &spend_pubkeys[0], BOB_ADDRESS[0], 33)); + CHECK(secp256k1_ec_pubkey_parse(CTX, &spend_pubkeys[1], BOB_ADDRESS[0], 33)); + spend_pubkey_ptrs[0] = &spend_pubkeys[0]; + spend_pubkey_ptrs[1] = &spend_pubkeys[1]; + output_ptrs[0] = &outputs[0]; + output_ptrs[1] = &outputs[1]; + + CHECK(secp256k1_silentpayments_recipient_create_output_pubkeys(CTX, output_ptrs, ALICE_SECKEY, &ps, spend_pubkey_ptrs, 2)); + + /* Check that NULL in "array of pointers" arguments is not allowed */ + for (i = 0; i < 2; i++) { + secp256k1_xonly_pubkey *original_ptr_output = output_ptrs[i]; + const secp256k1_pubkey *original_ptr_pubkey = spend_pubkey_ptrs[i]; + + output_ptrs[i] = NULL; + CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_create_output_pubkeys(CTX, output_ptrs, ALICE_SECKEY, &ps, spend_pubkey_ptrs, 2)); + output_ptrs[i] = original_ptr_output; + + spend_pubkey_ptrs[i] = NULL; + CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_create_output_pubkeys(CTX, output_ptrs, ALICE_SECKEY, &ps, spend_pubkey_ptrs, 2)); + spend_pubkey_ptrs[i] = original_ptr_pubkey; + } + + CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_create_output_pubkeys(CTX, NULL, ALICE_SECKEY, &ps, spend_pubkey_ptrs, 2)); + CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_create_output_pubkeys(CTX, output_ptrs, NULL, &ps, spend_pubkey_ptrs, 2)); + CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_create_output_pubkeys(CTX, output_ptrs, ALICE_SECKEY, NULL, spend_pubkey_ptrs, 2)); + CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_create_output_pubkeys(CTX, output_ptrs, ALICE_SECKEY, &ps, NULL, 2)); + CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_create_output_pubkeys(CTX, output_ptrs, ALICE_SECKEY, &ps, spend_pubkey_ptrs, 0)); + memset(&ps, 0, sizeof(ps)); + CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_create_output_pubkeys(CTX, output_ptrs, ALICE_SECKEY, &ps, spend_pubkey_ptrs, 2)); + /* Reset ps to a valid prevouts_summary object */ + CHECK(secp256k1_silentpayments_recipient_prevouts_summary_parse(CTX, &ps, BOB_ADDRESS[0], 33)); + CHECK(secp256k1_silentpayments_recipient_create_output_pubkeys(CTX, output_ptrs, MALFORMED_SECKEY, &ps, spend_pubkey_ptrs, 2) == 0); + /* Create uncombined prevouts_summary */ + CHECK(secp256k1_silentpayments_recipient_prevouts_summary_create(CTX, &ps, SMALLEST_OUTPOINT, tp, 1, pp, 1)); + CHECK(secp256k1_silentpayments_recipient_create_output_pubkeys(CTX, output_ptrs, ALICE_SECKEY, &ps, spend_pubkey_ptrs, 2)); + } } static void test_recipient_scan_label_precedes_direct_match(void) { @@ -757,7 +867,7 @@ void run_silentpayments_test_vector_receive(const struct bip352_test_vector *tes secp256k1_pubkey recipient_scan_pubkey; secp256k1_pubkey recipient_spend_pubkey; secp256k1_silentpayments_label label; - size_t i,j; + size_t i,j,v; int ret; uint32_t n_found = 0; unsigned char found_output[32]; @@ -812,76 +922,123 @@ void run_silentpayments_test_vector_receive(const struct bip352_test_vector *tes CHECK(secp256k1_silentpayments_recipient_label_serialize(CTX, cache_entry->label, &label)); labels_cache.entries_used++; } - CHECK(secp256k1_silentpayments_recipient_scan_outputs(CTX, - found_outputs, &n_found, - tx_outputs, subtest->num_to_scan_outputs, - subtest->scan_seckey, - &prevouts_summary, - &recipient_spend_pubkey, - label_lookup, &labels_cache) - ); - if (subtest->full_check) { - /* compare expected and scanned outputs (including calculated seckey tweaks and signatures) */ -#ifdef ENABLE_MODULE_SCHNORRSIG - static unsigned char found_signatures[MAX_OUTPUTS_PER_TEST_CASE][64]; - /* sha256("message") */ - static unsigned char MSG32[32] = { - 0xab,0x53,0x0a,0x13,0xe4,0x59,0x14,0x98, - 0x2b,0x79,0xf9,0xb7,0xe3,0xfb,0xa9,0x94, - 0xcf,0xd1,0xf3,0xfb,0x22,0xf7,0x1c,0xea, - 0x1a,0xfb,0xf0,0x2b,0x46,0x0c,0x6d,0x1d - }; - /* sha256("random auxiliary data") */ - static unsigned char AUX32[32] = { - 0x0b,0x3f,0xdd,0xfd,0x67,0xbf,0x76,0xae, - 0x76,0x39,0xee,0x73,0x5b,0x70,0xff,0x15, - 0x83,0xfd,0x92,0x48,0xc0,0x57,0xd2,0x86, - 0x07,0xa2,0x15,0xf4,0x0b,0x0a,0x3e,0xcc - }; - for (i = 0; i < n_found; i++) { - unsigned char full_seckey[32]; - secp256k1_keypair keypair; - unsigned char signature[64]; - memcpy(&full_seckey, subtest->spend_seckey, 32); - CHECK(secp256k1_ec_seckey_tweak_add(CTX, full_seckey, found_outputs[i]->tweak)); - CHECK(secp256k1_keypair_create(CTX, &keypair, full_seckey)); - CHECK(secp256k1_schnorrsig_sign32(CTX, signature, MSG32, &keypair, AUX32)); - memcpy(found_signatures[i], signature, 64); + /* Scan the outputs three times, using an equivalent prevouts_summary object each time: the one + * created from the transaction input data (combined = 0, the full node case), and the results of + * serializing and parsing that object back in both the compressed and the uncompressed format + * (combined = 1, the light client case). All variants must lead to identical scan results. */ + for (v = 0; v < 3; v++) { + secp256k1_silentpayments_prevouts_summary scan_prevouts_summary = prevouts_summary; + if (v > 0) { + size_t serlen = (v == 1) ? 33 : 65; + unsigned char prevouts_summary_ser[65]; + CHECK(secp256k1_silentpayments_recipient_prevouts_summary_serialize(CTX, prevouts_summary_ser, serlen, &prevouts_summary)); + CHECK(secp256k1_silentpayments_recipient_prevouts_summary_parse(CTX, &scan_prevouts_summary, prevouts_summary_ser, serlen)); } + CHECK(secp256k1_silentpayments_recipient_scan_outputs(CTX, + found_outputs, &n_found, + tx_outputs, subtest->num_to_scan_outputs, + subtest->scan_seckey, + &scan_prevouts_summary, + &recipient_spend_pubkey, + label_lookup, &labels_cache) + ); + if (subtest->full_check) { + /* compare expected and scanned outputs (including calculated seckey tweaks and signatures) */ +#ifdef ENABLE_MODULE_SCHNORRSIG + static unsigned char found_signatures[MAX_OUTPUTS_PER_TEST_CASE][64]; + /* sha256("message") */ + static unsigned char MSG32[32] = { + 0xab,0x53,0x0a,0x13,0xe4,0x59,0x14,0x98, + 0x2b,0x79,0xf9,0xb7,0xe3,0xfb,0xa9,0x94, + 0xcf,0xd1,0xf3,0xfb,0x22,0xf7,0x1c,0xea, + 0x1a,0xfb,0xf0,0x2b,0x46,0x0c,0x6d,0x1d + }; + /* sha256("random auxiliary data") */ + static unsigned char AUX32[32] = { + 0x0b,0x3f,0xdd,0xfd,0x67,0xbf,0x76,0xae, + 0x76,0x39,0xee,0x73,0x5b,0x70,0xff,0x15, + 0x83,0xfd,0x92,0x48,0xc0,0x57,0xd2,0x86, + 0x07,0xa2,0x15,0xf4,0x0b,0x0a,0x3e,0xcc + }; + for (i = 0; i < n_found; i++) { + unsigned char full_seckey[32]; + secp256k1_keypair keypair; + unsigned char signature[64]; + memcpy(&full_seckey, subtest->spend_seckey, 32); + CHECK(secp256k1_ec_seckey_tweak_add(CTX, full_seckey, found_outputs[i]->tweak)); + CHECK(secp256k1_keypair_create(CTX, &keypair, full_seckey)); + CHECK(secp256k1_schnorrsig_sign32(CTX, signature, MSG32, &keypair, AUX32)); + memcpy(found_signatures[i], signature, 64); + } #endif - for (i = 0; i < n_found; i++) { - int match = 0; - CHECK(secp256k1_xonly_pubkey_serialize(CTX, found_output, &found_outputs[i]->output)); - for (j = 0; j < subtest->num_found_output_pubkeys; j++) { - if (secp256k1_memcmp_var(&found_output, subtest->found_output_pubkeys[j], 32) == 0) { - CHECK(secp256k1_memcmp_var(found_outputs[i]->tweak, subtest->found_seckey_tweaks[j], 32) == 0); + for (i = 0; i < n_found; i++) { + int match = 0; + CHECK(secp256k1_xonly_pubkey_serialize(CTX, found_output, &found_outputs[i]->output)); + for (j = 0; j < subtest->num_found_output_pubkeys; j++) { + if (secp256k1_memcmp_var(&found_output, subtest->found_output_pubkeys[j], 32) == 0) { + CHECK(secp256k1_memcmp_var(found_outputs[i]->tweak, subtest->found_seckey_tweaks[j], 32) == 0); #ifdef ENABLE_MODULE_SCHNORRSIG - CHECK(secp256k1_memcmp_var(found_signatures[i], subtest->found_signatures[j], 64) == 0); + CHECK(secp256k1_memcmp_var(found_signatures[i], subtest->found_signatures[j], 64) == 0); #endif - match = 1; - break; + match = 1; + break; + } + } + CHECK(match); + + if (subtest->num_labels == 0) { + /* if the test case doesn't involve labels, we must not have any labeled matches */ + CHECK(!found_outputs[i]->found_with_label); + } else if (found_outputs[i]->found_with_label) { + /* if the test case involves labels and we have a labeled match, verify that the returned + * label is in the list of expected ones by manually checking against the label cache + * (note that the test vectors only contain a list of used labels, but not exactly which one + * of these have been applied for each individual output, so that's the best we can do) */ + unsigned char found_label_ser[33]; + const unsigned char *found_label_tweak; + CHECK(secp256k1_silentpayments_recipient_label_serialize(CTX, found_label_ser, &found_outputs[i]->label)); + found_label_tweak = label_lookup(found_label_ser, &labels_cache); + CHECK(found_label_tweak != NULL); + } + } + } + CHECK(n_found == subtest->num_found_output_pubkeys); + /* Check that the output public key creation function (intended for light clients) matches + * the first found output (k=0) of the full scanning function. If the full scan didn't find + * anything, the created k=0 output must not be among the transaction outputs either. */ + { + secp256k1_pubkey pubkey = recipient_spend_pubkey; + const secp256k1_pubkey *pubkey_ptrs[1]; + secp256k1_xonly_pubkey output; + secp256k1_xonly_pubkey *output_ptrs[1]; + size_t n_found_k0; + + pubkey_ptrs[0] = &pubkey; + output_ptrs[0] = &output; + if (n_found > 0 && found_outputs[0]->found_with_label) { + CHECK(secp256k1_silentpayments_recipient_create_labeled_spend_pubkey(CTX, + &pubkey, &recipient_spend_pubkey, &found_outputs[0]->label)); + } + CHECK(secp256k1_silentpayments_recipient_create_output_pubkeys(CTX, + output_ptrs, + subtest->scan_seckey, + &scan_prevouts_summary, + pubkey_ptrs, ARRAY_SIZE(pubkey_ptrs))); + n_found_k0 = 0; + for (i = 0; i < subtest->num_to_scan_outputs; i++) { + if (secp256k1_xonly_pubkey_cmp(CTX, &output, tx_outputs[i]) == 0) { + n_found_k0++; } } - CHECK(match); - - if (subtest->num_labels == 0) { - /* if the test case doesn't involve labels, we must not have any labeled matches */ - CHECK(!found_outputs[i]->found_with_label); - } else if (found_outputs[i]->found_with_label) { - /* if the test case involves labels and we have a labeled match, verify that the returned - * label is in the list of expected ones by manually checking against the label cache - * (note that the test vectors only contain a list of used labels, but not exactly which one - * of these have been applied for each individual output, so that's the best we can do) */ - unsigned char found_label_ser[33]; - const unsigned char *found_label_tweak; - CHECK(secp256k1_silentpayments_recipient_label_serialize(CTX, found_label_ser, &found_outputs[i]->label)); - found_label_tweak = label_lookup(found_label_ser, &labels_cache); - CHECK(found_label_tweak != NULL); + if (n_found > 0) { + CHECK(secp256k1_xonly_pubkey_cmp(CTX, &output, &found_outputs[0]->output) == 0); + CHECK(n_found_k0 == 1); + } else { + CHECK(n_found_k0 == 0); } } } - CHECK(n_found == subtest->num_found_output_pubkeys); } static void silentpayments_sha256_tag_test(void) {