Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit df6b63d

Browse files
committedMay 3, 2024
silentpayments: add examples/silentpayments.c
Demonstrate sending, scanning, and light client scanning.
1 parent bc4533f commit df6b63d

File tree

3 files changed

+429
-0
lines changed

3 files changed

+429
-0
lines changed
 

‎.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ ctime_tests
1010
ecdh_example
1111
ecdsa_example
1212
schnorr_example
13+
silentpayments_example
1314
*.exe
1415
*.so
1516
*.a

‎Makefile.am

+11
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,17 @@ schnorr_example_LDFLAGS += -lbcrypt
184184
endif
185185
TESTS += schnorr_example
186186
endif
187+
if ENABLE_MODULE_SILENTPAYMENTS
188+
noinst_PROGRAMS += silentpayments_example
189+
silentpayments_example_SOURCES = examples/silentpayments.c
190+
silentpayments_example_CPPFLAGS = -I$(top_srcdir)/include -DSECP256K1_STATIC
191+
silentpayments_example_LDADD = libsecp256k1.la
192+
silentpayments_example_LDFLAGS = -static
193+
if BUILD_WINDOWS
194+
silentpayments_example_LDFLAGS += -lbcrypt
195+
endif
196+
TESTS += silentpayments_example
197+
endif
187198
endif
188199

189200
### Precomputed tables

‎examples/silentpayments.c

+417
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,417 @@
1+
/*************************************************************************
2+
* Written in 2024 by josibake *
3+
* To the extent possible under law, the author(s) have dedicated all *
4+
* copyright and related and neighboring rights to the software in this *
5+
* file to the public domain worldwide. This software is distributed *
6+
* without any warranty. For the CC0 Public Domain Dedication, see *
7+
* EXAMPLES_COPYING or https://creativecommons.org/publicdomain/zero/1.0 *
8+
*************************************************************************/
9+
10+
#include <stdio.h>
11+
#include <assert.h>
12+
#include <string.h>
13+
14+
#include <secp256k1.h>
15+
#include <secp256k1_extrakeys.h>
16+
#include <secp256k1_silentpayments.h>
17+
18+
#include "examples_util.h"
19+
20+
/* Static data for Bob and Carol's silent payment addresses.
21+
* This consists of a scan key for each and the addresse data for each
22+
*/
23+
static unsigned char smallest_outpoint[36] = {
24+
0x16,0x9e,0x1e,0x83,0xe9,0x30,0x85,0x33,0x91,
25+
0xbc,0x6f,0x35,0xf6,0x05,0xc6,0x75,0x4c,0xfe,
26+
0xad,0x57,0xcf,0x83,0x87,0x63,0x9d,0x3b,0x40,
27+
0x96,0xc5,0x4f,0x18,0xf4,0x00,0x00,0x00,0x00
28+
};
29+
static unsigned char bob_scan_key[32] = {
30+
0xa8,0x90,0x54,0xc9,0x5b,0xe3,0xc3,0x01,
31+
0x56,0x65,0x74,0xf2,0xaa,0x93,0xad,0xe0,
32+
0x51,0x85,0x09,0x03,0xa6,0x9c,0xbd,0xd1,
33+
0xd4,0x7e,0xae,0x26,0x3d,0x7b,0xc0,0x31
34+
};
35+
static unsigned char bob_spend_pubkey[33] = {
36+
0x02,0xee,0x97,0xdf,0x83,0xb2,0x54,0x6a,
37+
0xf5,0xa7,0xd0,0x62,0x15,0xd9,0x8b,0xcb,
38+
0x63,0x7f,0xe0,0x5d,0xd0,0xfa,0x37,0x3b,
39+
0xd8,0x20,0xe6,0x64,0xd3,0x72,0xde,0x9a,0x01
40+
};
41+
static unsigned char bob_address[2][33] = {
42+
{
43+
0x02,0x15,0x40,0xae,0xa8,0x97,0x54,0x7a,
44+
0xd4,0x39,0xb4,0xe0,0xf6,0x09,0xe5,0xf0,
45+
0xfa,0x63,0xde,0x89,0xab,0x11,0xed,0xe3,
46+
0x1e,0x8c,0xde,0x4b,0xe2,0x19,0x42,0x5f,0x23
47+
},
48+
{
49+
0x02,0x3e,0xff,0xf8,0x18,0x51,0x65,0xea,
50+
0x63,0xa9,0x92,0xb3,0x9f,0x31,0xd8,0xfd,
51+
0x8e,0x0e,0x64,0xae,0xf9,0xd3,0x88,0x07,
52+
0x34,0x97,0x37,0x14,0xa5,0x3d,0x83,0x11,0x8d
53+
}
54+
};
55+
static unsigned char carol_scan_key[32] = {
56+
0x04,0xb2,0xa4,0x11,0x63,0x5c,0x09,0x77,
57+
0x59,0xaa,0xcd,0x0f,0x00,0x5a,0x4c,0x82,
58+
0xc8,0xc9,0x28,0x62,0xc6,0xfc,0x28,0x4b,
59+
0x80,0xb8,0xef,0xeb,0xc2,0x0c,0x3d,0x17
60+
};
61+
static unsigned char carol_address[2][33] = {
62+
{
63+
0x03,0xbb,0xc6,0x3f,0x12,0x74,0x5d,0x3b,
64+
0x9e,0x9d,0x24,0xc6,0xcd,0x7a,0x1e,0xfe,
65+
0xba,0xd0,0xa7,0xf4,0x69,0x23,0x2f,0xbe,
66+
0xcf,0x31,0xfb,0xa7,0xb4,0xf7,0xdd,0xed,0xa8
67+
},
68+
{
69+
0x03,0x81,0xeb,0x9a,0x9a,0x9e,0xc7,0x39,
70+
0xd5,0x27,0xc1,0x63,0x1b,0x31,0xb4,0x21,
71+
0x56,0x6f,0x5c,0x2a,0x47,0xb4,0xab,0x5b,
72+
0x1f,0x6a,0x68,0x6d,0xfb,0x68,0xea,0xb7,0x16
73+
}
74+
};
75+
76+
/* Labels
77+
* The structs and call back function are for demonstration only and not optimized.
78+
* In a production usecase, it is expected that the caller will be using a much more performant
79+
* method for storing and querying labels.
80+
*/
81+
82+
struct label_cache_entry {
83+
secp256k1_pubkey label;
84+
unsigned char label_tweak[32];
85+
};
86+
87+
struct labels_cache {
88+
const secp256k1_context *ctx;
89+
size_t entries_used;
90+
struct label_cache_entry entries[5];
91+
};
92+
93+
const unsigned char* label_lookup(const secp256k1_pubkey* key, const void* cache_ptr) {
94+
const struct labels_cache* cache = (const struct labels_cache*)cache_ptr;
95+
size_t i;
96+
for (i = 0; i < cache->entries_used; i++) {
97+
if (secp256k1_ec_pubkey_cmp(cache->ctx, &cache->entries[i].label, key) == 0) {
98+
return cache->entries[i].label_tweak;
99+
}
100+
}
101+
return NULL;
102+
}
103+
104+
int main(void) {
105+
enum { N_TX_INPUTS = 2, N_TX_OUTPUTS = 3 };
106+
unsigned char randomize[32];
107+
unsigned char xonly_print[32];
108+
secp256k1_xonly_pubkey tx_inputs[N_TX_INPUTS];
109+
secp256k1_xonly_pubkey tx_outputs[N_TX_OUTPUTS];
110+
int ret;
111+
size_t i;
112+
/* Before we can call actual API functions, we need to create a "context". */
113+
secp256k1_context* ctx = secp256k1_context_create(SECP256K1_CONTEXT_NONE);
114+
if (!fill_random(randomize, sizeof(randomize))) {
115+
printf("Failed to generate randomness\n");
116+
return 1;
117+
}
118+
/* Randomizing the context is recommended to protect against side-channel
119+
* leakage See `secp256k1_context_randomize` in secp256k1.h for more
120+
* information about it. This should never fail. */
121+
ret = secp256k1_context_randomize(ctx, randomize);
122+
assert(ret);
123+
124+
/*** Sending ***/
125+
{
126+
secp256k1_keypair sender_seckeys[N_TX_INPUTS];
127+
const secp256k1_keypair *sender_seckey_ptrs[N_TX_INPUTS];
128+
secp256k1_silentpayments_recipient recipients[N_TX_OUTPUTS];
129+
const secp256k1_silentpayments_recipient *recipient_ptrs[N_TX_OUTPUTS];
130+
secp256k1_xonly_pubkey *generated_output_ptrs[N_TX_OUTPUTS];
131+
char* address_amounts[N_TX_OUTPUTS] = {"1.0 BTC", "2.0 BTC", "3.0 BTC"};
132+
unsigned char (*sp_addresses[N_TX_OUTPUTS])[2][33];
133+
134+
/*** Generate private keys for the sender ***
135+
*
136+
* In this example, only taproot inputs are used but the function can be called with
137+
* a mix of taproot seckeys and plain seckeys. Taproot seckeys are passed as keypairs
138+
* to allow the sending function to check if the private keys need to be negated without needing
139+
* to do an expensive pubkey generation. This is not needed for plain seckeys since there is no need
140+
* for negation.
141+
*
142+
* The public key from each input keypair is saved in the `tx_inputs` array. This array will be used
143+
* later in the example to represent the public keys the recipient will extracte from the
144+
* transaction inputs.
145+
*/
146+
147+
for (i = 0; i < 2; i++) {
148+
/* If the secret key is zero or out of range (bigger than secp256k1's
149+
* order), we try to sample a new key. Note that the probability of this
150+
* happening is negligible. */
151+
while (1) {
152+
unsigned char seckey[32];
153+
if (!fill_random(seckey, sizeof(seckey))) {
154+
printf("Failed to generate randomness\n");
155+
return 1;
156+
}
157+
/* Try to create a keypair with a valid context, it should only fail if
158+
* the secret key is zero or out of range. */
159+
if (secp256k1_keypair_create(ctx, &sender_seckeys[i], seckey)) {
160+
sender_seckey_ptrs[i] = &sender_seckeys[i];
161+
ret = secp256k1_keypair_xonly_pub(ctx, &tx_inputs[i], NULL, &sender_seckeys[i]);
162+
assert(ret);
163+
break;
164+
} else {
165+
printf("Failed to create keypair\n");
166+
return 1;
167+
}
168+
}
169+
}
170+
/*** Create the recipient objects ***/
171+
172+
/* Alice is sending to Bob and Carol in this transaction:
173+
*
174+
* 1. One output to Bob's labelled address
175+
* 2. Two outputs for Carol (1.0 and 3.0 bitcoin)
176+
*
177+
* Alice creates the recipient objects and adds the index of the original ordering (i.e. the ordering
178+
* of the `sp_addresses` array) to each object. This index is used to return the generated outputs in
179+
* the original ordering so that Alice can match up the generated outputs with the correct amounts.
180+
*
181+
* Note: to create multiple outputs for Carol, Alice simply passes her silent payment
182+
* address mutltiple times.
183+
*/
184+
sp_addresses[0] = &carol_address; /* : 1.0 BTC */
185+
sp_addresses[1] = &bob_address; /* : 2.0 BTC */
186+
sp_addresses[2] = &carol_address; /* : 3.0 BTC */
187+
for (i = 0; i < N_TX_OUTPUTS; i++) {
188+
ret = secp256k1_ec_pubkey_parse(ctx, &recipients[i].scan_pubkey, (*(sp_addresses[i]))[0], 33);
189+
assert(ret);
190+
ret = secp256k1_ec_pubkey_parse(ctx, &recipients[i].spend_pubkey, (*(sp_addresses[i]))[1], 33);
191+
assert(ret);
192+
recipients[i].index = i;
193+
recipient_ptrs[i] = &recipients[i];
194+
}
195+
for (i = 0; i < N_TX_OUTPUTS; i++) {
196+
generated_output_ptrs[i] = &tx_outputs[i];
197+
}
198+
ret = secp256k1_silentpayments_sender_create_outputs(ctx,
199+
generated_output_ptrs,
200+
recipient_ptrs, N_TX_OUTPUTS,
201+
smallest_outpoint,
202+
sender_seckey_ptrs, N_TX_INPUTS,
203+
NULL, 0
204+
);
205+
assert(ret);
206+
printf("Alice created the following outputs for Bob and Carol: \n");
207+
for (i = 0; i < N_TX_OUTPUTS; i++) {
208+
printf(" ");
209+
printf("%s : ", address_amounts[i]);
210+
secp256k1_xonly_pubkey_serialize(ctx, xonly_print, &tx_outputs[i]);
211+
print_hex(xonly_print, sizeof(xonly_print));
212+
}
213+
/* It's best practice to try to clear secrets from memory after using them.
214+
* This is done because some bugs can allow an attacker to leak memory, for
215+
* example through "out of bounds" array access (see Heartbleed), Or the OS
216+
* swapping them to disk. Hence, we overwrite the secret key buffer with zeros.
217+
*
218+
* Here we are preventing these writes from being optimized out, as any good compiler
219+
* will remove any writes that aren't used. */
220+
for (i = 0; i < N_TX_INPUTS; i++) {
221+
secure_erase(&sender_seckeys[i], sizeof(sender_seckeys[i]));
222+
}
223+
}
224+
225+
/*** Receiving ***/
226+
{
227+
/*** Transaction data ***
228+
*
229+
* Here we create a few global variables to represent the transaction data:
230+
*
231+
* 1. The transaction inputs, `tx_input_ptrs`
232+
* 2. The transaction outputs, `tx_output_ptrs`
233+
*
234+
* These will be used to demonstrate scanning as a full node and scanning as a light client.
235+
*/
236+
const secp256k1_xonly_pubkey *tx_input_ptrs[N_TX_INPUTS];
237+
const secp256k1_xonly_pubkey *tx_output_ptrs[N_TX_OUTPUTS];
238+
unsigned char light_client_data33[33];
239+
240+
for (i = 0; i < N_TX_INPUTS; i++) {
241+
tx_input_ptrs[i] = &tx_inputs[i];
242+
}
243+
for (i = 0; i < N_TX_OUTPUTS; i++) {
244+
tx_output_ptrs[i] = &tx_outputs[i];
245+
}
246+
247+
/*** Scanning with labels as a full node (Bob) ***
248+
*
249+
* Since Bob has access to the full transaction, scanning is simple:
250+
*
251+
* 1. Collect the relevant data from the transaction inputs and call
252+
* `secp256k1_silentpayments_recipient_public_data_create`
253+
* 2. Call `secp256k1_silentpayments_recipient_scan_outputs`
254+
*
255+
* Additionally, since Bob has access to the full transaction outputs when scanning its easy for him
256+
* to scan with labels, as demonstrated below. For efficient scanning, Bob keeps a cache of
257+
* every label he has previously used and uses a callback to check if a potential label exists
258+
* in his cache. Since the labels are created using an incremental integer `m`, if Bob ever
259+
* forgets how many labels he has previously used, he can pregenerate a large number of
260+
* labels (e.g. 0..100_000) and use that while scanning.
261+
*/
262+
{
263+
secp256k1_silentpayments_found_output found_outputs[N_TX_OUTPUTS];
264+
secp256k1_silentpayments_found_output *found_output_ptrs[N_TX_OUTPUTS];
265+
secp256k1_silentpayments_public_data public_data;
266+
secp256k1_pubkey spend_pubkey;
267+
size_t n_found_outputs;
268+
unsigned int m = 1;
269+
struct labels_cache labels_cache;
270+
271+
for (i = 0; i < N_TX_OUTPUTS; i++) {
272+
found_output_ptrs[i] = &found_outputs[i];
273+
}
274+
275+
/* In this contrived example, our label context needs the secp256k1 context because our lookup function
276+
* is calling `secp256k1_ec_pubkey_cmp`. In practice, this context can be anything the lookup function needs.
277+
*/
278+
labels_cache.ctx = ctx;
279+
280+
/* Load Bob's spend public key */
281+
ret = secp256k1_ec_pubkey_parse(ctx, &spend_pubkey, bob_spend_pubkey, 33);
282+
283+
/* Add an entry to the cache. This implies Bob has previously called `secp256k1_silentpayments_recipient_create_labelled_spend_pubkey`
284+
* and used the labelled spend pubkey to encode a labelled silent payments address.
285+
*/
286+
ret = secp256k1_silentpayments_recipient_create_label_tweak(ctx,
287+
&labels_cache.entries[0].label,
288+
labels_cache.entries[0].label_tweak,
289+
bob_scan_key,
290+
m
291+
);
292+
assert(ret);
293+
labels_cache.entries_used = 1;
294+
295+
/* Bob collects the data from the transaction inputs and creates a `secp256k1_silentpayments_public_data` object.
296+
* He uses this for his own scanning and also serializes the `public_data` object to send to light clients. We will
297+
* use this later for Carol, who is scanning as a light client. Note, anyone can create and vend these `public_data`
298+
* objecs, i.e. you don't need to be a silent payments wallet, just someone interested in vending this data to light
299+
* clients, e.g. a wallet service provider. In our example, Bob is scanning for himself but also sharing this data
300+
* with light clients.
301+
*/
302+
ret = secp256k1_silentpayments_recipient_public_data_create(ctx,
303+
&public_data,
304+
smallest_outpoint,
305+
tx_input_ptrs, N_TX_INPUTS,
306+
NULL, 0 /* null because no eligible plain pubkey inputs were found in the tx */
307+
);
308+
assert(ret);
309+
/* Save the `public_data` output. This combines the `input_hash` scalar and public key sum by multiplying `input_hash * A_sum`.
310+
* The output is then saved as a 33 byte compressed key. Storing it this way saves 32 bytes for the light client because
311+
* now it can be send as a 33 byte compressed public key instead of 33 bytes for A_sum and 32 bytes for input_hash.
312+
*/
313+
ret = secp256k1_silentpayments_recipient_public_data_serialize(ctx, light_client_data33, &public_data);
314+
assert(ret);
315+
316+
/* Scan the transaction */
317+
n_found_outputs = 0;
318+
ret = secp256k1_silentpayments_recipient_scan_outputs(ctx,
319+
found_output_ptrs, &n_found_outputs,
320+
tx_output_ptrs, N_TX_OUTPUTS,
321+
bob_scan_key,
322+
&public_data,
323+
&spend_pubkey,
324+
label_lookup, &labels_cache /* NULL, NULL if scanning without labels */
325+
);
326+
assert(n_found_outputs == 1);
327+
printf("\n");
328+
printf("Bob found the following outputs: \n");
329+
for (i = 0; i < n_found_outputs; i++) {
330+
printf(" ");
331+
secp256k1_xonly_pubkey_serialize(ctx, xonly_print, &found_outputs[i].output);
332+
print_hex(xonly_print, sizeof(xonly_print));
333+
}
334+
}
335+
336+
/*** Scanning as a light client (Carol) ***
337+
*
338+
* Being a light client, Carol likely does not have access to the transaction outputs. This
339+
* means she will need to first generate an output, check if it exists in the UTXO set (e.g.
340+
* BIP158 or some other means of querying) and only proceed to check the next output (by
341+
* incrementing `k`) if the first output exists.
342+
*
343+
* For the transaction inputs, she needs the 33 byte compressed public key which is `input_hash * A_sum`.
344+
*/
345+
{
346+
/* In practice, Carol wouldn't know the number of outputs ahead of time but we are cheating here
347+
* to keep the example simple.
348+
*/
349+
unsigned char ser_found_outputs[2][32];
350+
unsigned char shared_secret[33];
351+
secp256k1_pubkey spend_pubkey;
352+
secp256k1_silentpayments_public_data public_data;
353+
size_t n_found_outputs;
354+
355+
/* Load Carol's spend public key */
356+
ret = secp256k1_ec_pubkey_parse(ctx, &spend_pubkey, carol_address[1], 33);
357+
assert(ret);
358+
359+
/* Scan, one output at a time, using the light client data from earlier */
360+
ret = secp256k1_silentpayments_recipient_public_data_parse(ctx, &public_data, light_client_data33);
361+
assert(ret);
362+
ret = secp256k1_silentpayments_recipient_create_shared_secret(ctx,
363+
shared_secret,
364+
carol_scan_key,
365+
&public_data
366+
);
367+
assert(ret);
368+
n_found_outputs = 0;
369+
{
370+
int found = 0;
371+
size_t k = 0;
372+
secp256k1_xonly_pubkey potential_output;
373+
374+
while(1) {
375+
376+
ret = secp256k1_silentpayments_recipient_create_output_pubkey(ctx,
377+
&potential_output,
378+
shared_secret,
379+
&spend_pubkey,
380+
k
381+
);
382+
assert(ret);
383+
/* At this point, we check that the utxo exists with a light client protocol.
384+
* For this example, we'll just iterate through the list of transaction outputs
385+
*/
386+
found = 0;
387+
for (i = 0; i < N_TX_OUTPUTS; i++) {
388+
if (secp256k1_xonly_pubkey_cmp(ctx, &potential_output, &tx_outputs[i]) == 0) {
389+
secp256k1_xonly_pubkey_serialize(ctx, ser_found_outputs[n_found_outputs], &potential_output);
390+
/* If found, create a new output with k++ and check again */
391+
found = 1;
392+
n_found_outputs++;
393+
k++;
394+
break;
395+
}
396+
}
397+
/* If we generate an output and it does not exist in the UTXO set,
398+
* we are done scanning this transaction */
399+
if (!found) {
400+
break;
401+
}
402+
}
403+
}
404+
405+
printf("\n");
406+
printf("Carol found the following outputs: \n");
407+
for (i = 0; i < n_found_outputs; i++) {
408+
printf(" ");
409+
print_hex(ser_found_outputs[i], 32);
410+
}
411+
}
412+
}
413+
414+
/* This will clear everything from the context and free the memory */
415+
secp256k1_context_destroy(ctx);
416+
return 0;
417+
}

0 commit comments

Comments
 (0)