What changed, and why it matters
This commit adds a new feature to the secp256k1 cryptographic library: the sender-side logic for Bitcoin Silent Payments (BIP352). It lets a wallet create special one-time payment addresses for recipients without revealing which recipient is being paid on the blockchain. The change is a normal, well-documented feature addition with no indication of a security bug or vulnerability fix.
No security action required. Treat as a normal feature addition. Reviewers may optionally audit the new BIP352 implementation for spec compliance and side-channel behavior, but the commit itself does not indicate a vulnerability.
Security signals we found
New feature implementation for BIP352 Silent Payments sender side
Extensive input validation (ARG_CHECK) and test coverage for malformed keys and bad arguments
Sensitive intermediate values (shared secrets, scalars) are explicitly cleared
Constant-time serialization of shared secret to preserve transaction indistinguishability
No vendor disclosure of security relevance, no CVE, no bug fix language in commit message
Evidence from the diff
The commit implements secp256k1_silentpayments_sender_create_outputs() and supporting helpers for BIP352 sender-side output generation. It sums input secret keys, computes the BIP352 input hash, groups recipients by scan public key, derives shared secrets via ECDH (a*B/b*A), and produces x-only output public keys using the BIP352/SharedSecret tagged hash. The code includes ARG_CHECK/VERIFY_CHECK input validation, constant-time serialization of shared secrets, explicit clearing of sensitive intermediates, and a test suite covering API misuse, malformed keys, and recipient ordering. There is no patch of an existing vulnerability; this is new feature code.
Changed components
include/secp256k1_silentpayments.hsrc/modules/silentpayments/main_impl.hsrc/modules/silentpayments/tests_impl.hsrc/tests.cInspect captured patch +719 / −1
diff --git a/include/secp256k1_silentpayments.h b/include/secp256k1_silentpayments.h
index edc4609..5eb9825 100644
--- a/include/secp256k1_silentpayments.h
+++ b/include/secp256k1_silentpayments.h
@@ -2,6 +2,7 @@
#define SECP256K1_SILENTPAYMENTS_H
#include "secp256k1.h"
+#include "secp256k1_extrakeys.h"
#ifdef __cplusplus
extern "C" {
@@ -25,6 +26,106 @@ 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`.
+ *
+ * `index` must be set to the position (starting with 0) of this recipient in the
+ * `recipients` array passed to `silentpayments_sender_create_outputs`. It is
+ * used to map the returned generated outputs back to the original recipient.
+ *
+ * Note:
+ * The spend public key named `spend_pubkey` may have been optionally tweaked with
+ * a label by the recipient. Whether `spend_pubkey` has actually been tagged with
+ * a label is irrelevant for the sender. As a documentation convention in this API,
+ * `unlabeled_spend_pubkey` is used to indicate when the unlabeled spend public key
+ * must be used.
+ */
+typedef struct secp256k1_silentpayments_recipient {
+ secp256k1_pubkey scan_pubkey;
+ secp256k1_pubkey spend_pubkey;
+ size_t index;
+} secp256k1_silentpayments_recipient;
+
+/** Create Silent Payments outputs for recipient(s).
+ *
+ * Given a list of n secret keys a_1...a_n (one for each Silent Payments
+ * eligible input to spend), a serialized outpoint, and a list of recipients,
+ * create the taproot outputs. Inputs with conditional branches or multiple
+ * public keys are excluded from Silent Payments eligible inputs; see BIP352
+ * for more information.
+ *
+ * `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. Determining the smallest outpoint from the list of transaction
+ * inputs is the responsibility of the caller. It is strongly recommended
+ * that implementations ensure they are doing this correctly by using the
+ * test vectors from BIP352.
+ *
+ * When creating more than one generated output, all of the generated outputs
+ * MUST be included in the final transaction. Dropping any of the generated
+ * outputs from the final transaction may make all or some of the outputs
+ * unfindable by the recipient.
+ *
+ * Returns: 1 if creation of outputs was successful.
+ * 0 on failure. This is expected only with an adversarially chosen
+ * recipient spend key. Specifically, failure occurs when:
+ * - Input secret keys sum to 0
+ * (negligible probability if at least one of the input secret
+ * keys is uniformly random and independent of all other keys)
+ * - A hash output is not a valid scalar (negligible probability
+ * per hash evaluation)
+ *
+ * Args: ctx: pointer to a context object
+ * (not secp256k1_context_static).
+ * Out: generated_outputs: pointer to an array of pointers to xonly public keys,
+ * one per recipient.
+ * The outputs are ordered to match the original
+ * ordering of the recipient objects, i.e.,
+ * `generated_outputs[0]` is the generated output
+ * for the `secp256k1_silentpayments_recipient` object
+ * with index = 0.
+ * In: recipients: pointer to an array of pointers to Silent Payments
+ * recipients, where each recipient is a scan public
+ * key, a spend public key, and an index indicating
+ * its position in the original ordering. The
+ * recipient array will be grouped by scan public key
+ * in place (as specified in BIP0352), but generated
+ * outputs are saved in the `generated_outputs` array
+ * to match the original ordering (using the index
+ * field). This ensures the caller is able to match
+ * the generated outputs to the correct Silent
+ * Payments addresses. The same recipient can be
+ * passed multiple times to create multiple outputs
+ * for the same recipient.
+ * n_recipients: the size of the recipients array.
+ * outpoint_smallest36: serialized (36-byte) smallest outpoint
+ * (lexicographically) from the transaction inputs
+ * keypairs: pointer to an array of pointers to taproot
+ * keypair inputs (can be NULL if no secret keys
+ * of taproot inputs are used)
+ * n_keypairs: the size of the keypairs array.
+ * seckeys: pointer to an array of pointers to 32-byte
+ * secret keys of non-taproot inputs (can be NULL
+ * if no secret keys of non-taproot inputs are
+ * used)
+ * n_seckeys: the size of the seckeys array.
+ */
+SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_silentpayments_sender_create_outputs(
+ const secp256k1_context *ctx,
+ secp256k1_xonly_pubkey **generated_outputs,
+ const secp256k1_silentpayments_recipient **recipients,
+ size_t n_recipients,
+ const unsigned char *outpoint_smallest36,
+ const secp256k1_keypair * const *keypairs,
+ size_t n_keypairs,
+ const unsigned char * const *seckeys,
+ size_t n_seckeys
+) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(5);
+
#ifdef __cplusplus
}
#endif
diff --git a/src/modules/silentpayments/main_impl.h b/src/modules/silentpayments/main_impl.h
index f8ccdd7..733812a 100644
--- a/src/modules/silentpayments/main_impl.h
+++ b/src/modules/silentpayments/main_impl.h
@@ -7,9 +7,324 @@
#define SECP256K1_MODULE_SILENTPAYMENTS_MAIN_H
#include "../../../include/secp256k1.h"
+#include "../../../include/secp256k1_extrakeys.h"
#include "../../../include/secp256k1_silentpayments.h"
-/* TODO: implement functions for sender side. */
+#include "../../eckey.h"
+#include "../../ecmult.h"
+#include "../../ecmult_const.h"
+#include "../../ecmult_gen.h"
+#include "../../group.h"
+#include "../../hash.h"
+#include "../../hsort.h"
+
+/** 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
+ * order, i.e. within scan pubkey groups, the spend pubkeys appear in the same order as they were
+ * passed in.
+ */
+static int secp256k1_silentpayments_recipient_sort_cmp(const void* pk1, const void* pk2, void *ctx) {
+ const secp256k1_silentpayments_recipient *r1 = *(const secp256k1_silentpayments_recipient **)pk1;
+ const secp256k1_silentpayments_recipient *r2 = *(const secp256k1_silentpayments_recipient **)pk2;
+
+ const int ret = secp256k1_ec_pubkey_cmp((secp256k1_context *)ctx, &r1->scan_pubkey, &r2->scan_pubkey);
+ if (ret != 0) {
+ return ret;
+ } else {
+ return (r1->index > r2->index) - (r1->index < r2->index);
+ }
+}
+
+static void secp256k1_silentpayments_recipient_sort(const secp256k1_context* ctx, const secp256k1_silentpayments_recipient **recipients, size_t n_recipients) {
+ /* Suppress wrong warning (fixed in MSVC 19.33) */
+ #if defined(_MSC_VER) && (_MSC_VER < 1933)
+ #pragma warning(push)
+ #pragma warning(disable: 4090)
+ #endif
+
+ secp256k1_hsort(recipients, n_recipients, sizeof(*recipients), secp256k1_silentpayments_recipient_sort_cmp, (void *)ctx);
+
+ #if defined(_MSC_VER) && (_MSC_VER < 1933)
+ #pragma warning(pop)
+ #endif
+}
+
+/** Set hash state to the BIP340 tagged hash midstate for "BIP0352/Inputs". */
+static void secp256k1_silentpayments_sha256_init_inputs(secp256k1_sha256* hash) {
+ static const uint32_t midstate[8] = {
+ 0xd4143ffcul, 0x012ea4b5ul, 0x36e21c8ful, 0xf7ec7b54ul,
+ 0x4dd4e2acul, 0x9bcaa0a4ul, 0xe244899bul, 0xcd06903eul
+ };
+ secp256k1_sha256_initialize_midstate(hash, 64, midstate);
+}
+
+/** Callers must ensure that pubkey_sum is not the point at infinity before calling this function. */
+static int secp256k1_silentpayments_calculate_input_hash_scalar(const secp256k1_hash_ctx *hash_ctx, secp256k1_scalar *input_hash_scalar, const unsigned char *outpoint_smallest36, secp256k1_ge *pubkey_sum) {
+ secp256k1_sha256 hash;
+ unsigned char pubkey_sum_ser[33];
+ unsigned char input_hash[32];
+ int overflow;
+
+ secp256k1_silentpayments_sha256_init_inputs(&hash);
+ secp256k1_sha256_write(hash_ctx, &hash, outpoint_smallest36, 36);
+ secp256k1_eckey_pubkey_serialize33(pubkey_sum, pubkey_sum_ser);
+ secp256k1_sha256_write(hash_ctx, &hash, pubkey_sum_ser, sizeof(pubkey_sum_ser));
+ secp256k1_sha256_finalize(hash_ctx, &hash, input_hash);
+ /* Convert input_hash to a scalar.
+ *
+ * This can only fail if the output of the hash function is zero or greater than or equal to the curve order, which
+ * happens with negligible probability. Normally, we would use VERIFY_CHECK as opposed to returning an error
+ * since returning an error here would result in an untestable branch in the code. But in this case, we return
+ * an error to ensure strict compliance with BIP0352.
+ */
+ secp256k1_scalar_set_b32(input_hash_scalar, input_hash, &overflow);
+ return (!secp256k1_scalar_is_zero(input_hash_scalar)) & (!overflow);
+}
+
+static void secp256k1_silentpayments_create_shared_secret(unsigned char *shared_secret33, const secp256k1_ge *public_component, const secp256k1_scalar *secret_component) {
+ secp256k1_gej ss_j;
+ secp256k1_ge ss;
+
+ VERIFY_CHECK(!secp256k1_ge_is_infinity(public_component));
+ VERIFY_CHECK(!secp256k1_scalar_is_zero(secret_component));
+
+ secp256k1_ecmult_const(&ss_j, public_component, secret_component);
+ secp256k1_ge_set_gej(&ss, &ss_j);
+
+ /* serialize shared secret in constant-time */
+ secp256k1_fe_normalize(&ss.x);
+ secp256k1_fe_normalize(&ss.y);
+ shared_secret33[0] = SECP256K1_TAG_PUBKEY_EVEN | secp256k1_fe_is_odd(&ss.y);
+ secp256k1_fe_get_b32(&shared_secret33[1], &ss.x);
+
+ /* Leaking these values would break indistinguishability of the transaction, so clear them. */
+ secp256k1_ge_clear(&ss);
+ secp256k1_gej_clear(&ss_j);
+}
+
+/** Set hash state to the BIP340 tagged hash midstate for "BIP0352/SharedSecret". */
+static void secp256k1_silentpayments_sha256_init_sharedsecret(secp256k1_sha256* hash) {
+ static const uint32_t midstate[8] = {
+ 0x88831537ul, 0x5127079bul, 0x69c2137bul, 0xab0303e6ul,
+ 0x98fa21faul, 0x4a888523ul, 0xbd99daabul, 0xf25e5e0aul
+ };
+ secp256k1_sha256_initialize_midstate(hash, 64, midstate);
+}
+
+static int secp256k1_silentpayments_create_output_tweak(const secp256k1_context *ctx, secp256k1_scalar *t_k_scalar, const unsigned char *shared_secret33, uint32_t k) {
+ const secp256k1_hash_ctx *hash_ctx = secp256k1_get_hash_context(ctx);
+ secp256k1_sha256 hash;
+ unsigned char hash_ser[32];
+ unsigned char k_serialized[4];
+ int overflow;
+
+ /* Compute hash(shared_secret || ser_32(k)) [sha256 with tag "BIP0352/SharedSecret"] */
+ secp256k1_silentpayments_sha256_init_sharedsecret(&hash);
+ secp256k1_sha256_write(hash_ctx, &hash, shared_secret33, 33);
+ secp256k1_write_be32(k_serialized, k);
+ secp256k1_sha256_write(hash_ctx, &hash, k_serialized, sizeof(k_serialized));
+ secp256k1_sha256_finalize(hash_ctx, &hash, hash_ser);
+
+ /* The only thing that the attacker can do with the hashed secret is derive the final pubkeys.
+ * On the side of the sender, we assume that will reveal those on the blockchain anyway.
+ * On the side of the scanner, we assume that the caller wants to branch when a payment is found. */
+ secp256k1_declassify(ctx, hash_ser, sizeof(hash_ser));
+
+ /* Convert output tweak t_k to a scalar.
+ *
+ * This can only fail if the output of the hash function is zero or greater than or equal to the curve order, which
+ * happens with negligible probability. Normally, we would use VERIFY_CHECK as opposed to returning an error
+ * since returning an error here would result in an untestable branch in the code. But in this case, we return
+ * an error to ensure strict compliance with BIP0352.
+ */
+ secp256k1_scalar_set_b32(t_k_scalar, hash_ser, &overflow);
+ /* Leaking this value would break indistinguishability of the transaction, so clear it. */
+ secp256k1_memclear_explicit(hash_ser, sizeof(hash_ser));
+ secp256k1_sha256_clear(&hash);
+ 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;
+ secp256k1_scalar t_k_scalar;
+ /* 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_secret33, k)) {
+ secp256k1_scalar_clear(&t_k_scalar);
+ 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;
+ }
+ 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;
+}
+
+int secp256k1_silentpayments_sender_create_outputs(
+ const secp256k1_context *ctx,
+ secp256k1_xonly_pubkey **generated_outputs,
+ const secp256k1_silentpayments_recipient **recipients,
+ size_t n_recipients,
+ const unsigned char *outpoint_smallest36,
+ const secp256k1_keypair * const *keypairs,
+ size_t n_keypairs,
+ const unsigned char * const *seckeys,
+ size_t n_seckeys
+) {
+ size_t i;
+ uint32_t k;
+ secp256k1_scalar seckey_sum_scalar, addend, input_hash_scalar;
+ secp256k1_ge prevouts_pubkey_sum_ge;
+ unsigned char shared_secret[33];
+ secp256k1_pubkey current_scan_pubkey;
+ int ret, sum_is_zero;
+
+ /* Sanity check inputs. */
+ VERIFY_CHECK(ctx != NULL);
+ ARG_CHECK(secp256k1_ecmult_gen_context_is_built(&ctx->ecmult_gen_ctx));
+ ARG_CHECK(generated_outputs != NULL);
+ ARG_CHECK(recipients != NULL);
+ ARG_CHECK(n_recipients > 0);
+ ARG_CHECK(outpoint_smallest36 != NULL);
+ ARG_CHECK((seckeys != NULL) || (keypairs != NULL));
+ if (keypairs != NULL) {
+ ARG_CHECK(n_keypairs > 0);
+ for (i = 0; i < n_keypairs; i++) {
+ ARG_CHECK(keypairs[i] != NULL);
+ }
+ } else {
+ ARG_CHECK(n_keypairs == 0);
+ }
+ if (seckeys != NULL) {
+ ARG_CHECK(n_seckeys > 0);
+ for (i = 0; i < n_seckeys; i++) {
+ ARG_CHECK(seckeys[i] != NULL);
+ }
+ } else {
+ ARG_CHECK(n_seckeys == 0);
+ }
+ for (i = 0; i < n_recipients; i++) {
+ ARG_CHECK(generated_outputs[i] != NULL);
+ ARG_CHECK(recipients[i] != NULL);
+ ARG_CHECK(recipients[i]->index == i);
+ }
+
+ seckey_sum_scalar = secp256k1_scalar_zero;
+ for (i = 0; i < n_seckeys; i++) {
+ ret = secp256k1_scalar_set_b32_seckey(&addend, seckeys[i]);
+ secp256k1_declassify(ctx, &ret, sizeof(ret));
+ if (!ret) {
+ secp256k1_scalar_clear(&addend);
+ secp256k1_scalar_clear(&seckey_sum_scalar);
+ return 0;
+ }
+ secp256k1_scalar_add(&seckey_sum_scalar, &seckey_sum_scalar, &addend);
+ }
+ /* Secret keys used for taproot outputs have to be negated if they result in an odd point. This is to ensure
+ * the sender and recipient can arrive at the same shared secret when using x-only public keys. */
+ for (i = 0; i < n_keypairs; i++) {
+ secp256k1_ge addend_point;
+ ret = secp256k1_keypair_load(ctx, &addend, &addend_point, keypairs[i]);
+ secp256k1_declassify(ctx, &ret, sizeof(ret));
+ if (!ret) {
+ secp256k1_scalar_clear(&addend);
+ secp256k1_scalar_clear(&seckey_sum_scalar);
+ return 0;
+ }
+ if (secp256k1_fe_is_odd(&addend_point.y)) {
+ secp256k1_scalar_negate(&addend, &addend);
+ }
+ secp256k1_scalar_add(&seckey_sum_scalar, &seckey_sum_scalar, &addend);
+ }
+ /* If there are any failures in loading/summing up the secret keys, fail early. */
+ sum_is_zero = secp256k1_scalar_is_zero(&seckey_sum_scalar);
+ secp256k1_declassify(ctx, &sum_is_zero, sizeof(sum_is_zero));
+ secp256k1_scalar_clear(&addend);
+ if (sum_is_zero) {
+ secp256k1_scalar_clear(&seckey_sum_scalar);
+ return 0;
+ }
+ secp256k1_ecmult_gen_ge(&ctx->ecmult_gen_ctx, &prevouts_pubkey_sum_ge, &seckey_sum_scalar);
+ /* We declassify the pubkey sum because serializing a group element (done in the
+ * `_calculate_input_hash_scalar` call following) is not a constant-time operation.
+ */
+ secp256k1_declassify(ctx, &prevouts_pubkey_sum_ge, sizeof(prevouts_pubkey_sum_ge));
+
+ /* Calculate the input_hash and convert it to a scalar so that it can be multiplied with the summed up private keys, i.e., a_sum = a_sum * input_hash.
+ * By multiplying the scalars together first, we can save an elliptic curve multiplication.
+ *
+ * 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)) {
+ secp256k1_scalar_clear(&seckey_sum_scalar);
+ return 0;
+ }
+ secp256k1_scalar_mul(&seckey_sum_scalar, &seckey_sum_scalar, &input_hash_scalar);
+ /* _recipient_sort sorts the array of recipients in place by their scan public keys (lexicographically).
+ * This ensures that all recipients with the same scan public key are grouped together, as specified in BIP0352.
+ *
+ * More specifically, this ensures `k` is incremented from 0 to the number of requested outputs for each recipient group,
+ * where a recipient group is all addresses with the same scan public key.
+ */
+ secp256k1_silentpayments_recipient_sort(ctx, recipients, n_recipients);
+ current_scan_pubkey = recipients[0]->scan_pubkey;
+ k = 0; /* This is a dead store but clang will emit a false positive warning if we omit it. */
+ for (i = 0; i < n_recipients; i++) {
+ if ((i == 0) || (secp256k1_ec_pubkey_cmp(ctx, ¤t_scan_pubkey, &recipients[i]->scan_pubkey) != 0)) {
+ /* If we are on a different scan pubkey, its time to recreate the shared secret and reset k to 0.
+ * It's very unlikely the scan public key is invalid by this point, since this means the caller would
+ * have created the _silentpayments_recipient object incorrectly, but just to be sure we still check that
+ * the public key is valid.
+ */
+ secp256k1_ge pk;
+ if (!secp256k1_pubkey_load(ctx, &pk, &recipients[i]->scan_pubkey)) {
+ secp256k1_scalar_clear(&seckey_sum_scalar);
+ /* Leaking this value would break indistinguishability of the transaction, so clear it. */
+ secp256k1_memclear_explicit(&shared_secret, sizeof(shared_secret));
+ 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,
+ * secret key sum, and scan public key) have all been verified at this point. */
+ secp256k1_silentpayments_create_shared_secret(shared_secret, &pk, &seckey_sum_scalar);
+ k = 0;
+ }
+ if (!secp256k1_silentpayments_create_output_pubkey(ctx, generated_outputs[recipients[i]->index], shared_secret, &recipients[i]->spend_pubkey, k)) {
+ secp256k1_scalar_clear(&seckey_sum_scalar);
+ secp256k1_memclear_explicit(&shared_secret, sizeof(shared_secret));
+ return 0;
+ }
+ current_scan_pubkey = recipients[i]->scan_pubkey;
+ k++;
+ /* TODO: limit k, in order to avoid quadratic scaling issue for scanning */
+ }
+ secp256k1_scalar_clear(&seckey_sum_scalar);
+ secp256k1_memclear_explicit(&shared_secret, sizeof(shared_secret));
+ return 1;
+}
/* TODO: implement functions for receiver side. */
diff --git a/src/modules/silentpayments/tests_impl.h b/src/modules/silentpayments/tests_impl.h
new file mode 100644
index 0000000..b770335
--- /dev/null
+++ b/src/modules/silentpayments/tests_impl.h
@@ -0,0 +1,295 @@
+/***********************************************************************
+ * Distributed under the MIT software license, see the accompanying *
+ * file COPYING or https://www.opensource.org/licenses/mit-license.php.*
+ ***********************************************************************/
+
+#ifndef SECP256K1_MODULE_SILENTPAYMENTS_TESTS_H
+#define SECP256K1_MODULE_SILENTPAYMENTS_TESTS_H
+
+#include "../../../include/secp256k1_silentpayments.h"
+#include "../../unit_test.h"
+
+/** Constants
+ *
+ * Malformed Seckey: a seckey that is all zeros
+ * Addresses: scan and spend public keys for Bob and Carol
+ * Outputs: generated outputs from Alice's secret key and Bob/Carol's
+ * scan public keys
+ * Smallest Outpoint: smallest outpoint lexicographically from the transaction
+ * Seckey: secret key for Alice
+ *
+ * The values themselves are not important.
+ */
+static unsigned char MALFORMED_SECKEY[32] = { 0x00 };
+static unsigned char BOB_ADDRESS[2][33] = {
+ {
+ 0x02, 0x15, 0x40, 0xae, 0xa8, 0x97, 0x54, 0x7a,
+ 0xd4, 0x39, 0xb4, 0xe0, 0xf6, 0x09, 0xe5, 0xf0,
+ 0xfa, 0x63, 0xde, 0x89, 0xab, 0x11, 0xed, 0xe3,
+ 0x1e, 0x8c, 0xde, 0x4b, 0xe2, 0x19, 0x42, 0x5f,
+ 0x23
+ },
+ {
+ 0x02, 0x3e, 0xff, 0xf8, 0x18, 0x51, 0x65, 0xea,
+ 0x63, 0xa9, 0x92, 0xb3, 0x9f, 0x31, 0xd8, 0xfd,
+ 0x8e, 0x0e, 0x64, 0xae, 0xf9, 0xd3, 0x88, 0x07,
+ 0x34, 0x97, 0x37, 0x14, 0xa5, 0x3d, 0x83, 0x11,
+ 0x8d
+ }
+};
+static unsigned char CAROL_ADDRESS[2][33] = {
+ {
+ 0x03, 0xbb, 0xc6, 0x3f, 0x12, 0x74, 0x5d, 0x3b,
+ 0x9e, 0x9d, 0x24, 0xc6, 0xcd, 0x7a, 0x1e, 0xfe,
+ 0xba, 0xd0, 0xa7, 0xf4, 0x69, 0x23, 0x2f, 0xbe,
+ 0xcf, 0x31, 0xfb, 0xa7, 0xb4, 0xf7, 0xdd, 0xed,
+ 0xa8
+ },
+ {
+ 0x03, 0x81, 0xeb, 0x9a, 0x9a, 0x9e, 0xc7, 0x39,
+ 0xd5, 0x27, 0xc1, 0x63, 0x1b, 0x31, 0xb4, 0x21,
+ 0x56, 0x6f, 0x5c, 0x2a, 0x47, 0xb4, 0xab, 0x5b,
+ 0x1f, 0x6a, 0x68, 0x6d, 0xfb, 0x68, 0xea, 0xb7,
+ 0x16
+ }
+};
+static unsigned char BOB_OUTPUT[32] = {
+ 0x46, 0x0d, 0x68, 0x08, 0x65, 0x64, 0x45, 0xee,
+ 0x4d, 0x4e, 0xc0, 0x8e, 0xba, 0x8a, 0x66, 0xea,
+ 0x66, 0x8e, 0x4e, 0x12, 0x98, 0x9a, 0x0e, 0x60,
+ 0x4b, 0x5c, 0x36, 0x0e, 0x43, 0xf5, 0x5a, 0xfa
+};
+static unsigned char CAROL_OUTPUT_ONE[32] = {
+ 0x4b, 0x81, 0x34, 0x5d, 0x53, 0x89, 0xba, 0xa3,
+ 0xd8, 0x93, 0xe2, 0xfb, 0xe7, 0x08, 0xdd, 0x6d,
+ 0x82, 0xdc, 0xd8, 0x49, 0xab, 0x03, 0xc1, 0xdb,
+ 0x68, 0xbe, 0xc7, 0xe9, 0x2a, 0x45, 0xfa, 0xc5
+};
+static unsigned char CAROL_OUTPUT_TWO[32] = {
+ 0xb7, 0xf3, 0xc6, 0x79, 0x30, 0x4a, 0xef, 0x8c,
+ 0xc0, 0xc7, 0x61, 0xf1, 0x00, 0x99, 0xdd, 0x7b,
+ 0x20, 0x65, 0x20, 0xd7, 0x11, 0x6f, 0xb7, 0x91,
+ 0xee, 0x74, 0x54, 0xa2, 0xfc, 0x22, 0x79, 0xf4
+};
+static unsigned char SMALLEST_OUTPOINT[36] = {
+ 0x16, 0x9e, 0x1e, 0x83, 0xe9, 0x30, 0x85, 0x33, 0x91,
+ 0xbc, 0x6f, 0x35, 0xf6, 0x05, 0xc6, 0x75, 0x4c, 0xfe,
+ 0xad, 0x57, 0xcf, 0x83, 0x87, 0x63, 0x9d, 0x3b, 0x40,
+ 0x96, 0xc5, 0x4f, 0x18, 0xf4, 0x00, 0x00, 0x00, 0x00
+};
+static unsigned char ALICE_SECKEY[32] = {
+ 0xea, 0xdc, 0x78, 0x16, 0x5f, 0xf1, 0xf8, 0xea,
+ 0x94, 0xad, 0x7c, 0xfd, 0xc5, 0x49, 0x90, 0x73,
+ 0x8a, 0x4c, 0x53, 0xf6, 0xe0, 0x50, 0x7b, 0x42,
+ 0x15, 0x42, 0x01, 0xb8, 0xe5, 0xdf, 0xf3, 0xb1
+};
+
+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];
+ const secp256k1_silentpayments_recipient *recipient_ptrs[3];
+ secp256k1_xonly_pubkey generated_outputs[3];
+ secp256k1_xonly_pubkey *generated_output_ptrs[3];
+ unsigned char xonly_ser[32];
+ size_t i;
+ int ret;
+
+ seckey_ptrs[0] = ALICE_SECKEY;
+ for (i = 0; i < 3; i++) {
+ CHECK(secp256k1_ec_pubkey_parse(CTX, &recipients[i].scan_pubkey, (*sp_addresses[i])[0], 33));
+ CHECK(secp256k1_ec_pubkey_parse(CTX, &recipients[i].spend_pubkey,(*sp_addresses[i])[1], 33));
+ recipients[i].index = i;
+ recipient_ptrs[i] = &recipients[i];
+ generated_output_ptrs[i] = &generated_outputs[i];
+ }
+ ret = secp256k1_silentpayments_sender_create_outputs(CTX,
+ generated_output_ptrs,
+ recipient_ptrs, 3,
+ SMALLEST_OUTPOINT,
+ NULL, 0,
+ seckey_ptrs, 1
+ );
+ CHECK(ret == 1);
+ for (i = 0; i < 3; i++) {
+ secp256k1_xonly_pubkey_serialize(CTX, xonly_ser, &generated_outputs[i]);
+ CHECK(secp256k1_memcmp_var(xonly_ser, (*sp_outputs[i]), 32) == 0);
+ }
+}
+
+static void test_recipient_sort(void) {
+ unsigned char (*sp_addresses[3])[2][33];
+ unsigned char (*sp_outputs[3])[32];
+
+ /* With a fixed set of addresses and a fixed set of inputs,
+ * test that we always get the same outputs, regardless of the ordering
+ * of the recipients
+ */
+ sp_addresses[0] = &CAROL_ADDRESS;
+ sp_addresses[1] = &BOB_ADDRESS;
+ sp_addresses[2] = &CAROL_ADDRESS;
+
+ sp_outputs[0] = &CAROL_OUTPUT_ONE;
+ sp_outputs[1] = &BOB_OUTPUT;
+ sp_outputs[2] = &CAROL_OUTPUT_TWO;
+ test_recipient_sort_helper(sp_addresses, sp_outputs);
+
+ sp_addresses[0] = &CAROL_ADDRESS;
+ sp_addresses[1] = &CAROL_ADDRESS;
+ sp_addresses[2] = &BOB_ADDRESS;
+
+ sp_outputs[0] = &CAROL_OUTPUT_ONE;
+ sp_outputs[1] = &CAROL_OUTPUT_TWO;
+ sp_outputs[2] = &BOB_OUTPUT;
+ test_recipient_sort_helper(sp_addresses, sp_outputs);
+
+ sp_addresses[0] = &BOB_ADDRESS;
+ sp_addresses[1] = &CAROL_ADDRESS;
+ sp_addresses[2] = &CAROL_ADDRESS;
+
+ sp_outputs[0] = &BOB_OUTPUT;
+ sp_outputs[1] = &CAROL_OUTPUT_ONE;
+ sp_outputs[2] = &CAROL_OUTPUT_TWO;
+ test_recipient_sort_helper(sp_addresses, sp_outputs);
+}
+
+static void test_send_api(void) {
+ unsigned char (*sp_addresses[2])[2][33];
+ unsigned char const *p[1];
+ secp256k1_keypair const *t[1];
+ secp256k1_silentpayments_recipient r[2];
+ const secp256k1_silentpayments_recipient *rp[2];
+ secp256k1_xonly_pubkey o[2];
+ secp256k1_xonly_pubkey *op[2];
+ secp256k1_keypair taproot;
+ size_t i;
+
+ /* Set up Bob and Carol as the recipients */
+ sp_addresses[0] = &BOB_ADDRESS;
+ sp_addresses[1] = &CAROL_ADDRESS;
+ for (i = 0; i < 2; i++) {
+ CHECK(secp256k1_ec_pubkey_parse(CTX, &r[i].scan_pubkey, (*sp_addresses[i])[0], 33));
+ CHECK(secp256k1_ec_pubkey_parse(CTX, &r[i].spend_pubkey,(*sp_addresses[i])[1], 33));
+ /* Set the index value incorrectly */
+ r[i].index = 0;
+ rp[i] = &r[i];
+ op[i] = &o[i];
+ }
+ /* Set up a taproot key and a plain key for Alice */
+ CHECK(secp256k1_keypair_create(CTX, &taproot, ALICE_SECKEY));
+ t[0] = &taproot;
+ p[0] = ALICE_SECKEY;
+
+ /* Fails if the index is set incorrectly */
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_sender_create_outputs(CTX, op, rp, 2, SMALLEST_OUTPOINT, NULL, 0, p, 1));
+
+ /* Set the index correctly for the next tests */
+ for (i = 0; i < 2; i++) {
+ r[i].index = i;
+ }
+ CHECK(secp256k1_silentpayments_sender_create_outputs(CTX, op, rp, 2, SMALLEST_OUTPOINT, NULL, 0, p, 1));
+
+ /* Check that NULL in "array of pointers" arguments is not allowed */
+ for (i = 0; i < 2; i++) {
+ secp256k1_xonly_pubkey *original_ptr_xpk = op[i];
+ const secp256k1_silentpayments_recipient *original_ptr_rec = rp[i];
+
+ op[i] = NULL;
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_sender_create_outputs(CTX, op, rp, 2, SMALLEST_OUTPOINT, NULL, 0, p, 1));
+ op[i] = original_ptr_xpk;
+
+ rp[i] = NULL;
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_sender_create_outputs(CTX, op, rp, 2, SMALLEST_OUTPOINT, NULL, 0, p, 1));
+ rp[i] = original_ptr_rec;
+ }
+ {
+ secp256k1_keypair const *original_ptr = t[0];
+ t[0] = NULL;
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_sender_create_outputs(CTX, op, rp, 2, SMALLEST_OUTPOINT, t, 1, NULL, 0));
+ t[0] = original_ptr;
+ }
+ {
+ unsigned char const *original_ptr = p[0];
+ p[0] = NULL;
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_sender_create_outputs(CTX, op, rp, 2, SMALLEST_OUTPOINT, NULL, 0, p, 1));
+ p[0] = original_ptr;
+ }
+
+ /* Check that null arguments are handled */
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_sender_create_outputs(CTX, NULL, rp, 2, SMALLEST_OUTPOINT, t, 1, p, 1));
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_sender_create_outputs(CTX, op, NULL, 2, SMALLEST_OUTPOINT, t, 1, p, 1));
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_sender_create_outputs(CTX, op, rp, 2, NULL, t, 1, p, 1));
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_sender_create_outputs(CTX, op, rp, 2, SMALLEST_OUTPOINT, NULL, 1, p, 1));
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_sender_create_outputs(CTX, op, rp, 2, SMALLEST_OUTPOINT, t, 1, NULL, 1));
+
+ /* Check correct context is used */
+ CHECK_ILLEGAL(STATIC_CTX, secp256k1_silentpayments_sender_create_outputs(STATIC_CTX, op, rp, 2, SMALLEST_OUTPOINT, NULL, 0, p, 1));
+
+ /* Check that array arguments are verified */
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_sender_create_outputs(CTX, op, rp, 2, SMALLEST_OUTPOINT, NULL, 0, NULL, 0));
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_sender_create_outputs(CTX, op, rp, 0, SMALLEST_OUTPOINT, NULL, 0, p, 1));
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_sender_create_outputs(CTX, op, rp, 2, SMALLEST_OUTPOINT, t, 0, p, 1));
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_sender_create_outputs(CTX, op, rp, 2, SMALLEST_OUTPOINT, t, 1, p, 0));
+
+ /* Create malformed keys for Alice by using a key that will overflow */
+ CHECK(secp256k1_ec_seckey_verify(CTX, secp256k1_group_order_bytes) == 0);
+ p[0] = secp256k1_group_order_bytes;
+ CHECK(secp256k1_keypair_create(CTX, &taproot, ALICE_SECKEY));
+ /* Malleate the keypair object so that the secret key is all zeros. We need to keep
+ * public key as is since it is loaded first and would hit an ARG_CHECK if invalid.
+ */
+ memset(&taproot.data[0], 0, 32);
+ /* Check that an invalid plain secret key is caught */
+ CHECK(secp256k1_silentpayments_sender_create_outputs(CTX, op, rp, 2, SMALLEST_OUTPOINT, NULL, 0, p, 1) == 0);
+ /* Check that an invalid keypair is caught */
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_sender_create_outputs(CTX, op, rp, 2, SMALLEST_OUTPOINT, t, 1, NULL, 0));
+ /* Create malformed keys for Alice by using a zero'd seckey */
+ p[0] = MALFORMED_SECKEY;
+ CHECK(secp256k1_silentpayments_sender_create_outputs(CTX, op, rp, 2, SMALLEST_OUTPOINT, NULL, 0, p, 1) == 0);
+ p[0] = ALICE_SECKEY;
+ /* Create malformed recipients by setting all of the public key bytes to zero.
+ * Realistically, this would never happen since a bad public key would get caught when
+ * trying to parse the public key with _ec_pubkey_parse
+ */
+ {
+ secp256k1_pubkey tmp = r[1].spend_pubkey;
+ memset(&r[1].spend_pubkey, 0, sizeof(r[1].spend_pubkey));
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_sender_create_outputs(CTX, op, rp, 2, SMALLEST_OUTPOINT, NULL, 0, p, 1));
+ r[1].spend_pubkey = tmp;
+ }
+ {
+ secp256k1_pubkey tmp = r[1].scan_pubkey;
+ int32_t ecount = 0;
+
+ memset(&r[1].scan_pubkey, 0, sizeof(r[1].scan_pubkey));
+ secp256k1_context_set_illegal_callback(CTX, counting_callback_fn, &ecount);
+ CHECK(secp256k1_silentpayments_sender_create_outputs(CTX, op, rp, 2, SMALLEST_OUTPOINT, NULL, 0, p, 1) == 0);
+ CHECK(ecount == 2);
+ secp256k1_context_set_illegal_callback(CTX, NULL, NULL);
+ r[1].scan_pubkey = tmp;
+ }
+ {
+ unsigned char malformed_spend_key[32] = {
+ 0x83, 0xe1, 0x79, 0xdf, 0x51, 0xbb, 0xc9, 0x6f,
+ 0xfb, 0x59, 0xb6, 0x2e, 0x57, 0xcf, 0x4e, 0x54,
+ 0x71, 0x79, 0x04, 0x9c, 0x01, 0x47, 0x00, 0xfe,
+ 0x52, 0xef, 0x5f, 0x53, 0x76, 0x39, 0xec, 0xe0
+ };
+ secp256k1_pubkey neg_spend_pubkey;
+ CHECK(secp256k1_ec_pubkey_create(CTX, &neg_spend_pubkey, malformed_spend_key));
+ CHECK(secp256k1_ec_pubkey_negate(CTX, &neg_spend_pubkey));
+ r[0].spend_pubkey = neg_spend_pubkey;
+ for (i = 0; i < 2; i++) {
+ r[i].index = i;
+ rp[i] = &r[i];
+ }
+ CHECK(secp256k1_silentpayments_sender_create_outputs(CTX, op, rp, 2, SMALLEST_OUTPOINT, NULL, 0, p, 1) == 0);
+ }
+}
+
+/* --- Test registry --- */
+static const struct tf_test_entry tests_silentpayments[] = {
+ CASE1(test_recipient_sort),
+ CASE1(test_send_api),
+};
+
+#endif
diff --git a/src/tests.c b/src/tests.c
index e821738..c0ab8aa 100644
--- a/src/tests.c
+++ b/src/tests.c
@@ -7727,6 +7727,10 @@ static void run_ecdsa_wycheproof(void) {
# include "modules/ellswift/tests_impl.h"
#endif
+#ifdef ENABLE_MODULE_SILENTPAYMENTS
+# include "modules/silentpayments/tests_impl.h"
+#endif
+
static void run_secp256k1_memczero_test(void) {
unsigned char buf1[6] = {1, 2, 3, 4, 5, 6};
unsigned char buf2[sizeof(buf1)];
@@ -8067,6 +8071,9 @@ static const struct tf_test_module registry_modules[] = {
#endif
#ifdef ENABLE_MODULE_ELLSWIFT
MAKE_TEST_MODULE(ellswift),
+#endif
+#ifdef ENABLE_MODULE_SILENTPAYMENTS
+ MAKE_TEST_MODULE(silentpayments),
#endif
MAKE_TEST_MODULE(utils),
};
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.