Skip to content

Commit

Permalink
silentpayments: sending
Browse files Browse the repository at this point in the history
Add a routine for the entire sending flow which takes a set of private keys,
the smallest outpoint, and list of recipients and returns a list of
x-only public keys by performing the following steps:

1. Sum up the private keys
2. Calculate the input_hash
3. For each recipient group:
    3a. Calculate a shared secret
    3b. Create the requested number of outputs

This function assumes a single sender context in that it requires the
sender to have access to all of the private keys. In the future, this
API may be expanded to allow for a multiple senders or for a single
sender who does not have access to all private keys at any given time,
but for now these modes are considered out of scope / unsafe.

Internal to the library, add:

1. A function for creating shared secrets (i.e., a*B or b*A)
2. A function for generating the "SharedSecret" tagged hash
3. A function for creating a single output public key

Finally, add tests for the sender API.
  • Loading branch information
josibake committed Aug 15, 2024
1 parent 1c74941 commit 9d6769f
Show file tree
Hide file tree
Showing 4 changed files with 564 additions and 2 deletions.
82 changes: 82 additions & 0 deletions include/secp256k1_silentpayments.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#define SECP256K1_SILENTPAYMENTS_H

#include "secp256k1.h"
#include "secp256k1_extrakeys.h"

#ifdef __cplusplus
extern "C" {
Expand All @@ -25,6 +26,87 @@ extern "C" {
* any further elliptic-curve operations from the wallet.
*/

/* This struct serves as an In param for passing the silent payment address
* data. The index field is for when more than one address is being sent to in
* a transaction. Index is set based on the original ordering of the addresses
* and used to return the generated outputs matching the original ordering.
* When more than one recipient is used the recipient array will be sorted in
* place as part of generating the outputs, but the generated outputs will be
* returned in the original ordering specified by the index to ensure the
* caller is able to match up the generated outputs to the correct silent
* payment address (e.g. to be able to assign the correct amounts to the
* correct generated outputs in the final transaction).
*/
typedef struct {
secp256k1_pubkey scan_pubkey;
secp256k1_pubkey spend_pubkey;
size_t index;
} secp256k1_silentpayments_recipient;

/** Create Silent Payment outputs for recipient(s).
*
* Given a list of n private keys a_1...a_n (one for each silent payment
* eligible input to spend), a serialized outpoint, and a list of recipients,
* create the taproot outputs.
*
* `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.
*
* If necessary, the private keys are negated to enforce the right y-parity.
* For that reason, the private keys have to be passed in via two different
* parameter pairs, depending on whether the seckeys correspond to x-only
* outputs or not.
*
* Returns: 1 if creation of outputs was successful. 0 if an error occured.
* Args: ctx: pointer to a context object
* Out: generated_outputs: pointer to an array of pointers to xonly pubkeys,
* one per recipient.
* The order of outputs here matches the original
* ordering of the recipients array.
* In: recipients: pointer to an array of pointers to silent payment
* 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 sorted in place, but
* generated outputs are saved in the
* `generated_outputs` array to match the ordering
* from the index field. This ensures the caller is
* able to match the generated outputs to the
* correct silent payment addresses. The same
* recipient can be passed multiple times to create
* multiple outputs for the same recipient.
* n_recipients: the number of recipients. This is equal to the
* total number of outputs to be generated as each
* recipient may passed multiple times to generate
* multiple outputs for the same recipient
* outpoint_smallest36: serialized smallest outpoint (lexicographically)
* from the transaction inputs
* taproot_seckeys: pointer to an array of pointers to 32-byte
* private keys of taproot inputs (can be NULL if no
* private keys of taproot inputs are used)
* n_taproot_seckeys: the number of sender's taproot input private keys
* plain_seckeys: pointer to an array of pointers to 32-byte
* private keys of non-taproot inputs (can be NULL
* if no private keys of non-taproot inputs are
* used)
* n_plain_seckeys: the number of sender's non-taproot input private
* keys
*/
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 *taproot_seckeys,
size_t n_taproot_seckeys,
const unsigned char * const *plain_seckeys,
size_t n_plain_seckeys
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(5);

#ifdef __cplusplus
}
#endif
Expand Down
243 changes: 241 additions & 2 deletions src/modules/silentpayments/main_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,249 @@
#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. */
/** Sort an array of silent payment 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. */
static int secp256k1_silentpayments_recipient_sort_cmp(const void* pk1, const void* pk2, void *ctx) {
return secp256k1_ec_pubkey_cmp((secp256k1_context *)ctx,
&(*(const secp256k1_silentpayments_recipient **)pk1)->scan_pubkey,
&(*(const secp256k1_silentpayments_recipient **)pk2)->scan_pubkey
);
}

/* TODO: implement functions for receiver side. */
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) {
secp256k1_sha256_initialize(hash);
hash->s[0] = 0xd4143ffcul;
hash->s[1] = 0x012ea4b5ul;
hash->s[2] = 0x36e21c8ful;
hash->s[3] = 0xf7ec7b54ul;
hash->s[4] = 0x4dd4e2acul;
hash->s[5] = 0x9bcaa0a4ul;
hash->s[6] = 0xe244899bul;
hash->s[7] = 0xcd06903eul;

hash->bytes = 64;
}

static void secp256k1_silentpayments_calculate_input_hash(unsigned char *input_hash, const unsigned char *outpoint_smallest36, secp256k1_ge *pubkey_sum) {
secp256k1_sha256 hash;
unsigned char pubkey_sum_ser[33];
size_t len;
int ret;

secp256k1_silentpayments_sha256_init_inputs(&hash);
secp256k1_sha256_write(&hash, outpoint_smallest36, 36);
ret = secp256k1_eckey_pubkey_serialize(pubkey_sum, pubkey_sum_ser, &len, 1);
VERIFY_CHECK(ret && len == sizeof(pubkey_sum_ser));
(void)ret;
secp256k1_sha256_write(&hash, pubkey_sum_ser, sizeof(pubkey_sum_ser));
secp256k1_sha256_finalize(&hash, input_hash);
}

static void secp256k1_silentpayments_create_shared_secret(unsigned char *shared_secret33, const secp256k1_scalar *secret_component, const secp256k1_ge *public_component) {
secp256k1_gej ss_j;
secp256k1_ge ss;
size_t len;
int ret;

/* Compute shared_secret = tweaked_secret_component * Public_component */
secp256k1_ecmult_const(&ss_j, public_component, secret_component);
secp256k1_ge_set_gej(&ss, &ss_j);
/* This can only fail if the shared secret is the point at infinity, which should be
* impossible at this point, considering we have already validated the public key and
* the secret key being used
*/
ret = secp256k1_eckey_pubkey_serialize(&ss, shared_secret33, &len, 1);
VERIFY_CHECK(ret && len == 33);
(void)ret;
/* While not technically "secret" data, explicitly clear the shared secret since leaking this would allow an attacker
* to identify the resulting transaction as a silent payments transaction and potentially link the transaction
* back to the silent payment address
*/
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) {
secp256k1_sha256_initialize(hash);
hash->s[0] = 0x88831537ul;
hash->s[1] = 0x5127079bul;
hash->s[2] = 0x69c2137bul;
hash->s[3] = 0xab0303e6ul;
hash->s[4] = 0x98fa21faul;
hash->s[5] = 0x4a888523ul;
hash->s[6] = 0xbd99daabul;
hash->s[7] = 0xf25e5e0aul;

hash->bytes = 64;
}

static void secp256k1_silentpayments_create_t_k(secp256k1_scalar *t_k_scalar, const unsigned char *shared_secret33, unsigned int k) {
secp256k1_sha256 hash;
unsigned char hash_ser[32];
unsigned char k_serialized[4];

/* Compute t_k = hash(shared_secret || ser_32(k)) [sha256 with tag "BIP0352/SharedSecret"] */
secp256k1_silentpayments_sha256_init_sharedsecret(&hash);
secp256k1_sha256_write(&hash, shared_secret33, 33);
secp256k1_write_be32(k_serialized, k);
secp256k1_sha256_write(&hash, k_serialized, sizeof(k_serialized));
secp256k1_sha256_finalize(&hash, hash_ser);
secp256k1_scalar_set_b32(t_k_scalar, hash_ser, NULL);
/* While not technically "secret" data, explicitly clear hash_ser since leaking this would allow an attacker
* to identify the resulting transaction as a silent payments transaction and potentially link the transaction
* back to the silent payment address
*/
memset(hash_ser, 0, sizeof(hash_ser));
}

static int secp256k1_silentpayments_create_output_pubkey(const secp256k1_context *ctx, secp256k1_xonly_pubkey *P_output_xonly, const unsigned char *shared_secret33, const secp256k1_pubkey *recipient_spend_pubkey, unsigned int k) {
secp256k1_ge P_output_ge;
secp256k1_scalar t_k_scalar;
int ret;

/* Calculate and return P_output_xonly = B_spend + t_k * G
* This will fail if B_spend is the point at infinity or if
* B_spend + t_k*G is the point at infinity.
*/
secp256k1_silentpayments_create_t_k(&t_k_scalar, shared_secret33, k);
ret = secp256k1_pubkey_load(ctx, &P_output_ge, recipient_spend_pubkey);
ret &= secp256k1_eckey_pubkey_tweak_add(&P_output_ge, &t_k_scalar);
secp256k1_xonly_pubkey_save(P_output_xonly, &P_output_ge);

/* While not technically "secret" data, explicitly clear t_k since leaking this would allow an attacker
* to identify the resulting transaction as a silent payments transaction and potentially link the transaction
* back to the silent payment address
*/
secp256k1_scalar_clear(&t_k_scalar);
return ret;
}

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 *taproot_seckeys,
size_t n_taproot_seckeys,
const unsigned char * const *plain_seckeys,
size_t n_plain_seckeys
) {
size_t i, k;
secp256k1_scalar a_sum_scalar, addend, input_hash_scalar;
secp256k1_ge A_sum_ge;
secp256k1_gej A_sum_gej;
unsigned char input_hash[32];
unsigned char shared_secret[33];
secp256k1_silentpayments_recipient last_recipient;
int overflow = 0;
int ret = 1;

/* 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((plain_seckeys != NULL) || (taproot_seckeys != NULL));
if (taproot_seckeys != NULL) {
ARG_CHECK(n_taproot_seckeys > 0);
} else {
ARG_CHECK(n_taproot_seckeys == 0);
}
if (plain_seckeys != NULL) {
ARG_CHECK(n_plain_seckeys > 0);
} else {
ARG_CHECK(n_plain_seckeys == 0);
}
ARG_CHECK(outpoint_smallest36 != NULL);
/* ensure the index field is set correctly */
for (i = 0; i < n_recipients; i++) {
ARG_CHECK(recipients[i]->index == i);
}

/* Compute input private keys sum: a_sum = a_1 + a_2 + ... + a_n */
a_sum_scalar = secp256k1_scalar_zero;
for (i = 0; i < n_plain_seckeys; i++) {
/* TODO: in other places where _set_b32_seckey is called, its normally followed by a _cmov call
* Do we need that here and if so, is it better to call it after the loop is finished?
*/
ret &= secp256k1_scalar_set_b32_seckey(&addend, plain_seckeys[i]);
secp256k1_scalar_add(&a_sum_scalar, &a_sum_scalar, &addend);
}
/* private keys used for taproot outputs have to be negated if they resulted in an odd point */
for (i = 0; i < n_taproot_seckeys; i++) {
secp256k1_ge addend_point;
/* TODO: why don't we need _cmov here after calling keypair_load? Because the ret is declassified? */
ret &= secp256k1_keypair_load(ctx, &addend, &addend_point, taproot_seckeys[i]);
if (secp256k1_fe_is_odd(&addend_point.y)) {
secp256k1_scalar_negate(&addend, &addend);
}
secp256k1_scalar_add(&a_sum_scalar, &a_sum_scalar, &addend);
}
/* If there are any failures in loading/summing up the secret keys, fail early */
if (!ret || secp256k1_scalar_is_zero(&a_sum_scalar)) {
return 0;
}
/* Compute input_hash = hash(outpoint_L || (a_sum * G)) */
secp256k1_ecmult_gen(&ctx->ecmult_gen_ctx, &A_sum_gej, &a_sum_scalar);
secp256k1_ge_set_gej(&A_sum_ge, &A_sum_gej);

/* Calculate the input hash and tweak a_sum, i.e., a_sum_tweaked = a_sum * input_hash */
secp256k1_silentpayments_calculate_input_hash(input_hash, outpoint_smallest36, &A_sum_ge);
secp256k1_scalar_set_b32(&input_hash_scalar, input_hash, &overflow);
ret &= !overflow;
secp256k1_scalar_mul(&a_sum_scalar, &a_sum_scalar, &input_hash_scalar);
secp256k1_silentpayments_recipient_sort(ctx, recipients, n_recipients);
last_recipient = *recipients[0];
k = 0;
for (i = 0; i < n_recipients; i++) {
if ((i == 0) || (secp256k1_ec_pubkey_cmp(ctx, &last_recipient.scan_pubkey, &recipients[i]->scan_pubkey) != 0)) {
/* If we are on a different scan pubkey, its time to recreate the the shared secret and reset k to 0.
* It's very unlikely tha 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;
ret &= secp256k1_pubkey_load(ctx, &pk, &recipients[i]->scan_pubkey);
if (!ret) break;
secp256k1_silentpayments_create_shared_secret(shared_secret, &a_sum_scalar, &pk);
k = 0;
}
ret &= secp256k1_silentpayments_create_output_pubkey(ctx, generated_outputs[recipients[i]->index], shared_secret, &recipients[i]->spend_pubkey, k);
k++;
last_recipient = *recipients[i];
}
/* Explicitly clear variables containing secret data */
secp256k1_scalar_clear(&addend);
secp256k1_scalar_clear(&a_sum_scalar);

/* While technically not "secret data," explicitly clear the shared secret since leaking this
* could result in a third party being able to identify the transaction as a silent payments transaction
* and potentially link the transaction back to a silent payment address
*/
memset(&shared_secret, 0, sizeof(shared_secret));
return ret;
}

#endif
Loading

0 comments on commit 9d6769f

Please sign in to comment.