|
| 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