Bitcoin Core 31.99.0
P2P Digital Currency
tests_silentpayments_generate.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2
3"""
4A script to convert BIP352 test vectors from JSON to a C header.
5
6Usage:
7
8 ./tools/tests_silentpayments_generate.py src/modules/silentpayments/bip352_send_and_receive_test_vectors.json > ./src/modules/silentpayments/vectors.h
9"""
10
11import hashlib
12import json
13import sys
14
15NUMS_H = bytes.fromhex("50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0")
16MAX_INPUTS_PER_TEST_CASE = 3
17MAX_OUTPUTS_PER_TEST_CASE = 2324 # K_max + 1
18MAX_RECIPIENT_ENTRIES_PER_TEST_CASE = 4
19MAX_LABELS_PER_TEST_CASE = 4
20MAX_PERMUTATIONS_PER_SENDING_TEST_CASE = 12
21MAX_RECEIVE_SUBTESTS = 2
22
23def sha256(s):
24 return hashlib.sha256(s).digest()
25
26def smallest_outpoint(outpoints):
27 serialized_outpoints = [bytes.fromhex(txid)[::-1] + n.to_bytes(4, 'little') for txid, n in outpoints]
28 return sorted(serialized_outpoints)[0]
29
30def is_p2tr(s): # OP_1 OP_PUSHBYTES_32 <32 bytes>
31 return (len(s) == 34) and (s[0] == 0x51) and (s[1] == 0x20)
32
33def get_pubkey_from_input(input_data, pubkey_hex):
34 """Extract the correct pubkey for an input, handling NUMS_H filtering and format conversion"""
35 spk = bytes.fromhex(input_data['prevout']['scriptPubKey']['hex'])
36 pubkey = bytes.fromhex(pubkey_hex)
37
38 if is_p2tr(spk): # taproot input
39 # Check for NUMS_H in witness (should be skipped)
40 witness = bytes.fromhex(input_data.get('txinwitness', ''))
41 # Parse witness stack
42 witness_stack = []
43 num_witness_items = 0
44 if len(witness) > 0:
45 num_witness_items = witness[0]
46 witness = witness[1:]
47 for i in range(num_witness_items):
48 item_len = witness[0]
49 witness_stack.append(witness[1:item_len+1])
50 witness = witness[item_len+1:]
51
52 # Check for script-path spend with NUMS_H
53 if len(witness_stack) > 1 and witness_stack[-1][0] == 0x50:
54 witness_stack.pop()
55 if len(witness_stack) > 1: # script-path spend?
56 control_block = witness_stack[-1]
57 internal_key = control_block[1:33]
58 if internal_key == NUMS_H: # skip
59 return b''
60
61 # Convert to x-only (32 bytes) for taproot
62 if len(pubkey) == 33:
63 pubkey = pubkey[1:] # Remove prefix byte
64 return pubkey
65 else: # regular input - use full compressed pubkey (33 bytes)
66 return pubkey
67
69 assert len(hex) % 2 == 0
70 if hex == "":
71 return "{0x00}"
72 s = ',0x'.join(a + b for a, b in zip(hex[::2], hex[1::2]))
73 return "{0x" + s + "}"
74
75def maybe_gen_comment(comment):
76 if comment:
77 return f" /* {comment} */"
78 else:
79 return ""
80
81def gen_key_material(keys, comment=None, prepend_count=False):
82 assert len(keys) <= MAX_INPUTS_PER_TEST_CASE
83 out = ""
84 if prepend_count:
85 out += f" {len(keys)},\n"
86 out += f" {{{maybe_gen_comment(comment)}\n"
87 for k in keys:
88 out += f" {gen_byte_array(k)},\n"
89 if not keys:
90 out += ' "",\n'
91 out += " },\n"
92 return out
93
95 assert len(recipients) <= MAX_RECIPIENT_ENTRIES_PER_TEST_CASE
96 num_outputs = sum([r.get('count', 1) for r in recipients])
97 assert num_outputs <= MAX_OUTPUTS_PER_TEST_CASE
98 out = f" {num_outputs},\n"
99 out += f" {len(recipients)},\n"
100 out += " { /* recipient pubkey entries (address data) */\n"
101 for i in range(MAX_RECIPIENT_ENTRIES_PER_TEST_CASE):
102 out += " {\n"
103 if i < len(recipients):
104 # Use the scan_pubkey and spend_pubkey directly from the recipient
105 scan_pubkey = bytes.fromhex(recipients[i]['scan_pub_key'])
106 spend_pubkey = bytes.fromhex(recipients[i]['spend_pub_key'])
107
108 out += f" {gen_byte_array(scan_pubkey.hex())},\n"
109 out += f" {gen_byte_array(spend_pubkey.hex())},\n"
110 out += f" {recipients[i].get('count', 1)},\n"
111 else:
112 out += ' "",\n'
113 out += ' "",\n'
114 out += ' 0,\n'
115 out += " },\n"
116 out += " },\n"
117 return out
118
119def gen_sending_outputs(output_sets, comment=None, prepend_count=False):
120 assert len(output_sets) <= MAX_PERMUTATIONS_PER_SENDING_TEST_CASE
121 out = ""
122 if prepend_count:
123 out += f" {len(output_sets)},\n"
124 out += f" {len(output_sets[0])},\n"
125 out += f" {{{maybe_gen_comment(comment)}\n"
126 for o in output_sets:
127 out += gen_outputs(outputs=o, prepend_count=False, indent=12)
128 if not output_sets:
129 out += gen_outputs(comment=None, outputs=[], prepend_count=False, indent=12)
130 out += " },\n"
131 return out
132
133def gen_outputs(outputs, comment=None, prepend_count=False, indent=8):
134 out = ""
135 spaces = indent * " "
136 if prepend_count:
137 out += spaces + f"{len(outputs)},\n"
138 out += f" {{{maybe_gen_comment(comment)}\n"
139 for o in outputs:
140 out += spaces + f" {gen_byte_array(o)},\n"
141 if not outputs:
142 out += spaces + ' "",\n'
143 out += spaces + "},\n"
144 return out
145
146def gen_labels(labels):
147 assert len(labels) <= MAX_LABELS_PER_TEST_CASE
148 out = " /* labels */\n"
149 out += f" {len(labels)}, {{"
150 for i in range(MAX_LABELS_PER_TEST_CASE):
151 if i < len(labels):
152 out += f"{labels[i]}, "
153 else:
154 out += "0xffffffff, "
155 out += "},\n"
156 return out
157
158def gen_preamble(test_vectors):
159 out = "/* Note: this file was autogenerated using tests_silentpayments_generate.py. Do not edit. */\n"
160 out += f"""
161#include <stddef.h>
162
163#define MAX_INPUTS_PER_TEST_CASE {MAX_INPUTS_PER_TEST_CASE}
164#define MAX_OUTPUTS_PER_TEST_CASE {MAX_OUTPUTS_PER_TEST_CASE}
165#define MAX_RECIPIENT_ENTRIES_PER_TEST_CASE {MAX_RECIPIENT_ENTRIES_PER_TEST_CASE}
166#define MAX_LABELS_PER_TEST_CASE {MAX_LABELS_PER_TEST_CASE}
167#define MAX_PERMUTATIONS_PER_SENDING_TEST_CASE {MAX_PERMUTATIONS_PER_SENDING_TEST_CASE}
168#define MAX_RECEIVE_SUBTESTS {MAX_RECEIVE_SUBTESTS}
169
170struct bip352_recipient_addressdata {{
171 unsigned char scan_pubkey[33];
172 unsigned char spend_pubkey[33];
173 size_t count;
174}};
175
176struct bip352_receive_subtest {{
177 /* Given recipient data */
178 unsigned char scan_seckey[32];
179 unsigned char spend_seckey[32];
180 size_t num_to_scan_outputs;
181 unsigned char to_scan_outputs[MAX_OUTPUTS_PER_TEST_CASE][32];
182 size_t num_labels;
183 unsigned int label_integers[MAX_LABELS_PER_TEST_CASE];
184
185 /* Expected recipient data */
186 size_t full_check; /* 1..detailed check against tweaks and signatures, 0..only check found outputs count */
187 size_t num_found_output_pubkeys;
188 unsigned char found_output_pubkeys[MAX_OUTPUTS_PER_TEST_CASE][32];
189 unsigned char found_seckey_tweaks[MAX_OUTPUTS_PER_TEST_CASE][32];
190 unsigned char found_signatures[MAX_OUTPUTS_PER_TEST_CASE][64];
191}};
192
193struct bip352_test_vector {{
194 /* Inputs (private keys / public keys + smallest outpoint) */
195 size_t num_plain_inputs;
196 unsigned char plain_seckeys[MAX_INPUTS_PER_TEST_CASE][32];
197 unsigned char plain_pubkeys[MAX_INPUTS_PER_TEST_CASE][33];
198 size_t num_taproot_inputs;
199 unsigned char taproot_seckeys[MAX_INPUTS_PER_TEST_CASE][32];
200 unsigned char xonly_pubkeys[MAX_INPUTS_PER_TEST_CASE][32];
201 unsigned char outpoint_smallest[36];
202
203 /* Given sender data (pubkeys encoded per output address to send to) */
204 size_t num_outputs;
205 size_t num_recipient_entries;
206 struct bip352_recipient_addressdata recipient_pubkeys[MAX_RECIPIENT_ENTRIES_PER_TEST_CASE];
207
208 /* Expected sender data */
209 size_t num_output_sets;
210 size_t num_recipient_outputs;
211 unsigned char recipient_outputs[MAX_PERMUTATIONS_PER_SENDING_TEST_CASE][MAX_OUTPUTS_PER_TEST_CASE][32];
212
213 size_t num_receive_subtests;
214 struct bip352_receive_subtest receive_subtests[MAX_RECEIVE_SUBTESTS];
215}};
216"""
217 return out
218
219def gen_test_vectors(test_vectors):
220 out = f"#define SECP256K1_SILENTPAYMENTS_NUMBER_TESTVECTORS {len(test_vectors)}\n\n"
221 out += "static const struct bip352_test_vector bip352_test_vectors[SECP256K1_SILENTPAYMENTS_NUMBER_TESTVECTORS] = {\n"
222 for test_i, test_vector in enumerate(test_vectors):
223 # determine input private and public keys, grouped into plain and taproot/x-only
224 input_plain_seckeys = []
225 input_taproot_seckeys = []
226 input_plain_pubkeys = []
227 input_xonly_pubkeys = []
228 outpoints = []
229
230 # To keep this script simple and the generated test vectors file (vectors.h) small, we take
231 # use of the fact that currently all BIP352 test vectors only have a single sending subtest.
232 # If that ever changes in the future, the following assertion would fail and this script,
233 # its emitted data structures and the test code consuming the generated header would need to
234 # be extended, in a similar way to how it's already done for the receiving subtests.
235 assert len(test_vector['sending']) == 1
236
237 pubkey_index = 0
238 input_pubkeys_hex = test_vector['sending'][0]['expected']['input_pub_keys']
239
240 for vec in test_vector['sending'][0]['given']['vin']:
241 outpoints.append((vec['txid'], vec['vout']))
242
243 if pubkey_index < len(input_pubkeys_hex):
244 pubkey = get_pubkey_from_input(vec, input_pubkeys_hex[pubkey_index])
245 if len(pubkey) == 33: # regular input
246 input_plain_seckeys.append(vec['private_key'])
247 input_plain_pubkeys.append(pubkey.hex())
248 pubkey_index += 1
249 elif len(pubkey) == 32: # taproot input
250 input_taproot_seckeys.append(vec['private_key'])
251 input_xonly_pubkeys.append(pubkey.hex())
252 pubkey_index += 1
253 # len(pubkey) == 0, it's a NUMS_H input - skip without incrementing
254
255 out += f" /* ----- {test_vector['comment']} ({test_i + 1}) ----- */\n"
256 out += " {\n"
257
258 outpoint_L = smallest_outpoint(outpoints).hex()
259 out += gen_key_material(input_plain_seckeys, "input plain seckeys", prepend_count=True)
260 out += gen_key_material(input_plain_pubkeys, "input plain pubkeys")
261 out += gen_key_material(input_taproot_seckeys, "input taproot seckeys", prepend_count=True)
262 out += gen_key_material(input_xonly_pubkeys, "input x-only pubkeys")
263 out += " /* smallest outpoint */\n"
264 out += f" {gen_byte_array(outpoint_L)},\n"
265
266 # emit recipient pubkeys (address data)
267 out += gen_recipient_addr_material(test_vector['sending'][0]['given']['recipients'])
268 # emit recipient outputs
269 out += gen_sending_outputs(test_vector['sending'][0]['expected']['outputs'], "recipient outputs", prepend_count=True)
270
271 assert len(test_vector['receiving']) <= MAX_RECEIVE_SUBTESTS
272 out += f" {len(test_vector['receiving'])}, /* number of receive subtests */\n"
273 out += " {\n"
274 for recv_test in test_vector['receiving']:
275 out += " {\n"
276 # emit recipient scan/spend seckeys
277 recv_test_given = recv_test['given']
278 recv_test_expected = recv_test['expected']
279 out += " /* recipient data (scan and spend seckeys) */\n"
280 out += f" {gen_byte_array(recv_test_given['key_material']['scan_priv_key'])},\n"
281 out += f" {gen_byte_array(recv_test_given['key_material']['spend_priv_key'])},\n"
282
283 # emit recipient to-scan outputs, labels and expected-found outputs
284 out += gen_outputs(recv_test_given['outputs'], "outputs to scan", prepend_count=True)
285 out += gen_labels(recv_test_given['labels'])
286 full_check = 'outputs' in recv_test_expected
287 out += f" {int(full_check)}, /* full check? */\n"
288 if full_check:
289 expected_pubkeys = [o['pub_key'] for o in recv_test_expected['outputs']]
290 expected_tweaks = [o['priv_key_tweak'] for o in recv_test_expected['outputs']]
291 expected_signatures = [o['signature'] for o in recv_test_expected['outputs']]
292 out += " /* expected output data (pubkeys and seckey tweaks) */\n"
293 else:
294 expected_pubkeys = []
295 expected_tweaks = []
296 expected_signatures = []
297 out += " /* expected number of outputs */\n"
298 out += f" {recv_test_expected['n_outputs']},\n"
299
300 out += gen_outputs(expected_pubkeys, prepend_count=full_check)
301 out += gen_outputs(expected_tweaks)
302 out += gen_outputs(expected_signatures)
303 out += " },\n"
304 for _ in range(len(test_vector['receiving']), MAX_RECEIVE_SUBTESTS):
305 out += ' { "", "", 0, {"",}, 0, {0,}, 0, 0, {"",}, {"",}, {"",} }, /* unused receive subtest entry */\n'
306 out += " },\n"
307 out += " },\n\n"
308 out += "};\n"
309 return out
310
311def main():
312 if len(sys.argv) != 2:
313 print(__doc__)
314 sys.exit(1)
315
316 filename_input = sys.argv[1]
317 with open(filename_input) as f:
318 test_vectors = json.load(f)
319
320 print(gen_preamble(test_vectors))
321 print(gen_test_vectors(test_vectors), end="")
322
323if __name__ == "__main__":
324 main()
volatile double sum
Definition: examples.cpp:10
def gen_sending_outputs(output_sets, comment=None, prepend_count=False)
def gen_outputs(outputs, comment=None, prepend_count=False, indent=8)
def gen_key_material(keys, comment=None, prepend_count=False)
def get_pubkey_from_input(input_data, pubkey_hex)