silentpayments: recipient label support
What changed, and why it matters
This commit adds new public API functions to the silentpayments module of libsecp256k1 to support recipient-side labels for BIP352 silent payments. It is a feature addition, not a security fix or vulnerability patch. There is no evidence in the commit or supplied references of any security defect, exploit, or incident.
No security action required. Treat as normal feature review for the silentpayments module.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit introduces four new API functions: secp256k1_silentpayments_recipient_label_parse, secp256k1_silentpayments_recipient_label_serialize, secp256k1_silentpayments_recipient_label_create, and secp256k1_silentpayments_recipient_create_labeled_spend_pubkey. These implement BIP352 label derivation (label_tweak = tagged_hash(‘BIP0352/Label’, scan_key || m); label = label_tweak * G) and labeled spend public key construction (unlabeled_spend_pubkey + label). The implementation includes input validation, tagged hash midstate initialization, serialization/deserialization with a magic prefix, and tests covering happy paths, round-trips, null pointer handling, invalid scan keys, and invalid/summing-to-zero public keys. No security-relevant bug is present in the diff.
Changed components
include/secp256k1_silentpayments.hsrc/modules/silentpayments/main_impl.hsrc/modules/silentpayments/tests_impl.hInspect captured patch +302 / −1
diff --git a/include/secp256k1_silentpayments.h b/include/secp256k1_silentpayments.h
index 5eb9825..c9a0d72 100644
--- a/include/secp256k1_silentpayments.h
+++ b/include/secp256k1_silentpayments.h
@@ -126,6 +126,106 @@ SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_silentpayments_sender_c
size_t n_seckeys
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(5);
+/** Opaque data structure that holds a Silent Payments label.
+ *
+ * Guaranteed to be 68 bytes in size. Serialized and parsed with
+ * `secp256k1_silentpayments_recipient_label_serialize` and
+ * `secp256k1_silentpayments_recipient_label_parse`.
+ */
+typedef struct secp256k1_silentpayments_label {
+ unsigned char data[68];
+} secp256k1_silentpayments_label;
+
+/** Parse a Silent Payments label.
+ *
+ * Returns: 1 when the label could be parsed, 0 otherwise.
+ * Args: ctx: pointer to a context object
+ * Out: label: pointer to a label object
+ * In: in33: pointer to the 33-byte label to be parsed
+ */
+SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_silentpayments_recipient_label_parse(
+ const secp256k1_context *ctx,
+ secp256k1_silentpayments_label *label,
+ const unsigned char *in33
+) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3);
+
+/** Serialize a Silent Payments label
+ *
+ * Returns: 1 always
+ * Args: ctx: pointer to a context object
+ * Out: out33: pointer to a 33-byte array to store the serialized label
+ * In: label: pointer to the label
+ */
+SECP256K1_API int secp256k1_silentpayments_recipient_label_serialize(
+ const secp256k1_context *ctx,
+ unsigned char *out33,
+ const secp256k1_silentpayments_label *label
+) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3);
+
+/** Create Silent Payments label tweak and label.
+ *
+ * Given a recipient's 32 byte scan key and a label integer m, calculate the
+ * corresponding label tweak and label:
+ *
+ * label_tweak = hash(scan_key || m)
+ * label = label_tweak * G
+ *
+ * Returns: 1 if label tweak and label creation was successful.
+ * 0 if scan_key32 is invalid or the hash output label_tweak32 is
+ * not a valid scalar (negligible probability per hash evaluation).
+ *
+ * WARNING: Creating a large number of labels may significantly degrade
+ * scanning performance in certain Silent Payments wallet implementations,
+ * such as light clients. The scanning function provided in this module,
+ * which is designed for full nodes, performs consistently even with hundreds
+ * of thousands of labels. Other implementations may not share this property
+ * or may be unable to use it due to lacking full transaction data.
+ *
+ * To maximize wallet interoperability, it is recommended to create only
+ * the change label (m = 0) and avoid distributing labeled addresses.
+ *
+ * Args: ctx: pointer to a context object
+ * (not secp256k1_context_static)
+ * Out: label: pointer to the resulting label
+ * label_tweak32: pointer to the 32 byte label tweak
+ * In: scan_key32: pointer to the recipient's 32 byte scan key
+ * m: integer for the m-th label (0 is used for change outputs)
+ */
+SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_silentpayments_recipient_label_create(
+ const secp256k1_context *ctx,
+ secp256k1_silentpayments_label *label,
+ unsigned char *label_tweak32,
+ const unsigned char *scan_key32,
+ uint32_t m
+) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4);
+
+/** Create Silent Payments labeled spend public key.
+ *
+ * Given a recipient's spend public key and a label, calculate the
+ * corresponding labeled spend public key:
+ *
+ * labeled_spend_pubkey = unlabeled_spend_pubkey + label
+ *
+ * The result is used by the recipient to create a Silent Payments address,
+ * consisting of the serialized and concatenated scan public key and
+ * (labeled) spend public key.
+ *
+ * Returns: 1 if labeled spend public key creation was successful.
+ * 0 if spend pubkey and label sum to zero (negligible probability for
+ * labels created according to BIP352).
+ *
+ * Args: ctx: pointer to a context object
+ * Out: labeled_spend_pubkey: pointer to the resulting labeled spend public key
+ * In: unlabeled_spend_pubkey: pointer to the recipient's unlabeled spend public key
+ * label: pointer to the recipient's label
+ */
+SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_silentpayments_recipient_create_labeled_spend_pubkey(
+ const secp256k1_context *ctx,
+ secp256k1_pubkey *labeled_spend_pubkey,
+ const secp256k1_pubkey *unlabeled_spend_pubkey,
+ const secp256k1_silentpayments_label *label
+) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4);
+
#ifdef __cplusplus
}
#endif
diff --git a/src/modules/silentpayments/main_impl.h b/src/modules/silentpayments/main_impl.h
index 733812a..38acd15 100644
--- a/src/modules/silentpayments/main_impl.h
+++ b/src/modules/silentpayments/main_impl.h
@@ -326,6 +326,129 @@ int secp256k1_silentpayments_sender_create_outputs(
return 1;
}
-/* TODO: implement functions for receiver side. */
+/** Set hash state to the BIP340 tagged hash midstate for "BIP0352/Label". */
+static void secp256k1_silentpayments_sha256_init_label(secp256k1_sha256* hash) {
+ static const uint32_t midstate[8] = {
+ 0x26b95d63ul, 0x8bf1b740ul, 0x10a5986ful, 0x06a387a5ul,
+ 0x2d1c1c30ul, 0xd035951aul, 0x2d7f0f96ul, 0x29e3e0dbul
+ };
+ secp256k1_sha256_initialize_midstate(hash, 64, midstate);
+}
+
+static const unsigned char secp256k1_silentpayments_label_magic[4] = { 0x27, 0x9d, 0x44, 0xba };
+
+/* Saves a group element into a label. Requires that the provided group element is not infinity. */
+static void secp256k1_silentpayments_label_save(secp256k1_silentpayments_label* label, const secp256k1_ge* ge) {
+ memcpy(&label->data[0], secp256k1_silentpayments_label_magic, 4);
+ secp256k1_ge_to_bytes(label->data + 4, ge);
+}
+
+/* Loads a group element from a label. Returns 1 unless the label wasn't properly initialized. */
+static int secp256k1_silentpayments_label_load(const secp256k1_context* ctx, secp256k1_ge* ge, const secp256k1_silentpayments_label* label) {
+ ARG_CHECK(secp256k1_memcmp_var(&label->data[0], secp256k1_silentpayments_label_magic, 4) == 0);
+ secp256k1_ge_from_bytes(ge, label->data + 4);
+ return 1;
+}
+
+int secp256k1_silentpayments_recipient_label_parse(const secp256k1_context* ctx, secp256k1_silentpayments_label* label, const unsigned char *in33) {
+ secp256k1_ge ge;
+
+ VERIFY_CHECK(ctx != NULL);
+ ARG_CHECK(label != NULL);
+ memset(label, 0, sizeof(*label));
+ ARG_CHECK(in33 != NULL);
+
+ if (!secp256k1_eckey_pubkey_parse(&ge, in33, 33)) {
+ return 0;
+ }
+
+ secp256k1_silentpayments_label_save(label, &ge);
+ return 1;
+}
+
+int secp256k1_silentpayments_recipient_label_serialize(const secp256k1_context* ctx, unsigned char *out33, const secp256k1_silentpayments_label* label) {
+ secp256k1_ge ge;
+
+ VERIFY_CHECK(ctx != NULL);
+ ARG_CHECK(out33 != NULL);
+ memset(out33, 0, 33);
+ ARG_CHECK(label != NULL);
+
+ if (!secp256k1_silentpayments_label_load(ctx, &ge, label)) {
+ return 0;
+ }
+ secp256k1_eckey_pubkey_serialize33(&ge, out33);
+ return 1;
+}
+
+int secp256k1_silentpayments_recipient_label_create(const secp256k1_context *ctx, secp256k1_silentpayments_label *label, unsigned char *label_tweak32, const unsigned char *scan_key32, uint32_t m) {
+ secp256k1_sha256 hash;
+ unsigned char m_serialized[4];
+ secp256k1_ge label_ge;
+ secp256k1_scalar label_tweak_scalar;
+ int ret;
+
+ /* Sanity check inputs. */
+ VERIFY_CHECK(ctx != NULL);
+ ARG_CHECK(label != NULL);
+ memset(label, 0, sizeof(*label));
+ ARG_CHECK(secp256k1_ecmult_gen_context_is_built(&ctx->ecmult_gen_ctx));
+ ARG_CHECK(label_tweak32 != NULL);
+ ARG_CHECK(scan_key32 != NULL);
+
+ /* ensure that the passed scan key is valid, in order to avoid creating unspendable labels */
+ ret = secp256k1_ec_seckey_verify(ctx, scan_key32);
+
+ /* Compute hash(ser_256(b_scan) || ser_32(m)) [sha256 with tag "BIP0352/Label"] */
+ secp256k1_silentpayments_sha256_init_label(&hash);
+ secp256k1_sha256_write(secp256k1_get_hash_context(ctx), &hash, scan_key32, 32);
+ secp256k1_write_be32(m_serialized, m);
+ secp256k1_sha256_write(secp256k1_get_hash_context(ctx), &hash, m_serialized, sizeof(m_serialized));
+ secp256k1_sha256_finalize(secp256k1_get_hash_context(ctx), &hash, label_tweak32);
+
+ ret &= secp256k1_ec_pubkey_create_helper(&ctx->ecmult_gen_ctx, &label_tweak_scalar, &label_ge, label_tweak32);
+ secp256k1_silentpayments_label_save(label, &label_ge);
+ secp256k1_memczero(label, sizeof(*label), !ret);
+ secp256k1_memczero(label_tweak32, 32, !ret);
+
+ secp256k1_scalar_clear(&label_tweak_scalar);
+ secp256k1_memclear_explicit(m_serialized, sizeof(m_serialized));
+ secp256k1_sha256_clear(&hash);
+
+ return ret;
+}
+
+int secp256k1_silentpayments_recipient_create_labeled_spend_pubkey(const secp256k1_context *ctx, secp256k1_pubkey *labeled_spend_pubkey, const secp256k1_pubkey *unlabeled_spend_pubkey, const secp256k1_silentpayments_label *label) {
+ secp256k1_ge labeled_spend_pubkey_ge, label_addend;
+ secp256k1_gej result_gej;
+ secp256k1_ge result_ge;
+ int ret;
+
+ /* Sanity check inputs. */
+ VERIFY_CHECK(ctx != NULL);
+ ARG_CHECK(labeled_spend_pubkey != NULL);
+ memset(labeled_spend_pubkey, 0, sizeof(*labeled_spend_pubkey));
+ ARG_CHECK(unlabeled_spend_pubkey != NULL);
+ ARG_CHECK(label != NULL);
+
+ /* Calculate labeled_spend_pubkey = unlabeled_spend_pubkey + label.
+ * If either the label or spend public key is invalid, return early.
+ */
+ ret = secp256k1_pubkey_load(ctx, &labeled_spend_pubkey_ge, unlabeled_spend_pubkey);
+ ret &= secp256k1_silentpayments_label_load(ctx, &label_addend, label);
+ if (!ret) {
+ return 0;
+ }
+ secp256k1_gej_set_ge(&result_gej, &labeled_spend_pubkey_ge);
+ secp256k1_gej_add_ge_var(&result_gej, &result_gej, &label_addend, NULL);
+ if (secp256k1_gej_is_infinity(&result_gej)) {
+ return 0;
+ }
+
+ secp256k1_ge_set_gej_var(&result_ge, &result_gej);
+ secp256k1_pubkey_save(labeled_spend_pubkey, &result_ge);
+
+ return 1;
+}
#endif
diff --git a/src/modules/silentpayments/tests_impl.h b/src/modules/silentpayments/tests_impl.h
index b770335..de6842c 100644
--- a/src/modules/silentpayments/tests_impl.h
+++ b/src/modules/silentpayments/tests_impl.h
@@ -286,10 +286,88 @@ static void test_send_api(void) {
}
}
+static void test_label_api(void) {
+ secp256k1_silentpayments_label l;
+ secp256k1_pubkey s, ls, e; /* spend pk, labeled spend pk, expected labeled spend pk */
+ unsigned char lt[32]; /* label tweak */
+ unsigned char label_ser[33]; /* serialized label */
+ const unsigned char expected[33] = {
+ 0x03, 0xdc, 0x7f, 0x09, 0x9a, 0xbe, 0x95, 0x7a,
+ 0x58, 0x43, 0xd2, 0xb6, 0xbb, 0x35, 0x79, 0x61,
+ 0x5c, 0x60, 0x36, 0xa4, 0x9b, 0x86, 0xf4, 0xbe,
+ 0x46, 0x38, 0x60, 0x28, 0xa8, 0x1a, 0x77, 0xd4,
+ 0x91
+ };
+
+ /* Create a label and labeled spend public key, verify we get the expected result */
+ CHECK(secp256k1_ec_pubkey_parse(CTX, &s, BOB_ADDRESS[1], 33));
+ CHECK(secp256k1_silentpayments_recipient_label_create(CTX, &l, lt, ALICE_SECKEY, 1));
+ CHECK(secp256k1_silentpayments_recipient_create_labeled_spend_pubkey(CTX, &ls, &s, &l));
+ CHECK(secp256k1_ec_pubkey_parse(CTX, &e, expected, 33));
+ CHECK(secp256k1_ec_pubkey_cmp(CTX, &ls, &e) == 0);
+
+ /* Check label (de)serialization round-trip */
+ {
+ secp256k1_silentpayments_label parsed_label;
+ unsigned char parsed_label_ser[33];
+ static const unsigned char invalid_label_ser[33] = {0};
+
+ CHECK(secp256k1_silentpayments_recipient_label_serialize(CTX, label_ser, &l));
+ CHECK(secp256k1_silentpayments_recipient_label_parse(CTX, &parsed_label, label_ser));
+ CHECK(secp256k1_silentpayments_recipient_label_serialize(CTX, parsed_label_ser, &parsed_label));
+ CHECK(secp256k1_memcmp_var(label_ser, parsed_label_ser, 33) == 0);
+
+ CHECK(secp256k1_silentpayments_recipient_label_parse(CTX, &parsed_label, invalid_label_ser) == 0);
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_label_serialize(CTX, parsed_label_ser, &parsed_label));
+ }
+
+ /* Check null values are handled */
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_label_create(CTX, NULL, lt, ALICE_SECKEY, 1));
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_label_create(CTX, &l, NULL, ALICE_SECKEY, 1));
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_label_create(CTX, &l, lt, NULL, 1));
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_label_parse(CTX, NULL, expected));
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_label_parse(CTX, &l, NULL));
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_label_serialize(CTX, NULL, &l));
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_label_serialize(CTX, label_ser, NULL));
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_create_labeled_spend_pubkey(CTX, NULL, &s, &l));
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_create_labeled_spend_pubkey(CTX, &ls, NULL, &l));
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_create_labeled_spend_pubkey(CTX, &ls, &s, NULL));
+ /* Check that creating a label with an invalid scan key fails */
+ CHECK(secp256k1_silentpayments_recipient_label_create(CTX, &l, lt, MALFORMED_SECKEY, 1) == 0);
+ CHECK(secp256k1_silentpayments_recipient_label_create(CTX, &l, lt, secp256k1_group_order_bytes, 1) == 0);
+ /* Check for malformed spend public key and label, i.e., any single pubkey is malformed or the public
+ * keys are valid but sum up to zero.
+ */
+ {
+ secp256k1_pubkey neg_spend_pubkey = s;
+ unsigned char neg_spend_label_ser[33];
+ size_t serlen = 33;
+ secp256k1_silentpayments_label neg_spend_label;
+
+ CHECK(secp256k1_ec_pubkey_negate(CTX, &neg_spend_pubkey));
+ CHECK(secp256k1_ec_pubkey_serialize(CTX, neg_spend_label_ser, &serlen, &neg_spend_pubkey, SECP256K1_EC_COMPRESSED));
+ CHECK(secp256k1_silentpayments_recipient_label_parse(CTX, &neg_spend_label, neg_spend_label_ser));
+
+ CHECK(secp256k1_silentpayments_recipient_create_labeled_spend_pubkey(CTX, &ls, &s, &neg_spend_label) == 0);
+ /* Also test with a malformed spend public key. */
+ memset(&s, 0, sizeof(s));
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_create_labeled_spend_pubkey(CTX, &ls, &s, &neg_spend_label));
+ /* Reset s back to a valid public key for the next test. */
+ CHECK(secp256k1_ec_pubkey_parse(CTX, &s, BOB_ADDRESS[1], 33));
+ memset(&l, 0, sizeof(l));
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_create_labeled_spend_pubkey(CTX, &ls, &s, &l));
+ /* Reset l back to a valid public key for the next test */
+ CHECK(secp256k1_silentpayments_recipient_label_create(CTX, &l, lt, ALICE_SECKEY, 1));
+ memset(&s, 0, sizeof(s));
+ CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_create_labeled_spend_pubkey(CTX, &ls, &s, &l));
+ }
+}
+
/* --- Test registry --- */
static const struct tf_test_entry tests_silentpayments[] = {
CASE1(test_recipient_sort),
CASE1(test_send_api),
+ CASE1(test_label_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.