Skip to content
38 changes: 38 additions & 0 deletions include/secp256k1_silentpayments.h
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,44 @@ SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_silentpayments_recipien
) 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 Payment output public keys.
*
* Given a scan key, a prevouts_summary, and 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 an output (the first output) 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_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 if the transaction is not a silent payments transaction.
*
* 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 `_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 **spend_pubkeys,
Copy link

@nymius nymius Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason for this to not be const secp256k1_pubkey * const *spend_pubkeys? like _pubkeys in _silentpayments_recipient_prevouts_summary_create or _seckeys in _silentpayments_sender_create_outputs

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, outputs_xonly is the out-param where we will write the generated xonly public keys. This is the same as found_outputs in the scanning function and generated_outputs in the _create_outputs function.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we referring to the same line? I was talking about line 421: const secp256k1_pubkey **spend_pubkeys.

Copy link

@nymius nymius Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I missed a word in the original comment. I edited it. tldr:
Why not const secp256k1_pubkey **spend_pubkeys -> const secp256k1_pubkey * const *spend_pubkeys?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, gotcha! Sorry, I misunderstood your original comment. This should be const secp256k1_pubkey * const *spend_pubkeys, as you suggest.

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
Expand Down
33 changes: 33 additions & 0 deletions src/modules/silentpayments/main_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -761,4 +761,37 @@ 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 **spend_pubkeys, size_t n_spend_pubkeys)
{
secp256k1_scalar scan_key_scalar;
secp256k1_ge input_pubkey_ge;
int combined, valid_scan_key;
unsigned char shared_secret[33];

VERIFY_CHECK(ctx != NULL);
ARG_CHECK(outputs_xonly != NULL);
ARG_CHECK(scan_key32 != NULL);
ARG_CHECK(prevouts_summary != NULL);
ARG_CHECK(spend_pubkeys != NULL);
ARG_CHECK(n_spend_pubkeys > 0);
ARG_CHECK(secp256k1_memcmp_var(&prevouts_summary->data[0], secp256k1_silentpayments_prevouts_summary_magic, 4) == 0);
/* If there are any issues with the recipient scan key, return early. */
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(&input_pubkey_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(ctx, shared_secret, &input_pubkey_ge, &scan_key_scalar);
secp256k1_scalar_clear(&scan_key_scalar);
return secp256k1_silentpayments_create_output_pubkeys(ctx, outputs_xonly, shared_secret, spend_pubkeys, n_spend_pubkeys, 0);
Comment on lines +793 to +794
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in _recipient_create_output_pubkeys: should also clear out the shared_secret here

}

#endif
28 changes: 28 additions & 0 deletions src/modules/silentpayments/tests_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,10 @@ static void test_recipient_api(void) {
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_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 */
secp256k1_pubkey p; /* plain public key */
secp256k1_pubkey malformed_p; /* malformed public key */
secp256k1_pubkey const *pp[1]; /* array of pointers to plain pks */
Expand All @@ -351,6 +355,12 @@ static void test_recipient_api(void) {
size_t n_f; /* number of found outputs */

CHECK(secp256k1_ec_pubkey_parse(CTX, &p, BOB_ADDRESS[0], 33));
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];
memset(&malformed_p, 0, sizeof(malformed_p));
memset(&malformed_t, 0, sizeof(malformed_t));
memset(&md, 0, sizeof(md));
Expand Down Expand Up @@ -470,6 +480,24 @@ static void test_recipient_api(void) {
CHECK(secp256k1_silentpayments_recipient_scan_outputs(CTX, fp, &n_f, tp, 1, MALFORMED_SECKEY, &pd, &p, NULL, NULL) == 0);
memset(&pd, 0, sizeof(pd));
CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_scan_outputs(CTX, fp, &n_f, tp, 1, ALICE_SECKEY, &pd, &p, NULL, NULL));
/* Reset pd to a valid prevouts_summary object */
CHECK(secp256k1_silentpayments_recipient_prevouts_summary_parse(CTX, &pd, BOB_ADDRESS[0], 33));

/* Test recipient light client API */
CHECK(secp256k1_silentpayments_recipient_create_output_pubkeys(CTX, output_ptrs, ALICE_SECKEY, &pd, spend_pubkey_ptrs, 1));
CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_create_output_pubkeys(CTX, NULL, ALICE_SECKEY, &pd, spend_pubkey_ptrs, 1));
CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_create_output_pubkeys(CTX, output_ptrs, NULL, &pd, spend_pubkey_ptrs, 1));
CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_create_output_pubkeys(CTX, output_ptrs, ALICE_SECKEY, NULL, spend_pubkey_ptrs, 1));
CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_create_output_pubkeys(CTX, output_ptrs, ALICE_SECKEY, &pd, NULL, 1));
CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_create_output_pubkeys(CTX, output_ptrs, ALICE_SECKEY, &pd, spend_pubkey_ptrs, 0));
memset(&pd, 0, sizeof(pd));
CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_create_output_pubkeys(CTX, output_ptrs, ALICE_SECKEY, &pd, spend_pubkey_ptrs, 1));
/* Reset pd to a valid prevouts_summary object */
CHECK(secp256k1_silentpayments_recipient_prevouts_summary_parse(CTX, &pd, BOB_ADDRESS[0], 33));
CHECK(secp256k1_silentpayments_recipient_create_output_pubkeys(CTX, output_ptrs, MALFORMED_SECKEY, &pd, spend_pubkey_ptrs, 1) == 0);
/* Create uncombined prevouts_summary */
CHECK(secp256k1_silentpayments_recipient_prevouts_summary_create(CTX, &pd, SMALLEST_OUTPOINT, tp, 1, pp, 1));
CHECK(secp256k1_silentpayments_recipient_create_output_pubkeys(CTX, output_ptrs, ALICE_SECKEY, &pd, spend_pubkey_ptrs, 1));
}

void run_silentpayments_tests(void) {
Expand Down