@@ -159,3 +159,295 @@ impl TryFrom<&str> for SilentPaymentCode {
159159 } )
160160 }
161161}
162+
163+ #[ cfg( test) ]
164+ mod test {
165+ use std:: str:: FromStr ;
166+
167+ use bitcoin:: {
168+ hex:: DisplayHex ,
169+ network:: Network :: Bitcoin ,
170+ secp256k1:: { PublicKey , Scalar , SecretKey } ,
171+ ScriptBuf ,
172+ } ;
173+
174+ use once_cell:: sync:: Lazy ;
175+ use serde:: Deserialize ;
176+
177+ use super :: SilentPaymentCode ;
178+
179+ const ENCODING_TEST_VECTORS : & str = r#"
180+ [
181+ {
182+ "index": 0,
183+ "comment": "successfully parse mainnet code",
184+ "input": "sp1qq0u4yswlkqx36shz7j8mwt335p4el5txc8tt6yny3dqewlw4rwdqkqewtzh728u7mzkne3uf0a35mzqlm0jf4q2kgc5aakq4d04a9l734ujpez3s",
185+ "error": null,
186+ "output": {
187+ "version": 0,
188+ "spend": "032e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af",
189+ "scan": "03f95241dfb00d1d42e2f48fb72e31a06b9fd166c1d6bd12648b41977dd51b9a0b",
190+ "network": "bitcoin"
191+ }
192+ },
193+ {
194+ "index": 1,
195+ "comment": "successfully parse testnet code",
196+ "input": "tsp1qq0u4yswlkqx36shz7j8mwt335p4el5txc8tt6yny3dqewlw4rwdqkqewtzh728u7mzkne3uf0a35mzqlm0jf4q2kgc5aakq4d04a9l734uxwehmt",
197+ "error": null,
198+ "output": {
199+ "version": 0,
200+ "spend": "032e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af",
201+ "scan": "03f95241dfb00d1d42e2f48fb72e31a06b9fd166c1d6bd12648b41977dd51b9a0b",
202+ "network": "testnet"
203+ }
204+ },
205+ {
206+ "index": 2,
207+ "comment": "successfully parse regtest code",
208+ "input": "sprt1qq0u4yswlkqx36shz7j8mwt335p4el5txc8tt6yny3dqewlw4rwdqkqewtzh728u7mzkne3uf0a35mzqlm0jf4q2kgc5aakq4d04a9l734u5ddn6e",
209+ "error": null,
210+ "output": {
211+ "version": 0,
212+ "spend": "032e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af",
213+ "scan": "03f95241dfb00d1d42e2f48fb72e31a06b9fd166c1d6bd12648b41977dd51b9a0b",
214+ "network": "regtest"
215+ }
216+ },
217+ {
218+ "index": 3,
219+ "comment": "fail to parse mainnet code with invalid spend key",
220+ "input": "sp19q0u4yswlkqx36shz7j8mwt335p4el5txc8tt6yny3dqewlw4rwdqhpj0pezd9rdd9lvdcxz54gcwgph24j020xzu0nxvx6a0gr9u39ce35yz3n50",
221+ "error": "malformed public key",
222+ "output": null
223+ },
224+ {
225+ "index": 4,
226+ "comment": "fail to parse mainnet code with invalid scan key",
227+ "input": "sp19m66lz4dwzqcqe5vjndhawxr7x40cv8mjgxgzjuylmujqzhnelkgs7qle2fqalvqdr4pw9ay0kuhrrgrtnlgkdswkh5fxfz6pja7a2xu6pvv50l9w",
228+ "error": "malformed public key",
229+ "output": null
230+ },
231+ {
232+ "index": 5,
233+ "comment": "fail to parse code with wrong hrp",
234+ "input": "bc1qq0u4yswlkqx36shz7j8mwt335p4el5txc8tt6yny3dqewlw4rwdqkqle2fqalvqdr4pw9ay0kuhrrgrtnlgkdswkh5fxfz6pja7a2xu6pvgqultw",
235+ "error": "unknown hrp: bc",
236+ "output": null
237+ },
238+ {
239+ "index": 6,
240+ "comment": "successfully parse v5 code with data portion above 66 bytes",
241+ "input": "sp19q0u4yswlkqx36shz7j8mwt335p4el5txc8tt6yny3dqewlw4rwdqkqewtzh728u7mzkne3uf0a35mzqlm0jf4q2kgc5aakq4d04a9l734ug7pexw6tsec0xextt0qextmudpaenmxyj688a48326cerg99n62kca3jutrxw3efjdytad3dyreupeugcdusgazwad388e6zfcu76056zzuz",
242+ "error": null,
243+ "output": {
244+ "version": 0,
245+ "spend": "032e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af",
246+ "scan": "03f95241dfb00d1d42e2f48fb72e31a06b9fd166c1d6bd12648b41977dd51b9a0b",
247+ "network": "bitcoin"
248+ }
249+ },
250+ {
251+ "index": 7,
252+ "comment": "fail to parse code with v31",
253+ "input": "sp1lq0u4yswlkqx36shz7j8mwt335p4el5txc8tt6yny3dqewlw4rwdqkqle2fqalvqdr4pw9ay0kuhrrgrtnlgkdswkh5fxfz6pja7a2xu6pvccpqt4",
254+ "error": "version 31 codes are not backward compatible",
255+ "output": null
256+ },
257+ {
258+ "index": 8,
259+ "comment": "fail to parse v0 mainnet code with invalid data size",
260+ "error": "payload length does not match version spec",
261+ "input": "sp1qq0u4yswlkqx36shz7j8mwt335p4el5txc8tt6yny3dqewlw4rwdqkqle2fqalvqdr4pw9ay0kuhrrgrtnlgkdswkh5fxfz6pja7a2xu6pdcn3286hvtm4jmfp67nnfxfk2ah8wy9t93u5dxs7qd56agnxuujkh27y86v3jlyzp65r47zumz4w4wje869xpaym6qzhxztcef7s4qfrzvyk2",
262+ "output": null
263+ },
264+ {
265+ "index": 9,
266+ "comment": "fail to parse v5 mainnet code with short data size",
267+ "error": "payload length does not match version spec",
268+ "input": "sp19q0u4yswlkqx36shz7j8mwt335p4el5txc8tt6yny3dqewlw4rwdqknjxnvv",
269+ "output": null
270+ },
271+ {
272+ "index": 10,
273+ "comment": "fail to parse mainnet code with invalid checksum",
274+ "input": "sp1qq0u4yswlkqx36shz7j8mwt335p4el5txc8tt6yny3dqewlw4rwdqkqewtzh728u7mzkne3uf0a35mzqlm0jf4q2kgc5aakq4d04a9l734ujptzes",
275+ "error": "invalid checksum",
276+ "output": null
277+ }
278+ ]
279+ "# ;
280+
281+ static ENCODING_TEST_CASES : Lazy < Vec < EncodingTestCase > > =
282+ Lazy :: new ( || serde_json:: from_str ( ENCODING_TEST_VECTORS ) . expect ( "Invalid JSON" ) ) ;
283+
284+ const SCAN_SK : & str = "57f0148f94d13095cfda539d0da0d1541304b678d8b36e243980aab4e1b7cead" ;
285+ const SCAN_PK : & str = "03f95241dfb00d1d42e2f48fb72e31a06b9fd166c1d6bd12648b41977dd51b9a0b" ;
286+ const SPEND_PK : & str = "032e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af" ;
287+
288+ #[ derive( Debug , Deserialize ) ]
289+ struct EncodingTestCase {
290+ pub index : usize ,
291+ #[ serde( alias = "comment" ) ]
292+ pub _comment : String ,
293+ pub input : String ,
294+ pub output : Option < TestOutput > ,
295+ pub error : Option < String > ,
296+ }
297+
298+ #[ derive( Debug , Deserialize ) ]
299+ struct TestOutput {
300+ #[ serde( alias = "version" ) ]
301+ pub _version : u8 ,
302+ pub scan : String ,
303+ pub spend : String ,
304+ pub network : String ,
305+ }
306+
307+ fn assert_encoding ( test_index : usize ) {
308+ let test_case = & ENCODING_TEST_CASES [ test_index] ;
309+ assert_eq ! ( test_case. index, test_index) ;
310+ if test_case. error . is_some ( ) && test_case. output . is_none ( ) {
311+ let expected_error = test_case. error . clone ( ) . expect ( "already checked is some" ) ;
312+ let output = SilentPaymentCode :: try_from ( test_case. input . as_str ( ) ) ;
313+ assert ! ( output. is_err( ) ) ;
314+ assert_eq ! ( expected_error, output. unwrap_err( ) . to_string( ) ) ;
315+ } else if test_case. output . is_some ( ) {
316+ let TestOutput {
317+ scan,
318+ spend,
319+ network,
320+ ..
321+ } = test_case. output . as_ref ( ) . expect ( "already checked is some" ) ;
322+ let sp_code = SilentPaymentCode :: try_from ( test_case. input . as_str ( ) ) . unwrap ( ) ;
323+ assert_eq ! ( scan, & sp_code. scan. to_string( ) ) ;
324+ assert_eq ! ( spend, & sp_code. spend. to_string( ) ) ;
325+ assert_eq ! ( network, & sp_code. network. to_string( ) ) ;
326+ // Check roundtrip
327+ if sp_code. version ( ) == 0 {
328+ assert_eq ! ( test_case. input, sp_code. to_string( ) ) ;
329+ }
330+ } else {
331+ panic ! ( "test case definition is wrong" ) ;
332+ }
333+ }
334+
335+ fn scan_n_spend_pks ( ) -> ( PublicKey , PublicKey ) {
336+ let scan = PublicKey :: from_str ( SCAN_PK ) . expect ( "reading from constant" ) ;
337+ let spend = PublicKey :: from_str ( SPEND_PK ) . expect ( "reading from constant" ) ;
338+
339+ ( scan, spend)
340+ }
341+
342+ fn scan_sk ( ) -> SecretKey {
343+ SecretKey :: from_str ( SCAN_SK ) . expect ( "reading from constant" )
344+ }
345+
346+ #[ test]
347+ fn test_0_successfully_parse_mainnet_code ( ) {
348+ assert_encoding ( 0 ) ;
349+ }
350+
351+ #[ test]
352+ fn test_1_successfully_parse_testnet_code ( ) {
353+ assert_encoding ( 1 ) ;
354+ }
355+
356+ #[ test]
357+ fn test_2_successfully_parse_regtest_code ( ) {
358+ assert_encoding ( 2 ) ;
359+ }
360+
361+ #[ test]
362+ fn test_3_fail_to_parse_mainnet_code_with_invalid_spend_key ( ) {
363+ assert_encoding ( 3 ) ;
364+ }
365+
366+ #[ test]
367+ fn test_4_fail_to_parse_mainnet_code_with_invalid_scan_key ( ) {
368+ assert_encoding ( 4 ) ;
369+ }
370+
371+ #[ test]
372+ fn test_5_fail_to_parse_code_with_wrong_hrp ( ) {
373+ assert_encoding ( 5 ) ;
374+ }
375+
376+ #[ test]
377+ fn test_6_successfully_parse_higher_version_code_with_data_portion_above_66_bytes ( ) {
378+ assert_encoding ( 6 ) ;
379+ }
380+
381+ #[ test]
382+ fn test_7_fail_to_parse_code_with_v31 ( ) {
383+ assert_encoding ( 7 ) ;
384+ }
385+
386+ #[ test]
387+ fn test_8_fail_to_parse_v0_mainnet_code_with_invalid_data_size ( ) {
388+ assert_encoding ( 8 ) ;
389+ }
390+
391+ #[ test]
392+ fn fail_to_parse_v5_mainnet_code_with_short_data_size ( ) {
393+ assert_encoding ( 9 ) ;
394+ }
395+
396+ #[ test]
397+ fn fail_to_parse_mainnet_code_with_invalid_checksum ( ) {
398+ assert_encoding ( 10 ) ;
399+ }
400+
401+ #[ test]
402+ fn get_label ( ) {
403+ let expected_label: & str =
404+ "6f3cec525b194328307cb10e83c559e29f946cd47e4a9a92eaf55967d9d22cfe" ;
405+ let output_label = SilentPaymentCode :: get_label ( scan_sk ( ) , 4 ) ;
406+ assert_eq ! (
407+ expected_label,
408+ output_label. to_be_bytes( ) . as_hex( ) . to_string( )
409+ )
410+ }
411+
412+ #[ test]
413+ fn get_labelled_sp_code ( ) {
414+ let expected_labeled_code: & str = "sp1qq0u4yswlkqx36shz7j8mwt335p4el5txc8tt6yny3dqewlw4rwdqkq57x0y7k4rs5zkkd7pmumhkdadq7du5t7qf7nkyy6rfzp3jd697cg9zhz0x" ;
415+ let expected_labeled_spend_key: & str =
416+ "029e33c9eb5470a0ad66f83be6ef66f5a0f37945f809f4ec426869106326e8bec2" ;
417+
418+ let ( scan, spend) = scan_n_spend_pks ( ) ;
419+ let sp_code = SilentPaymentCode :: new_v0 ( scan, spend, Bitcoin ) ;
420+ let label = SilentPaymentCode :: get_label ( scan_sk ( ) , 4 ) ;
421+ let output_labelled_code = sp_code. add_label ( label) . expect ( "should not err" ) ;
422+ assert_eq ! ( expected_labeled_code, output_labelled_code. to_string( ) ) ;
423+ assert_eq ! (
424+ expected_labeled_spend_key,
425+ output_labelled_code. spend. to_string( )
426+ ) ;
427+ assert_eq ! ( SCAN_PK , output_labelled_code. scan. to_string( ) )
428+ }
429+
430+ #[ test]
431+ fn crafted_labeling_failure_case ( ) {
432+ let ( scan, spend) = scan_n_spend_pks ( ) ;
433+ // Use the scan key as spend (because we only have the sk of scan)
434+ let sp_code = SilentPaymentCode :: new_v0 ( spend, scan, Bitcoin ) ;
435+ let negated_scan_scalar = Scalar :: from ( scan_sk ( ) . negate ( ) ) ;
436+ let output = sp_code. add_label ( negated_scan_scalar) ;
437+ assert ! ( output. is_err( ) ) ;
438+ assert_eq ! ( "bad tweak" , output. unwrap_err( ) . to_string( ) ) ;
439+ }
440+
441+ #[ test]
442+ fn check_placeholder_spk ( ) {
443+ let ( scan, spend) = scan_n_spend_pks ( ) ;
444+ let expected_placeholder_spk: & str =
445+ "5120da3d55f0ecf27a3505ded1ac780c3d77299dc4b253010214ac9f2d1d9b365d09" ;
446+ let sp_code = SilentPaymentCode :: new_v0 ( scan, spend, Bitcoin ) ;
447+ let output_placeholder_spk: ScriptBuf = sp_code. get_placeholder_p2tr_spk ( ) ;
448+ assert_eq ! (
449+ expected_placeholder_spk,
450+ output_placeholder_spk. to_hex_string( )
451+ ) ;
452+ }
453+ }
0 commit comments