What changed, and why it matters
This commit adds new code for receiving BIP352 Silent Payments in the libsecp256k1 cryptography library. It introduces functions that let a wallet scan a Bitcoin transaction to find outputs sent to it and compute the secret tweaks needed to spend them later. The change is a feature addition with extensive tests; nothing in the commit message or diff indicates a security bug fix or vulnerability.
No security action required. Treat as normal feature review; consider following up on the TODOs regarding scanning performance and quadratic iteration limits in future releases.
Security signals we found
New feature code, not a security patch
Extensive input validation and NULL checks added
Sensitive scalars cleared after use (shared_secret, scan_key_scalar, t_k_scalar)
Magic-byte integrity check on opaque prevouts_summary object
Early rejection of prevout public key sums to infinity (potential DoS/malicious transaction)
TODO comments about future quadratic scaling and batch inversion improvements
Evidence from the diff
The patch adds the receiving side of the Silent Payments module: secp256k1_silentpayments_recipient_prevouts_summary_create and secp256k1_silentpayments_recipient_scan_outputs, plus a prevouts_summary opaque type and a label-lookup callback type. It implements BIP352-style scanning: summing prevout public keys, deriving an input hash, computing a shared secret via ECDH, deriving per-output tweaks, and matching against transaction outputs (including labeled variants). The implementation includes ARG_CHECK/VERIFY_CHECK validation, magic-byte initialization checks for the opaque summary object, clearing of sensitive scalars/secrets, and a test suite covering malformed keys, NULL arguments, sum-to-zero prevouts, and edge cases where tweaking fails. Two TODO comments note future performance/quadratic-scaling improvements.
Changed components
include/secp256k1_silentpayments.hsrc/modules/silentpayments/main_impl.hsrc/modules/silentpayments/tests_impl.hInspect captured patch +632 / −1
diff --git a/include/secp256k1_silentpayments.h b/include/secp256k1_silentpayments.h
index c9a0d72..b2f1ce0 100644
--- a/include/secp256k1_silentpayments.h
+++ b/include/secp256k1_silentpayments.h
@@ -1,6 +1,7 @@
#ifndef SECP256K1_SILENTPAYMENTS_H
#define SECP256K1_SILENTPAYMENTS_H
+#include <stdint.h>
#include "secp256k1.h"
#include "secp256k1_extrakeys.h"
@@ -26,7 +27,6 @@ extern "C" {
* any further elliptic-curve operations from the wallet.
*/
-
/** The data from a single recipient address
*
* This struct serves as an input argument to `silentpayments_sender_create_outputs`.
@@ -226,6 +226,159 @@ SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_silentpayments_recipien
const secp256k1_silentpayments_label *label
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4);
+/** Opaque data structure that holds Silent Payments prevouts summary data.
+ *
+ * The exact representation of data inside is implementation defined and not
+ * 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`.
+ */
+typedef struct secp256k1_silentpayments_prevouts_summary {
+ unsigned char data[101];
+} secp256k1_silentpayments_prevouts_summary;
+
+/** Compute Silent Payments prevouts summary from prevout public keys and transaction
+ * inputs.
+ *
+ * Given a list of n public keys A_1...A_n (one for each Silent Payments
+ * eligible input to spend) and a serialized outpoint_smallest36, create a
+ * `prevouts_summary` object. This object summarizes the prevout data from the
+ * transaction inputs needed for scanning.
+ *
+ * `outpoint_smallest36` refers to the smallest outpoint lexicographically
+ * from the transaction inputs (both Silent Payments eligible and non-eligible
+ * inputs). This value MUST be the smallest outpoint out of all of the
+ * transaction inputs, otherwise the recipient will be unable to find the
+ * payment.
+ *
+ * The public keys have to be passed in via two different parameter pairs, one
+ * for regular and one for x-only public keys, in order to avoid the need of
+ * users converting to a common public key format before calling this function.
+ * The resulting data can be used for scanning on the recipient side.
+ *
+ * Returns: 1 if prevouts summary creation was successful.
+ * 0 if the transaction is not a Silent Payments transaction.
+ *
+ * Args: ctx: pointer to a context object
+ * Out: prevouts_summary: pointer to prevouts_summary object containing the
+ * summed public key and input_hash.
+ * In: outpoint_smallest36: serialized smallest outpoint (lexicographically)
+ * from the transaction inputs
+ * xonly_pubkeys: pointer to an array of pointers to taproot
+ * x-only public keys (can be NULL if no taproot
+ * inputs are used)
+ * n_xonly_pubkeys: the size of the xonly_pubkeys array.
+ * pubkeys: pointer to an array of pointers to non-taproot
+ * public keys (can be NULL if no non-taproot
+ * inputs are used)
+ * n_pubkeys: the size of the pubkeys array.
+ */
+SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_silentpayments_recipient_prevouts_summary_create(
+ const secp256k1_context *ctx,
+ secp256k1_silentpayments_prevouts_summary *prevouts_summary,
+ const unsigned char *outpoint_smallest36,
+ const secp256k1_xonly_pubkey * const *xonly_pubkeys,
+ size_t n_xonly_pubkeys,
+ const secp256k1_pubkey * const *pubkeys,
+ size_t n_pubkeys
+) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3);
+
+/** Type of callback function for label lookups
+ *
+ * A function of this type will be used to retrieve the label tweak for a given
+ * label during scanning. A typical implementation will perform a lookup in a
+ * key-value store called the "label cache".
+ *
+ * For creating the label cache data,
+ * `secp256k1_silentpayments_recipient_label_create` and
+ * `secp256k1_silentpayments_recipient_label_serialize` can be used.
+ *
+ * Returns: pointer to the 32-byte label tweak if there is a match.
+ * NULL pointer if there is no match.
+ *
+ * In: label: pointer to the serialized 33-byte label to check
+ * (computed during scanning)
+ * label_context: pointer to the recipient's label cache.
+ */
+typedef const unsigned char* (*secp256k1_silentpayments_label_lookup)(const unsigned char* label33, const void* label_context);
+
+/** Found outputs struct
+ *
+ * Struct for holding a found output along with data needed to spend it later.
+ *
+ * output: the x-only public key for the taproot output
+ * tweak: the 32-byte tweak needed to spend the output
+ * found_with_label: boolean value to indicate if the output was sent to a
+ * labeled address. If true, label will be set to a valid value.
+ * label: the label used. If found_with_label = false, this is set to
+ * an invalid value.
+ */
+typedef struct secp256k1_silentpayments_found_output {
+ secp256k1_xonly_pubkey output;
+ unsigned char tweak[32];
+ int found_with_label;
+ secp256k1_silentpayments_label label;
+} secp256k1_silentpayments_found_output;
+
+/** Scan for Silent Payments transaction outputs.
+ *
+ * Given a prevouts_summary object, a recipient's 32 byte scan key and spend public key,
+ * and the relevant transaction outputs, scan for outputs belonging to
+ * the recipient and return the tweak(s) needed for spending the output(s). An
+ * optional label_lookup callback function and label_context can be passed if
+ * the recipient uses labels. This allows for checking if a label exists in
+ * the recipients label cache and retrieving the label tweak during scanning.
+ *
+ * If used, the `label_lookup` function must return a pointer to a 32-byte label
+ * tweak if the label is found, or NULL otherwise. The returned pointer must remain
+ * valid until the next call to `label_lookup` or until the function returns,
+ * whichever comes first. It is not retained beyond that.
+ *
+ * For creating the label cache, `secp256k1_silentpayments_recipient_label_create`
+ * and `secp256k1_silentpayments_recipient_label_serialize` can be used.
+ *
+ * Returns: 1 if output scanning was successful.
+ * 0 if the transaction is not a Silent Payments transaction,
+ * or if the arguments are invalid.
+ *
+ * Args: ctx: pointer to a context object
+ * Out: found_outputs: pointer to an array of pointers to found
+ * output objects. The found outputs array MUST
+ * have the same length as the tx_outputs array.
+ * n_found_outputs: pointer to an integer indicating the final
+ * size of the found outputs array. This number
+ * represents the number of outputs found while
+ * scanning (0 if none are found).
+ * In: tx_outputs: pointer to the transaction's x-only public key outputs
+ * n_tx_outputs: the size of the tx_outputs array.
+ * 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`).
+ * unlabeled_spend_pubkey: pointer to the recipient's unlabeled spend public key
+ * label_lookup: pointer to a callback function for looking up
+ * a label value. This function takes a serialized 33-byte
+ * label as an argument and returns a pointer to the
+ * 32-byte label tweak if the label exists, otherwise
+ * returns a NULL pointer (NULL if labels are not
+ * used)
+ * label_context: pointer to a label context object (NULL if
+ * labels are not used or context is not needed)
+ */
+SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_silentpayments_recipient_scan_outputs(
+ const secp256k1_context *ctx,
+ secp256k1_silentpayments_found_output **found_outputs,
+ uint32_t *n_found_outputs,
+ const secp256k1_xonly_pubkey * const *tx_outputs,
+ size_t n_tx_outputs,
+ const unsigned char *scan_key32,
+ const secp256k1_silentpayments_prevouts_summary *prevouts_summary,
+ const secp256k1_pubkey *unlabeled_spend_pubkey,
+ secp256k1_silentpayments_label_lookup label_lookup,
+ 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);
+
#ifdef __cplusplus
}
#endif
diff --git a/src/modules/silentpayments/main_impl.h b/src/modules/silentpayments/main_impl.h
index 38acd15..a4eb344 100644
--- a/src/modules/silentpayments/main_impl.h
+++ b/src/modules/silentpayments/main_impl.h
@@ -18,6 +18,9 @@
#include "../../hash.h"
#include "../../hsort.h"
+/** magic bytes for ensuring prevouts_summary objects were initialized correctly. */
+static const unsigned char secp256k1_silentpayments_prevouts_summary_magic[4] = { 0xa7, 0x1c, 0xd3, 0x5e };
+
/** Sort an array of Silent Payments recipients. This is used to group recipients by scan pubkey to
* ensure the correct values of k are used when creating multiple outputs for a recipient.
* Since heap sort is unstable, we use the recipient's index as tie-breaker to have a well-defined
@@ -451,4 +454,300 @@ int secp256k1_silentpayments_recipient_create_labeled_spend_pubkey(const secp256
return 1;
}
+/** An explanation of the prevouts_summary object and its usage:
+ *
+ * The prevouts_summary object contains:
+ *
+ * [magic: 4 bytes][boolean: 1 byte][prevouts_pubkey_sum: 64 bytes][input_hash: 32 bytes]
+ *
+ * The magic bytes are checked by functions using the prevouts_summary object to
+ * check that the prevouts_summary object was initialized correctly.
+ *
+ * The boolean (combined) indicates whether or not the summed prevout public keys and the
+ * input_hash scalar have already been combined or are both included. The reason
+ * for keeping input_hash and the summed prevout public keys separate is so that an elliptic
+ * 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.
+ *
+ * For each function:
+ *
+ * - `_recipient_prevouts_summary_create` always creates a prevouts_summary object with combined = false
+ */
+
+int secp256k1_silentpayments_recipient_prevouts_summary_create(
+ const secp256k1_context *ctx,
+ secp256k1_silentpayments_prevouts_summary *prevouts_summary,
+ const unsigned char *outpoint_smallest36,
+ const secp256k1_xonly_pubkey * const *xonly_pubkeys,
+ size_t n_xonly_pubkeys,
+ const secp256k1_pubkey * const *pubkeys,
+ size_t n_pubkeys
+) {
+ size_t i;
+ secp256k1_ge prevouts_pubkey_sum_ge, addend;
+ secp256k1_gej prevouts_pubkey_sum_gej;
+ secp256k1_scalar input_hash_scalar;
+
+ /* Sanity check inputs */
+ VERIFY_CHECK(ctx != NULL);
+ ARG_CHECK(prevouts_summary != NULL);
+ memset(prevouts_summary, 0, sizeof(*prevouts_summary));
+ ARG_CHECK(outpoint_smallest36 != NULL);
+ ARG_CHECK((pubkeys != NULL) || (xonly_pubkeys != NULL));
+ if (xonly_pubkeys != NULL) {
+ ARG_CHECK(n_xonly_pubkeys > 0);
+ for (i = 0; i < n_xonly_pubkeys; i++) {
+ ARG_CHECK(xonly_pubkeys[i] != NULL);
+ }
+ } else {
+ ARG_CHECK(n_xonly_pubkeys == 0);
+ }
+ if (pubkeys != NULL) {
+ ARG_CHECK(n_pubkeys > 0);
+ for (i = 0; i < n_pubkeys; i++) {
+ ARG_CHECK(pubkeys[i] != NULL);
+ }
+ } else {
+ ARG_CHECK(n_pubkeys == 0);
+ }
+
+ /* Compute prevouts_pubkey_sum = A_1 + A_2 + ... + A_n.
+ *
+ * Since an attacker can maliciously craft transactions where the public keys sum to zero, fail early here
+ * to avoid making the caller do extra work, e.g., when building an index or scanning a malicious transaction.
+ *
+ * This will also fail if any of the provided prevout public keys are malformed.
+ */
+ secp256k1_gej_set_infinity(&prevouts_pubkey_sum_gej);
+ for (i = 0; i < n_pubkeys; i++) {
+ if (!secp256k1_pubkey_load(ctx, &addend, pubkeys[i])) {
+ return 0;
+ }
+ secp256k1_gej_add_ge_var(&prevouts_pubkey_sum_gej, &prevouts_pubkey_sum_gej, &addend, NULL);
+ }
+ for (i = 0; i < n_xonly_pubkeys; i++) {
+ if (!secp256k1_xonly_pubkey_load(ctx, &addend, xonly_pubkeys[i])) {
+ return 0;
+ }
+ secp256k1_gej_add_ge_var(&prevouts_pubkey_sum_gej, &prevouts_pubkey_sum_gej, &addend, NULL);
+ }
+ if (secp256k1_gej_is_infinity(&prevouts_pubkey_sum_gej)) {
+ return 0;
+ }
+ secp256k1_ge_set_gej_var(&prevouts_pubkey_sum_ge, &prevouts_pubkey_sum_gej);
+ /* Calculate the input_hash and convert it to a scalar.
+ *
+ * Note: _input_hash_scalar can only fail if the output of the hash function is zero or greater than or equal to the
+ * curve order, which is statistically improbable. Returning an error here results in an untestable branch in the
+ * code, but we do this anyways to ensure strict compliance with BIP0352.
+ */
+ if (!secp256k1_silentpayments_calculate_input_hash_scalar(secp256k1_get_hash_context(ctx), &input_hash_scalar, outpoint_smallest36, &prevouts_pubkey_sum_ge)) {
+ return 0;
+ }
+ memcpy(&prevouts_summary->data[0], secp256k1_silentpayments_prevouts_summary_magic, 4);
+ prevouts_summary->data[4] = 0;
+ secp256k1_ge_to_bytes(&prevouts_summary->data[5], &prevouts_pubkey_sum_ge);
+ secp256k1_scalar_get_b32(&prevouts_summary->data[5 + 64], &input_hash_scalar);
+ return 1;
+}
+
+int secp256k1_silentpayments_recipient_scan_outputs(
+ const secp256k1_context *ctx,
+ secp256k1_silentpayments_found_output **found_outputs, uint32_t *n_found_outputs,
+ const secp256k1_xonly_pubkey * const *tx_outputs, size_t n_tx_outputs,
+ const unsigned char *scan_key32,
+ const secp256k1_silentpayments_prevouts_summary *prevouts_summary,
+ const secp256k1_pubkey *unlabeled_spend_pubkey,
+ secp256k1_silentpayments_label_lookup label_lookup,
+ const void *label_context
+) {
+ secp256k1_scalar scan_key_scalar;
+ secp256k1_ge unlabeled_spend_pubkey_ge, prevouts_pubkey_sum_ge, tx_output_ge;
+ unsigned char shared_secret[33];
+ uint32_t k;
+ size_t i;
+ int found_idx, combined, valid_scan_key, ret;
+
+ /* Sanity check inputs */
+ VERIFY_CHECK(ctx != NULL);
+ ARG_CHECK(found_outputs != NULL);
+ ARG_CHECK(n_found_outputs != NULL);
+ *n_found_outputs = 0;
+ ARG_CHECK(tx_outputs != NULL);
+ ARG_CHECK(n_tx_outputs > 0);
+ for (i = 0; i < n_tx_outputs; i++) {
+ ARG_CHECK(found_outputs[i] != NULL);
+ ARG_CHECK(tx_outputs[i] != NULL);
+ /* Validate each tx output object early so malformed pubkeys are always rejected. */
+ if (!secp256k1_xonly_pubkey_load(ctx, &tx_output_ge, tx_outputs[i])) {
+ return 0;
+ }
+ }
+ 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(unlabeled_spend_pubkey != NULL);
+ /* Passing a context without a lookup function is non-sensical */
+ if (label_context != NULL) {
+ ARG_CHECK(label_lookup != NULL);
+ }
+ 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);
+ }
+ ret = secp256k1_pubkey_load(ctx, &unlabeled_spend_pubkey_ge, unlabeled_spend_pubkey);
+ if (!ret) {
+ secp256k1_scalar_clear(&scan_key_scalar);
+ return 0;
+ }
+ /* Creating the shared secret requires that the public and secret components are
+ * non-infinity and non-zero, respectively. Note that the involved parts (input hash,
+ * scan secret key, and prevouts public key sum) have all been verified at this point,
+ * assuming that the user hasn't tampered the `prevouts_summary` object manually. */
+ secp256k1_silentpayments_create_shared_secret(shared_secret, &prevouts_pubkey_sum_ge, &scan_key_scalar);
+ /* Clear the scan_key_scalar since we no longer need it and leaking this value would break indistinguishability of the transaction. */
+ secp256k1_scalar_clear(&scan_key_scalar);
+
+ found_idx = -1;
+ /* TODO: limit number of k iterations, in order to avoid quadratic scaling issue */
+ /* TODO: improve scanning performance by performing batch inversion for label scanning */
+ for (k = 0; k < n_tx_outputs; k++) {
+ secp256k1_scalar t_k_scalar;
+ secp256k1_xonly_pubkey unlabeled_output_xonly;
+ secp256k1_ge unlabeled_output_ge = unlabeled_spend_pubkey_ge;
+ secp256k1_ge unlabeled_output_negated_ge;
+ const unsigned char *label_tweak = NULL;
+ secp256k1_ge label_ge;
+ size_t j;
+
+ /* 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 the curve order, which is statistically improbable. Returning an error here results in an untestable
+ * branch in the code, but we do this anyways to ensure strict compliance with BIP0352.
+ */
+ if (!secp256k1_silentpayments_create_output_tweak(ctx, &t_k_scalar, shared_secret, k)) {
+ secp256k1_scalar_clear(&t_k_scalar);
+ secp256k1_memclear_explicit(&shared_secret, sizeof(shared_secret));
+ return 0;
+ }
+
+ /* Calculate unlabeled_output = unlabeled_spend_pubkey + t_k * G.
+ * This can fail if t_k * G is the negation of unlabeled_spend_pubkey, but this happens only with negligible
+ * probability for honestly created unlabeled_spend_pubkey as t_k is the output of a hash function. */
+ if (!secp256k1_eckey_pubkey_tweak_add(&unlabeled_output_ge, &t_k_scalar)) {
+ /* Leaking these values would break indistinguishability of the transaction, so clear them. */
+ secp256k1_scalar_clear(&t_k_scalar);
+ secp256k1_memclear_explicit(&shared_secret, sizeof(shared_secret));
+ return 0;
+ }
+ /* Calculate unlabeled_output_negated = -unlabeled_output */
+ secp256k1_ge_neg(&unlabeled_output_negated_ge, &unlabeled_output_ge);
+
+ found_idx = -1;
+ secp256k1_fe_normalize_var(&unlabeled_output_ge.y);
+ secp256k1_extrakeys_ge_even_y(&unlabeled_output_ge);
+ secp256k1_xonly_pubkey_save(&unlabeled_output_xonly, &unlabeled_output_ge);
+ for (j = 0; j < n_tx_outputs; j++) {
+ if (secp256k1_xonly_pubkey_cmp(ctx, &unlabeled_output_xonly, tx_outputs[j]) == 0) {
+ label_tweak = NULL;
+ found_idx = j;
+ break;
+ }
+
+ /* If not found, proceed to check for labels (if a label lookup function is provided). */
+ if (label_lookup != NULL) {
+ secp256k1_gej tx_output_gej;
+ secp256k1_gej label_candidates_gej[2];
+ secp256k1_ge label_candidates_ge[2];
+
+ secp256k1_xonly_pubkey_load(ctx, &tx_output_ge, tx_outputs[j]);
+ secp256k1_gej_set_ge(&tx_output_gej, &tx_output_ge);
+ /* Calculate scan label candidates:
+ * label_candidate1 = tx_output - unlabeled_output
+ * label_candidate2 = -tx_output - unlabeled_output
+ */
+ secp256k1_gej_add_ge_var(&label_candidates_gej[0], &tx_output_gej, &unlabeled_output_negated_ge, NULL);
+ secp256k1_gej_neg(&tx_output_gej, &tx_output_gej);
+ secp256k1_gej_add_ge_var(&label_candidates_gej[1], &tx_output_gej, &unlabeled_output_negated_ge, NULL);
+ secp256k1_ge_set_all_gej_var(label_candidates_ge, label_candidates_gej, 2);
+
+ /* Check if either of the label candidates is in the label cache */
+ for (i = 0; i < 2; i++) {
+ unsigned char label33[33];
+ /* Note: serialize will only fail if label_ge is the point at infinity, but we know this
+ * cannot happen since we only hit this branch if tx_output != unlabeled_output_xonly.
+ * Thus, we know that label_ge = tx_output_gej + unlabeled_output_negated_ge cannot be the
+ * point at infinity.
+ */
+ secp256k1_eckey_pubkey_serialize33(&label_candidates_ge[i], label33);
+ label_tweak = label_lookup(label33, label_context);
+ if (label_tweak != NULL) {
+ found_idx = j;
+ label_ge = label_candidates_ge[i];
+ break;
+ }
+ }
+ if (found_idx != -1) {
+ break;
+ }
+ }
+ }
+ if (found_idx != -1) {
+ found_outputs[k]->output = *tx_outputs[found_idx];
+ secp256k1_scalar_get_b32(found_outputs[k]->tweak, &t_k_scalar);
+ /* Clear the t_k_scalar since we no longer need it and leaking this value would
+ * break indistinguishability of the transaction. */
+ secp256k1_scalar_clear(&t_k_scalar);
+ if (label_tweak != NULL) {
+ found_outputs[k]->found_with_label = 1;
+ /* This is extremely unlikely to fail in that it can only really happen if label_tweak
+ * is the negation of the shared secret tweak. But since both tweak and label_tweak are
+ * created by hashing data, practically speaking this would only happen if an attacker
+ * tricked us into using a particular label_tweak (deviating from the protocol).
+ * Note that this call could also fail due to a malformed label_tweak data from the
+ * label cache, but we generally assume the passed in data is created using the API
+ * functions and thus have already been checked for correctness.
+ *
+ * Furthermore, although technically a failure for ec_seckey_tweak_add, this is not treated
+ * as a failure for Silent Payments because the output is still spendable with just the
+ * spend secret key. We set `tweak = 0` for this case.
+ */
+ if (!secp256k1_ec_seckey_tweak_add(ctx, found_outputs[k]->tweak, label_tweak)) {
+ memset(found_outputs[k]->tweak, 0, 32);
+ }
+ secp256k1_silentpayments_label_save(&found_outputs[k]->label, &label_ge);
+ } else {
+ found_outputs[k]->found_with_label = 0;
+ /* Set the label to an invalid value. */
+ memset(&found_outputs[k]->label, 0, sizeof(found_outputs[k]->label));
+ }
+ /* Reset everything for the next round of scanning. */
+ label_tweak = NULL;
+ } else {
+ secp256k1_scalar_clear(&t_k_scalar);
+ break;
+ }
+ }
+ *n_found_outputs = k;
+
+ /* Leaking the shared_secret would break indistinguishability of the transaction, so clear it. */
+ secp256k1_memclear_explicit(shared_secret, sizeof(shared_secret));
+ return 1;
+}
+
#endif
diff --git a/src/modules/silentpayments/tests_impl.h b/src/modules/silentpayments/tests_impl.h
index de6842c..c880460 100644
--- a/src/modules/silentpayments/tests_impl.h
+++ b/src/modules/silentpayments/tests_impl.h
@@ -84,6 +84,31 @@ static unsigned char ALICE_SECKEY[32] = {
0x15, 0x42, 0x01, 0xb8, 0xe5, 0xdf, 0xf3, 0xb1
};
+struct label_cache_entry {
+ unsigned char label[33];
+ unsigned char label_tweak[32];
+};
+struct labels_cache {
+ size_t entries_used;
+ struct label_cache_entry entries[10];
+};
+struct labels_cache labels_cache;
+const unsigned char* label_lookup(const unsigned char* key, const void* cache_ptr) {
+ const struct labels_cache* cache;
+ size_t i;
+
+ if (cache_ptr == NULL) {
+ return NULL;
+ }
+ cache = (const struct labels_cache*)cache_ptr;
+ for (i = 0; i < cache->entries_used; i++) {
+ if (secp256k1_memcmp_var(cache->entries[i].label, key, 33) == 0) {
+ return cache->entries[i].label_tweak;
+ }
+ }
+ return NULL;
+}
+
static void test_recipient_sort_helper(unsigned char (*sp_addresses[3])[2][33], unsigned char (*sp_outputs[3])[32]) {
unsigned char const *seckey_ptrs[1];
secp256k1_silentpayments_recipient recipients[3];
@@ -363,11 +388,165 @@ static void test_label_api(void) {
}
}
+static void test_recipient_api(void) {
+ secp256k1_silentpayments_prevouts_summary ps; /* prevouts_summary */
+ secp256k1_silentpayments_found_output f; /* a silent payment found output */
+ secp256k1_silentpayments_found_output *fp[1]; /* array of pointers to found outputs */
+ secp256k1_xonly_pubkey t; /* taproot x-only public key */
+ secp256k1_xonly_pubkey malformed_t; /* malformed x-only public key */
+ secp256k1_xonly_pubkey const *tp[1]; /* array of pointers to xonly pks */
+ secp256k1_pubkey p; /* plain public key */
+ secp256k1_pubkey malformed_p; /* malformed public key */
+ secp256k1_pubkey const *pp[1]; /* array of pointers to plain pks */
+ uint32_t n_f; /* number of found outputs */
+
+ CHECK(secp256k1_ec_pubkey_parse(CTX, &p, BOB_ADDRESS[0], 33));
+ memset(&malformed_p, 0, sizeof(malformed_p));
+ memset(&malformed_t, 0, sizeof(malformed_t));
+ CHECK(secp256k1_xonly_pubkey_parse(CTX, &t, &BOB_ADDRESS[0][1]));
+ tp[0] = &t;
+ pp[0] = &p;
+ fp[0] = &f;
+ CHECK(secp256k1_silentpayments_recipient_prevouts_summary_create(CTX, &ps, SMALLEST_OUTPOINT, tp, 1, pp, 1));
+ /* Reusing a prevouts_summary after a failure must not leave the old object valid. */
+ {
+ secp256k1_pubkey neg_p = p;
+ secp256k1_pubkey const *pp_sum_zero[2];
+
+ CHECK(secp256k1_ec_pubkey_negate(CTX, &neg_p));
+ pp_sum_zero[0] = &p;
+ pp_sum_zero[1] = &neg_p;
+ CHECK(secp256k1_silentpayments_recipient_prevouts_summary_create(CTX, &ps, SMALLEST_OUTPOINT, NULL, 0, pp_sum_zero, 2) == 0);
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_scan_outputs(CTX, fp, &n_f, tp, 1, ALICE_SECKEY, &ps, &p, NULL, NULL));
+ CHECK(secp256k1_silentpayments_recipient_prevouts_summary_create(CTX, &ps, SMALLEST_OUTPOINT, tp, 1, pp, 1));
+ }
+ /* Check that malformed input public keys are caught. Input public keys summing to zero is tested later,
+ * in the BIP0352 test vectors.
+ */
+ pp[0] = &malformed_p;
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_prevouts_summary_create(CTX, &ps, SMALLEST_OUTPOINT, tp, 1, pp, 1));
+ pp[0] = &p;
+ /* Check that malformed x-only input public keys are caught. */
+ tp[0] = &malformed_t;
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_prevouts_summary_create(CTX, &ps, SMALLEST_OUTPOINT, tp, 1, pp, 1));
+ tp[0] = &t;
+
+ /* Check that NULL in "array of pointers" arguments is not allowed */
+ {
+ secp256k1_xonly_pubkey const *original_ptr = tp[0];
+ tp[0] = NULL;
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_prevouts_summary_create(CTX, &ps, SMALLEST_OUTPOINT, tp, 1, pp, 1));
+ tp[0] = original_ptr;
+ }
+ {
+ secp256k1_pubkey const *original_ptr = pp[0];
+ pp[0] = NULL;
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_prevouts_summary_create(CTX, &ps, SMALLEST_OUTPOINT, tp, 1, pp, 1));
+ pp[0] = original_ptr;
+ }
+
+ /* Check null values are handled */
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_prevouts_summary_create(CTX, NULL, SMALLEST_OUTPOINT, tp, 1, pp, 1));
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_prevouts_summary_create(CTX, &ps, NULL, tp, 1, pp, 1));
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_prevouts_summary_create(CTX, &ps, SMALLEST_OUTPOINT, NULL, 1, pp, 1));
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_prevouts_summary_create(CTX, &ps, SMALLEST_OUTPOINT, tp, 1, NULL, 1));
+
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_prevouts_summary_create(CTX, &ps, SMALLEST_OUTPOINT, tp, 0, pp, 1));
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_prevouts_summary_create(CTX, &ps, SMALLEST_OUTPOINT, tp, 1, pp, 0));
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_prevouts_summary_create(CTX, &ps, SMALLEST_OUTPOINT, NULL, 0, pp, 0));
+ 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));
+
+ /* 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 */
+ {
+ unsigned char output_tweak[32] = {
+ 0x96, 0x32, 0xb4, 0x06, 0xeb, 0x56, 0xcc, 0xb2,
+ 0x0f, 0xc6, 0xe5, 0x2c, 0x41, 0xd5, 0x73, 0xb2,
+ 0xae, 0xa0, 0x45, 0x07, 0x63, 0xf1, 0xf6, 0x22,
+ 0xfa, 0x87, 0xc2, 0x4c, 0x7d, 0x80, 0x58, 0x62,
+ };
+ secp256k1_pubkey neg_spend_pubkey;
+ CHECK(secp256k1_ec_pubkey_create(CTX, &neg_spend_pubkey, output_tweak));
+ CHECK(secp256k1_ec_pubkey_negate(CTX, &neg_spend_pubkey));
+ CHECK(secp256k1_silentpayments_recipient_scan_outputs(CTX, fp, &n_f, tp, 1, ALICE_SECKEY, &ps, &neg_spend_pubkey, &label_lookup, &labels_cache) == 0);
+ }
+ /* check the _recipients_scan_outputs cornercase where the output_tweak is the negation of the label_tweak */
+ {
+ unsigned char output_tweak[32] = {
+ 0x96, 0x32, 0xb4, 0x06, 0xeb, 0x56, 0xcc, 0xb2,
+ 0x0f, 0xc6, 0xe5, 0x2c, 0x41, 0xd5, 0x73, 0xb2,
+ 0xae, 0xa0, 0x45, 0x07, 0x63, 0xf1, 0xf6, 0x22,
+ 0xfa, 0x87, 0xc2, 0x4c, 0x7d, 0x80, 0x58, 0x62,
+ };
+ static const unsigned char zero[32] = {0};
+ secp256k1_pubkey spk, neg_label_pubkey;
+ secp256k1_xonly_pubkey output_xonly;
+ secp256k1_xonly_pubkey const *output_xonly_ptrs[1];
+ size_t len = 33;
+ uint32_t found;
+ CHECK(secp256k1_ec_pubkey_parse(CTX, &spk, BOB_ADDRESS[0], 33));
+ CHECK(secp256k1_xonly_pubkey_from_pubkey(CTX, &output_xonly, NULL, &spk));
+ output_xonly_ptrs[0] = &output_xonly;
+ CHECK(secp256k1_ec_seckey_negate(CTX, output_tweak));
+ CHECK(secp256k1_ec_pubkey_create(CTX, &neg_label_pubkey, output_tweak));
+ CHECK(secp256k1_ec_pubkey_serialize(CTX, labels_cache.entries[0].label, &len, &neg_label_pubkey, SECP256K1_EC_COMPRESSED));
+ memcpy(labels_cache.entries[0].label_tweak, output_tweak, 32);
+ labels_cache.entries_used = 1;
+ found = 0;
+ CHECK(secp256k1_silentpayments_recipient_scan_outputs(CTX, fp, &found, output_xonly_ptrs, 1, ALICE_SECKEY, &ps, &spk, &label_lookup, &labels_cache));
+ CHECK(found == 1);
+ CHECK(secp256k1_memcmp_var(fp[0]->tweak, zero, 32) == 0);
+ CHECK(fp[0]->found_with_label == 1);
+ {
+ unsigned char found_label_ser[33];
+ CHECK(secp256k1_silentpayments_recipient_label_serialize(CTX, found_label_ser, &fp[0]->label));
+ CHECK(secp256k1_memcmp_var(found_label_ser, labels_cache.entries[0].label, 33) == 0);
+ }
+ }
+
+ n_f = 0;
+ labels_cache.entries_used = 0;
+ CHECK(secp256k1_silentpayments_recipient_scan_outputs(CTX, fp, &n_f, tp, 1, ALICE_SECKEY, &ps, &p, &label_lookup, &labels_cache));
+ CHECK(secp256k1_silentpayments_recipient_scan_outputs(CTX, fp, &n_f, tp, 1, ALICE_SECKEY, &ps, &p, &label_lookup, NULL));
+ CHECK(secp256k1_silentpayments_recipient_scan_outputs(CTX, fp, &n_f, tp, 1, ALICE_SECKEY, &ps, &p, NULL, NULL));
+
+ /* Check that NULL in "array of pointers" arguments is not allowed */
+ {
+ secp256k1_silentpayments_found_output *original_ptr = fp[0];
+ fp[0] = NULL;
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_scan_outputs(CTX, fp, &n_f, tp, 1, ALICE_SECKEY, &ps, &p, NULL, NULL));
+ fp[0] = original_ptr;
+ }
+ {
+ secp256k1_xonly_pubkey const *original_ptr = tp[0];
+ tp[0] = NULL;
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_scan_outputs(CTX, fp, &n_f, tp, 1, ALICE_SECKEY, &ps, &p, NULL, NULL));
+ tp[0] = original_ptr;
+ }
+
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_scan_outputs(CTX, NULL, &n_f, tp, 1, ALICE_SECKEY, &ps, &p, &label_lookup, &labels_cache));
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_scan_outputs(CTX, fp, NULL, tp, 1, ALICE_SECKEY, &ps, &p, &label_lookup, &labels_cache));
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_scan_outputs(CTX, fp, &n_f, NULL, 1, ALICE_SECKEY, &ps, &p, &label_lookup, &labels_cache));
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_scan_outputs(CTX, fp, &n_f, tp, 1, NULL, &ps, &p, &label_lookup, &labels_cache));
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_scan_outputs(CTX, fp, &n_f, tp, 1, ALICE_SECKEY, NULL, &p, &label_lookup, &labels_cache));
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_scan_outputs(CTX, fp, &n_f, tp, 1, ALICE_SECKEY, &ps, NULL, &label_lookup, &labels_cache));
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_scan_outputs(CTX, fp, &n_f, tp, 0, ALICE_SECKEY, &ps, &p, &label_lookup, &labels_cache));
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_scan_outputs(CTX, fp, &n_f, tp, 1, ALICE_SECKEY, &ps, &p, NULL, &labels_cache));
+
+ /* Check that malformed secret key, public keys, and prevouts_summary arguments are handled */
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_scan_outputs(CTX, fp, &n_f, tp, 1, ALICE_SECKEY, &ps, &malformed_p, NULL, NULL));
+ 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));
+}
+
/* --- Test registry --- */
static const struct tf_test_entry tests_silentpayments[] = {
CASE1(test_recipient_sort),
CASE1(test_send_api),
CASE1(test_label_api),
+ CASE1(test_recipient_api),
};
#endif
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.