Problem statement: The current silentpayments sending API consists of a single function that creates transaction (x-only public key) outputs corresponding to the recipient list, without providing a possibility to verify whether these outputs have been derived correctly. This is fine for scenarios where both transaction creation and broadcasting happens on a single device (e.g. hot wallets), but is insufficient if the transaction broadcasting happens on a different machine that doesn't have access to the secret keys (e.g. HW wallets); in this case, there is the possibility that the "SP output creation" device has derived those incorrectly (either by malicious intent or e.g. a bug), which could ultimately lead to a loss of money if it remains undetected before broadcasting. We can verify correct SP output creation without the need of secret key access by using DLEQ proofs, see also the motivation section in BIP-374 and a explaining blog post by benma from BitBox.
This issue is opened for the purpose of figuring out on how to best integrate this functionality w.r.t. API design, continuing the recent discussion started in #1802 (comment). Prior work in this direction has been done in PR #1651, where two new SP functions _sender_create_outputs_with_proof and _verify_proof are added (IIUC the outputs re-creation part is missing there). PR #1802 provides a generic DLEQ module, which might still be useful and could go in first.
Designing the API to be compatible with the strongly related BIP-375 makes a lot of sense; I don't think it's strictly necessary to fully support it at this point, the "per-input ECDH share creation/aggregation" parts seems less important than the one for the global share/proof (which solves the issue described above and already used as of today, e.g. in BitBox02), though it's reasonable to keep it in mind for API design already. Curious to hear opinions on that.
<details>
<summary>Quoting also the proposed API design by [@macgyver13](/bitcoin-core-secp256k1/contributor/macgyver13/) here, as I think it could serve as a good starting point:</summary>
The shape follows from BIP375 having two forms of share/proof, a global one over every eligible input key and a per-input one, plus a verifier holding no key for the inputs it checks and a combiner holding none at all. Listed below in order of how much key material the caller holds, starting with @stratospher's existing function for context:
/* Signer holding EVERY eligible input key, on one device: * derive the outputs and the global share/proof together, sharing the single ECDH * per unique scan key that both steps need. This is [#1651](/bitcoin-core-secp256k1/1651/)'s existing function, * shown with master's parameter naming. Would fill PSBT_GLOBAL_SP_ECDH_SHARE / _DLEQ, * from dleq_data, subject to the question below the block. */ int secp256k1_silentpayments_sender_create_outputs_with_proof( const secp256k1_context *ctx, secp256k1_xonly_pubkey **generated_outputs, secp256k1_silentpayments_dleq_data **dleq_data, size_t *n_dleq_size, 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 ); /* Signer creates the ECDH share and DLEQ proof for ONE scan key. Derives no outputs, * so a caller holding only some of the eligible input keys can still contribute its * share to a transaction it cannot complete alone. * * The share is computed over the sum of whatever keys are passed, and that choice * decides which PSBT field the result belongs in: * all eligible input keys -> global share -> PSBT_GLOBAL_SP_ECDH_SHARE / _DLEQ * one input's key -> that input's -> PSBT_IN_SP_ECDH_SHARE / _DLEQ * * Which mode to use is the caller's decision: BIP375 allows a signer holding every key * to still choose per-input ("or does not want to create a global ECDH share"). * * Call once per scan key in global mode; once per (input, scan key) pair in per-input * mode. Keys passed via keypairs are treated as taproot inputs and even-Y negated * before summing as in _sender_create_outputs. */ int secp256k1_silentpayments_sender_create_share_and_proof( const secp256k1_context *ctx, unsigned char *share33, unsigned char *proof64, const secp256k1_pubkey *recipient_scan_pubkey, const unsigned char *aux_rand32, const secp256k1_keypair * const *keypairs, size_t n_keypairs, const unsigned char * const *seckeys, size_t n_seckeys ); /* Signer verifying shares it did NOT create, holding no key for those inputs: * BIP375 assigns this to the party that verifies proofs "for all inputs it does * not have the private keys for". * One pubkey for the per-input case, all of them for the global case. */ int secp256k1_silentpayments_verify_share_proof( const secp256k1_context *ctx, const unsigned char *share33, const unsigned char *proof64, const secp256k1_pubkey *recipient_scan_pubkey, const secp256k1_xonly_pubkey * const *xonly_pubkeys, size_t n_xonly_pubkeys, const secp256k1_pubkey * const *pubkeys, size_t n_pubkeys ); /* Transaction Extractor, holding NO secret keys at all: * compute output scripts from shares supplied by others. This is the case * _sender_create_outputs_with_proof cannot cover, since it requires the seckeys * in order to derive the outputs itself. * Takes one already-summed share per scan key, keyed by share_scan_pubkeys, so a * caller holding per-input shares combines them first. * Input pubkeys for every eligible input are needed, since input_hash commits * to their sum. */ int secp256k1_silentpayments_sender_create_outputs_from_shares( 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_pubkey * const *share_scan_pubkeys, const unsigned char * const *shares33, size_t n_shares, const secp256k1_xonly_pubkey * const *xonly_pubkeys, size_t n_xonly_pubkeys, const secp256k1_pubkey * const *pubkeys, size_t n_pubkeys );A sender holding every key should keep using the first, unchanged in shape, since deriving outputs and proofs together shares the one ECDH per unique scan key that both need. The other three are illustrative. The last one keys each share by its scan pubkey, matching how BIP375 keys the share fields themselves.
</details>
I have a few thoughts on that already (e.g. on simplification by passing prevouts_summary objects instead of public key objects on the verification side, and maybe unifying the "verify proofs" and "re-create outputs" in a single API call), which I will try to structure and write down here within the next days.