payment_request_encoding_benchmark.rs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  1. //! Payment Request Encoding Benchmark
  2. //!
  3. //! Compares NUT-18 (CBOR/base64) vs NUT-26 (Bech32m) encoding formats across
  4. //! various payment request complexities to demonstrate format efficiency tradeoffs.
  5. //!
  6. //! # Format Overview
  7. //!
  8. //! ## NUT-18 (creqA prefix)
  9. //! - **Binary Encoding**: CBOR (Concise Binary Object Representation)
  10. //! - **Text Encoding**: URL-safe base64
  11. //! - **Characteristics**: Compact binary format, case-sensitive
  12. //!
  13. //! ## NUT-26 (CREQB prefix)
  14. //! - **Binary Encoding**: TLV (Type-Length-Value)
  15. //! - **Text Encoding**: Bech32m
  16. //! - **Characteristics**: QR-optimized, case-insensitive, error detection
  17. //!
  18. //! # When to Use Each Format
  19. //!
  20. //! ## Use NUT-26 (CREQB) when:
  21. //! - **Minimal requests** (~5 bytes / 7% smaller for simple payment IDs)
  22. //! - **QR code display** (100% alphanumeric-compatible vs 99%+)
  23. //! - **Error detection is critical** (Bech32m has built-in checksums)
  24. //! - **Case-insensitive parsing** needed (URLs, voice transcription)
  25. //! - **Visual verification** (human-readable structure)
  26. //!
  27. //! ## Use NUT-18 (creqA) when:
  28. //! - **Complex requests** (~13-163 bytes / 16-19% smaller with more data)
  29. //! - **Multiple mints** (~59 bytes / 24% smaller with 4 mints)
  30. //! - **Transport callbacks** (~49 bytes / 19% smaller with 1 transport)
  31. //! - **NUT-10 locking** (~91 bytes / 17% smaller with P2PK)
  32. //! - **Nested structures** (CBOR excels at hierarchical data)
  33. //! - **Bandwidth is constrained** (smaller encoded size)
  34. //!
  35. //! # Benchmark Results Summary
  36. //!
  37. //! | Scenario | NUT-18 Size | NUT-26 Size | Winner | Savings |
  38. //! |----------|-------------|-------------|--------|---------|
  39. //! | Minimal payment | 77 bytes | 72 bytes | NUT-26 | 5 bytes (7%) |
  40. //! | With amount/unit | 81 bytes | 94 bytes | NUT-18 | 13 bytes (16%) |
  41. //! | 4 mints | 249 bytes | 308 bytes | NUT-18 | 59 bytes (24%) |
  42. //! | 1 transport | 253 bytes | 302 bytes | NUT-18 | 49 bytes (19%) |
  43. //! | Complete + P2PK | 529 bytes | 620 bytes | NUT-18 | 91 bytes (17%) |
  44. //! | Very complex | 857 bytes | 1020 bytes | NUT-18 | 163 bytes (19%) |
  45. //!
  46. //! **Key Insight**: NUT-26 is optimal for simple requests, NUT-18 scales better
  47. //! for complex payment requests with multiple mints, transports, or NUT-10 locks.
  48. use std::str::FromStr;
  49. use cashu::nuts::nut10::Kind;
  50. use cashu::nuts::{CurrencyUnit, Nut10SecretRequest, PaymentRequest, Transport, TransportType};
  51. use cashu::{Amount, MintUrl};
  52. fn main() -> Result<(), Box<dyn std::error::Error>> {
  53. println!("=== NUT-18 vs NUT-26 Format Comparison ===\n");
  54. // Example 1: Minimal payment request
  55. println!("1. Minimal Payment Request:");
  56. minimal_comparison()?;
  57. // Example 2: Payment with amount and unit
  58. println!("\n2. Payment with Amount and Unit:");
  59. amount_unit_comparison()?;
  60. // Example 3: Complex payment with multiple mints
  61. println!("\n3. Complex Payment with Multiple Mints:");
  62. multiple_mints_comparison()?;
  63. // Example 4: Payment with transport
  64. println!("\n4. Payment with Transport:");
  65. transport_comparison()?;
  66. // Example 5: Complete payment with NUT-10 locking
  67. println!("\n5. Complete Payment with NUT-10 P2PK Lock:");
  68. complete_with_nut10_comparison()?;
  69. // Example 6: Very complex payment request
  70. println!("\n6. Very Complex Payment Request:");
  71. very_complex_comparison()?;
  72. // Summary
  73. println!("\n=== Summary ===");
  74. summary();
  75. println!("\n=== Format Comparison Complete ===");
  76. Ok(())
  77. }
  78. fn minimal_comparison() -> Result<(), Box<dyn std::error::Error>> {
  79. let payment_request = PaymentRequest {
  80. payment_id: Some("test123".to_string()),
  81. amount: None,
  82. unit: None,
  83. single_use: None,
  84. mints: Some(vec![MintUrl::from_str("https://mint.example.com")?]),
  85. description: None,
  86. transports: vec![],
  87. nut10: None,
  88. };
  89. compare_formats(&payment_request, "Minimal")?;
  90. Ok(())
  91. }
  92. fn amount_unit_comparison() -> Result<(), Box<dyn std::error::Error>> {
  93. let payment_request = PaymentRequest {
  94. payment_id: Some("pay456".to_string()),
  95. amount: Some(Amount::from(2100)),
  96. unit: Some(CurrencyUnit::Sat),
  97. single_use: None,
  98. mints: Some(vec![MintUrl::from_str("https://mint.example.com")?]),
  99. description: None,
  100. transports: vec![],
  101. nut10: None,
  102. };
  103. compare_formats(&payment_request, "Amount + Unit")?;
  104. Ok(())
  105. }
  106. fn multiple_mints_comparison() -> Result<(), Box<dyn std::error::Error>> {
  107. let payment_request = PaymentRequest {
  108. payment_id: Some("multi789".to_string()),
  109. amount: Some(Amount::from(10000)),
  110. unit: Some(CurrencyUnit::Sat),
  111. single_use: Some(true),
  112. mints: Some(vec![
  113. MintUrl::from_str("https://mint1.example.com")?,
  114. MintUrl::from_str("https://mint2.example.com")?,
  115. MintUrl::from_str("https://mint3.example.com")?,
  116. MintUrl::from_str("https://backup-mint.cashu.space")?,
  117. ]),
  118. description: Some("Payment with multiple mint options".to_string()),
  119. transports: vec![],
  120. nut10: None,
  121. };
  122. compare_formats(&payment_request, "Multiple Mints")?;
  123. Ok(())
  124. }
  125. fn transport_comparison() -> Result<(), Box<dyn std::error::Error>> {
  126. let transport = Transport {
  127. _type: TransportType::HttpPost,
  128. target: "https://api.example.com/cashu/payment/callback".to_string(),
  129. tags: Some(vec![
  130. vec!["method".to_string(), "POST".to_string()],
  131. vec!["auth".to_string(), "bearer".to_string()],
  132. ]),
  133. };
  134. let payment_request = PaymentRequest {
  135. payment_id: Some("transport123".to_string()),
  136. amount: Some(Amount::from(5000)),
  137. unit: Some(CurrencyUnit::Sat),
  138. single_use: Some(true),
  139. mints: Some(vec![MintUrl::from_str("https://mint.example.com")?]),
  140. description: Some("Payment with callback transport".to_string()),
  141. transports: vec![transport],
  142. nut10: None,
  143. };
  144. compare_formats(&payment_request, "With Transport")?;
  145. Ok(())
  146. }
  147. fn complete_with_nut10_comparison() -> Result<(), Box<dyn std::error::Error>> {
  148. let nut10 = Nut10SecretRequest::new(
  149. Kind::P2PK,
  150. "026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198",
  151. Some(vec![
  152. vec!["locktime".to_string(), "1609459200".to_string()],
  153. vec![
  154. "refund".to_string(),
  155. "03a34d1f4e6d1e7f8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2".to_string(),
  156. ],
  157. ]),
  158. );
  159. let transport = Transport {
  160. _type: TransportType::HttpPost,
  161. target: "https://callback.example.com/payment".to_string(),
  162. tags: Some(vec![vec!["priority".to_string(), "high".to_string()]]),
  163. };
  164. let payment_request = PaymentRequest {
  165. payment_id: Some("complete789".to_string()),
  166. amount: Some(Amount::from(5000)),
  167. unit: Some(CurrencyUnit::Sat),
  168. single_use: Some(true),
  169. mints: Some(vec![
  170. MintUrl::from_str("https://mint1.example.com")?,
  171. MintUrl::from_str("https://mint2.example.com")?,
  172. ]),
  173. description: Some("Complete payment with P2PK locking and refund key".to_string()),
  174. transports: vec![transport],
  175. nut10: Some(nut10),
  176. };
  177. compare_formats(&payment_request, "Complete with NUT-10")?;
  178. Ok(())
  179. }
  180. fn very_complex_comparison() -> Result<(), Box<dyn std::error::Error>> {
  181. let nut10 = Nut10SecretRequest::new(
  182. Kind::P2PK,
  183. "026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198",
  184. Some(vec![
  185. vec!["locktime".to_string(), "1609459200".to_string()],
  186. vec![
  187. "refund".to_string(),
  188. "03a34d1f4e6d1e7f8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e".to_string(),
  189. ],
  190. ]),
  191. );
  192. let transport1 = Transport {
  193. _type: TransportType::HttpPost,
  194. target: "https://primary-callback.example.com/payment/webhook".to_string(),
  195. tags: Some(vec![
  196. vec!["priority".to_string(), "high".to_string()],
  197. vec!["timeout".to_string(), "30".to_string()],
  198. ]),
  199. };
  200. let transport2 = Transport {
  201. _type: TransportType::HttpPost,
  202. target: "https://backup-callback.example.com/payment/webhook".to_string(),
  203. tags: Some(vec![
  204. vec!["priority".to_string(), "medium".to_string()],
  205. vec!["timeout".to_string(), "60".to_string()],
  206. ]),
  207. };
  208. let payment_request = PaymentRequest {
  209. payment_id: Some("very_complex_payment_id_12345".to_string()),
  210. amount: Some(Amount::from(21000)),
  211. unit: Some(CurrencyUnit::Sat),
  212. single_use: Some(true),
  213. mints: Some(vec![
  214. MintUrl::from_str("https://primary-mint.cashu.space")?,
  215. MintUrl::from_str("https://secondary-mint.example.com")?,
  216. MintUrl::from_str("https://backup-mint-1.example.org")?,
  217. MintUrl::from_str("https://backup-mint-2.example.net")?,
  218. MintUrl::from_str("https://emergency-mint.example.io")?,
  219. ]),
  220. description: Some("Complex payment with multiple mints and transports".to_string()),
  221. transports: vec![transport1, transport2],
  222. nut10: Some(nut10),
  223. };
  224. compare_formats(&payment_request, "Very Complex")?;
  225. Ok(())
  226. }
  227. fn compare_formats(
  228. payment_request: &PaymentRequest,
  229. label: &str,
  230. ) -> Result<(), Box<dyn std::error::Error>> {
  231. // Encode using NUT-18 (CBOR/base64, creqA)
  232. let nut18_encoded = payment_request.to_string();
  233. // Encode using NUT-26 (Bech32m, CREQB)
  234. let nut26_encoded = payment_request.to_bech32_string()?;
  235. // Calculate sizes
  236. let nut18_size = nut18_encoded.len();
  237. let nut26_size = nut26_encoded.len();
  238. let size_diff = nut26_size as i32 - nut18_size as i32;
  239. let size_ratio = (nut26_size as f64 / nut18_size as f64) * 100.0;
  240. println!(" {} Payment Request:", label);
  241. println!(" Payment ID: {:?}", payment_request.payment_id);
  242. println!(" Amount: {:?}", payment_request.amount);
  243. println!(
  244. " Mints: {}",
  245. payment_request.mints.as_ref().map_or(0, |m| m.len())
  246. );
  247. println!(" Transports: {}", payment_request.transports.len());
  248. println!(" NUT-10: {}", payment_request.nut10.is_some());
  249. println!("\n NUT-18 (CBOR/base64, creqA):");
  250. println!(" Size: {} bytes", nut18_size);
  251. println!(
  252. " Format: {}",
  253. &nut18_encoded[..nut18_encoded.len().min(80)]
  254. );
  255. if nut18_encoded.len() > 80 {
  256. println!(" ... ({} more chars)", nut18_encoded.len() - 80);
  257. }
  258. println!("\n NUT-26 (Bech32m, CREQB):");
  259. println!(" Size: {} bytes", nut26_size);
  260. println!(
  261. " Format: {}",
  262. &nut26_encoded[..nut26_encoded.len().min(80)]
  263. );
  264. if nut26_encoded.len() > 80 {
  265. println!(" ... ({} more chars)", nut26_encoded.len() - 80);
  266. }
  267. println!("\n Comparison:");
  268. println!(
  269. " Size difference: {} bytes ({:.1}%)",
  270. size_diff, size_ratio
  271. );
  272. if size_diff < 0 {
  273. println!(" Winner: NUT-26 is {} bytes smaller!", size_diff.abs());
  274. } else if size_diff > 0 {
  275. println!(" Winner: NUT-18 is {} bytes smaller!", size_diff);
  276. } else {
  277. println!(" Equal size!");
  278. }
  279. // Analyze QR code efficiency
  280. analyze_qr_efficiency(&nut18_encoded, &nut26_encoded);
  281. // Verify round-trip for both formats
  282. println!("\n Round-trip verification:");
  283. // NUT-18 round-trip
  284. let nut18_decoded = PaymentRequest::from_str(&nut18_encoded)?;
  285. assert_eq!(nut18_decoded.payment_id, payment_request.payment_id);
  286. assert_eq!(nut18_decoded.amount, payment_request.amount);
  287. println!(" NUT-18: ✓ Decoded successfully");
  288. // NUT-26 round-trip
  289. let nut26_decoded = PaymentRequest::from_str(&nut26_encoded)?;
  290. assert_eq!(nut26_decoded.payment_id, payment_request.payment_id);
  291. assert_eq!(nut26_decoded.amount, payment_request.amount);
  292. println!(" NUT-26: ✓ Decoded successfully");
  293. // Verify both decode to the same data
  294. assert_eq!(nut18_decoded.payment_id, nut26_decoded.payment_id);
  295. assert_eq!(nut18_decoded.amount, nut26_decoded.amount);
  296. assert_eq!(nut18_decoded.unit, nut26_decoded.unit);
  297. assert_eq!(nut18_decoded.single_use, nut26_decoded.single_use);
  298. assert_eq!(nut18_decoded.description, nut26_decoded.description);
  299. println!(" ✓ Both formats decode to identical data");
  300. Ok(())
  301. }
  302. fn analyze_qr_efficiency(nut18: &str, nut26: &str) {
  303. // QR codes have different encoding modes:
  304. // - Alphanumeric: 0-9, A-Z (uppercase), space, $, %, *, +, -, ., /, : (most efficient for text)
  305. // - Byte: any data (less efficient)
  306. let alphanumeric_chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:";
  307. let nut18_alphanumeric = nut18
  308. .chars()
  309. .filter(|c| alphanumeric_chars.contains(c.to_ascii_uppercase()))
  310. .count();
  311. let nut18_alphanumeric_ratio = (nut18_alphanumeric as f64 / nut18.len() as f64) * 100.0;
  312. let nut26_alphanumeric = nut26
  313. .chars()
  314. .filter(|c| alphanumeric_chars.contains(c.to_ascii_uppercase()))
  315. .count();
  316. let nut26_alphanumeric_ratio = (nut26_alphanumeric as f64 / nut26.len() as f64) * 100.0;
  317. println!("\n QR Code Efficiency:");
  318. println!(
  319. " NUT-18: {:.1}% alphanumeric-compatible",
  320. nut18_alphanumeric_ratio
  321. );
  322. println!(
  323. " NUT-26: {:.1}% alphanumeric-compatible",
  324. nut26_alphanumeric_ratio
  325. );
  326. if nut26_alphanumeric_ratio > nut18_alphanumeric_ratio {
  327. println!(
  328. " NUT-26 is more QR-friendly (+{:.1}%)",
  329. nut26_alphanumeric_ratio - nut18_alphanumeric_ratio
  330. );
  331. }
  332. // Estimate QR version (simplified)
  333. let nut18_qr_version = estimate_qr_version(nut18.len(), nut18_alphanumeric_ratio > 80.0);
  334. let nut26_qr_version = estimate_qr_version(nut26.len(), nut26_alphanumeric_ratio > 80.0);
  335. println!(
  336. " NUT-18 QR version: ~{} ({}×{} modules)",
  337. nut18_qr_version,
  338. 21 + (nut18_qr_version - 1) * 4,
  339. 21 + (nut18_qr_version - 1) * 4
  340. );
  341. println!(
  342. " NUT-26 QR version: ~{} ({}×{} modules)",
  343. nut26_qr_version,
  344. 21 + (nut26_qr_version - 1) * 4,
  345. 21 + (nut26_qr_version - 1) * 4
  346. );
  347. }
  348. fn estimate_qr_version(data_length: usize, is_alphanumeric: bool) -> u8 {
  349. // Simplified QR version estimation (Level L - Low error correction)
  350. let capacity = if is_alphanumeric {
  351. // Alphanumeric mode capacity
  352. match data_length {
  353. 0..=20 => 1,
  354. 21..=38 => 2,
  355. 39..=61 => 3,
  356. 62..=90 => 4,
  357. 91..=122 => 5,
  358. 123..=154 => 6,
  359. 155..=192 => 7,
  360. 193..=230 => 8,
  361. 231..=271 => 9,
  362. 272..=321 => 10,
  363. 322..=367 => 11,
  364. 368..=425 => 12,
  365. 426..=458 => 13,
  366. 459..=520 => 14,
  367. 521..=586 => 15,
  368. _ => 16,
  369. }
  370. } else {
  371. // Byte mode capacity
  372. match data_length {
  373. 0..=14 => 1,
  374. 15..=26 => 2,
  375. 27..=42 => 3,
  376. 43..=62 => 4,
  377. 63..=84 => 5,
  378. 85..=106 => 6,
  379. 107..=122 => 7,
  380. 123..=152 => 8,
  381. 153..=180 => 9,
  382. 181..=213 => 10,
  383. 214..=251 => 11,
  384. 252..=287 => 12,
  385. 288..=331 => 13,
  386. 332..=362 => 14,
  387. 363..=394 => 15,
  388. _ => 16,
  389. }
  390. };
  391. capacity
  392. }
  393. fn summary() {
  394. println!(" Key Observations:");
  395. println!(" • NUT-18 (creqA): CBOR binary + URL-safe base64 encoding");
  396. println!(" • NUT-26 (CREQB): TLV binary + Bech32m encoding");
  397. println!(" • Bech32m is optimized for QR codes (uppercase alphanumeric)");
  398. println!(" • CBOR may be more compact for complex nested structures");
  399. println!(" • Both formats support the same feature set");
  400. println!(" • NUT-26 has better error detection (Bech32m checksum)");
  401. println!(" • NUT-26 is case-insensitive for parsing");
  402. println!(" • Both can be parsed from the same FromStr implementation");
  403. }
  404. #[cfg(test)]
  405. mod tests {
  406. use super::*;
  407. #[test]
  408. fn test_minimal_comparison() {
  409. assert!(minimal_comparison().is_ok());
  410. }
  411. #[test]
  412. fn test_amount_unit_comparison() {
  413. assert!(amount_unit_comparison().is_ok());
  414. }
  415. #[test]
  416. fn test_multiple_mints_comparison() {
  417. assert!(multiple_mints_comparison().is_ok());
  418. }
  419. #[test]
  420. fn test_transport_comparison() {
  421. assert!(transport_comparison().is_ok());
  422. }
  423. #[test]
  424. fn test_complete_with_nut10_comparison() {
  425. assert!(complete_with_nut10_comparison().is_ok());
  426. }
  427. #[test]
  428. fn test_very_complex_comparison() {
  429. assert!(very_complex_comparison().is_ok());
  430. }
  431. #[test]
  432. fn test_round_trip_equivalence() {
  433. let payment_request = PaymentRequest {
  434. payment_id: Some("test".to_string()),
  435. amount: Some(Amount::from(1000)),
  436. unit: Some(CurrencyUnit::Sat),
  437. single_use: None,
  438. mints: Some(vec![MintUrl::from_str("https://mint.example.com").unwrap()]),
  439. description: Some("Test".to_string()),
  440. transports: vec![],
  441. nut10: None,
  442. };
  443. // Encode both ways
  444. let nut18 = payment_request.to_string();
  445. let nut26 = payment_request.to_bech32_string().unwrap();
  446. // Decode both
  447. let from_nut18 = PaymentRequest::from_str(&nut18).unwrap();
  448. let from_nut26 = PaymentRequest::from_str(&nut26).unwrap();
  449. // Should be equal
  450. assert_eq!(from_nut18.payment_id, from_nut26.payment_id);
  451. assert_eq!(from_nut18.amount, from_nut26.amount);
  452. assert_eq!(from_nut18.unit, from_nut26.unit);
  453. assert_eq!(from_nut18.description, from_nut26.description);
  454. }
  455. }