From 0a6ef0c515a4b15e31455a990e1a0e515dc26581 Mon Sep 17 00:00:00 2001 From: ok300 <106775972+ok300@users.noreply.github.com> Date: Fri, 1 Nov 2024 03:13:05 +0100 Subject: [PATCH 1/5] Add NUT-XX: Describe a primes-based keyset, proof scheme --- xx.md | 129 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 xx.md diff --git a/xx.md b/xx.md new file mode 100644 index 0000000..aac52a3 --- /dev/null +++ b/xx.md @@ -0,0 +1,129 @@ +# NUT-XX: Prime proofs + +`optional` + +`depends on: NUT-XX (Keyset extension that allows the mint to signal the keyset type via a new field keyset_type)` + +--- + +This NUT proposes a proof selection scheme that enables more compact tokens. With it, a token can represent any even amount with at most 2 proofs and any odd amount with at most 3 proofs. + +--- + +Goldbach's conjecture states that every even natural number greater than 2 is the sum of two primes. Though unproven, it was shown to hold at least up to `4 * 10^19`[^1]. This NUT is for mints and wallets dealing with maximum amounts below this value. + +Based on this, a token can be decomposed in proofs as follows: + +| Token amount | Proof amounts | +|-------------:|-------------------------:| +| 1 | 1 | +| `p` | `p` | +| `2n` | `p1 + p2` | +| `2n + 1` | `p + 2` or `p1 + p2 + 1` | + +where `p`, `p1` and `p2` are primes. + +In other words, any amount can be built from at most 3 proofs, but sometimes from only 1 or 2. + +To allow wallets to use this scheme, a keyset has to include signatures for `1` and primes up to `P`. + + +### Client algorithm + +There are multiple possible decompositions[^2] for each token amount. When building a token, the wallet only has to find one. + +For very low-powered devices, the app can include a table with precomputed decompositions for `1..M`. + +Otherwise, the wallet could decompose an amount as follows: + +``` +// Inputs: +// - keyset_amounts[]: 1 and the first primes until P +// - token_amount: amount for which to construct a token, token_amount <= mint maximum M +function find_proof_amounts(keyset_amounts[], token_amount): + // Special cases: if token_amount is 1 or a prime + if keyset_amounts[] contains token_amount + return [token_amount] + + proof_amounts = [] + + target = + if token_amount % 2 == 0 + token_amount + else + append 1 to proof_amounts[] + token_amount - 1 + + for p1 in keyset_amounts + if p1 > target / 2 + break + + p2 = target - p1 + + if keyset_amounts[] not contains p2 + continue + + append p1 to proof_amounts[] + append p2 to proof_amounts[] + return proof_amounts[] + + return Error("No matching primes found") +``` + + +### Mint Considerations + +When choosing how many primes to include, the mint has 2 limiting factors: + + - the maximum token amount the mint allows to be minted or melted `M = max(MAX_mint, MAX_melt)` + - the payload size for the `GET /v1/keys/:keyset_id` endpoint + + +#### Range + +A naive, but simple approach is for the keyset to include all primes up to `P <= M`. + + +#### Payload size + +Here are a few sample keysets that contain signatures for `1` and primes up to `P`, which allow all token amounts `1..=M` to be represented as a sum of up to 3 keyset amounts: + +| Number of primes | Max prime `P` | Max token amount `M` | Decomposition of `M` | Keyset size | 4G (ms) | +|-----------------:|--------------:|---------------------:|----------------------------:|------------:|--------:| +| `100` | `541` | `967` | `479 + 487 + 1` | 9 KB | 3.6 | +| `1_000` | `7_919` | `15_523` | `7_699 + 7_823 + 1` | 86 KB | 34.4 | +| `10_000` | `104_729` | `208_927` | `104_399 + 104_527 + 1` | 869 KB | 347.6 | +| `100_000` | `1_299_709` | `2_598_333` | `1_298_779 + 1_299_553 + 1` | 8.8 MB | 3604.5 | + +Mints which mint or melt amounts of up to `~200_000` can use a keyset containing signatures for `1` and the first `10_000` primes. The `GET /v1/keys/:keyset_id` uncompressed payload would be ~870 KB. With HTTP compression, that can be halved[^3]. The uncompressed payload can be downloaded over 4G in about 350 ms, assuming an average speed of 20 Mbps. + +Since the keyset signatures are not fetched that often[^4] by the wallets, even a 3.6s download time (or 1.8s with HTTP compression) could be acceptable for mints that wish to support max amounts of `~2_500_000`. + + +### Summary + +This NUT brings consistently small tokens (up to 3 proofs for any token amount) in exchange for a larger keyset. The keyset size can be reduced by the mint choosing a lower maximum mint or melt amount `M`. Furthermore, the keyset size can be halved if the mint and the wallets support HTTP compression. + +In contrast, keysets that use amounts that are powers of two often result in larger tokens (i.e. more proofs needed): + +| Token amount | Proof amounts (powers of two) | Proof amounts (this NUT) | +|-------------:|:------------------------------|:-------------------------| +| 6 | 2, 4 | 3, 3 | +| 7 | 1, 2, 4 | 2, 5 | +| 10 | 2, 8 | 5, 5 | +| 30 | 2, 4, 8, 16 | 7, 23 | +| 31 | 1, 2, 4, 8, 16 | 1, 7, 23 | +| 63 | 1, 2, 4, 8, 16, 32 | 1, 3, 59 | +| 127 | 1, 2, 4, 8, 16, 32, 64 | 127 | +| 255 | 1, 2, 4, 8, 16, 32, 64, 128 | 1, 3, 251 | +| 700 | 4, 8, 16, 32, 128, 512 | 17, 683 | +| 1000 | 8, 32, 64, 128, 256, 512 | 3, 997 | + + +[^1]: This is 1 order of magnitude away from what a 64 bit unsigned integer can hold (`~1.8 * 10^20`). + +[^2]: https://en.wikipedia.org/wiki/Goldbach%27s_comet + +[^3]: https://github.com/cashubtc/nuts/pull/176 + +[^4]: As per NUT-02: https://cashubtc.github.io/nuts/02/#wallet-implementation-notes \ No newline at end of file From eb758f10f29eafe6ff58f76422dc63ce09919bb6 Mon Sep 17 00:00:00 2001 From: ok300 <106775972+ok300@users.noreply.github.com> Date: Sun, 3 Nov 2024 06:05:11 +0100 Subject: [PATCH 2/5] Move client pseudocode in Appendix --- xx.md | 77 +++++++++++++++++++++++++++++++---------------------------- 1 file changed, 41 insertions(+), 36 deletions(-) diff --git a/xx.md b/xx.md index aac52a3..c3c7bbd 100644 --- a/xx.md +++ b/xx.md @@ -28,47 +28,13 @@ In other words, any amount can be built from at most 3 proofs, but sometimes fro To allow wallets to use this scheme, a keyset has to include signatures for `1` and primes up to `P`. -### Client algorithm +### Client Considerations There are multiple possible decompositions[^2] for each token amount. When building a token, the wallet only has to find one. For very low-powered devices, the app can include a table with precomputed decompositions for `1..M`. -Otherwise, the wallet could decompose an amount as follows: - -``` -// Inputs: -// - keyset_amounts[]: 1 and the first primes until P -// - token_amount: amount for which to construct a token, token_amount <= mint maximum M -function find_proof_amounts(keyset_amounts[], token_amount): - // Special cases: if token_amount is 1 or a prime - if keyset_amounts[] contains token_amount - return [token_amount] - - proof_amounts = [] - - target = - if token_amount % 2 == 0 - token_amount - else - append 1 to proof_amounts[] - token_amount - 1 - - for p1 in keyset_amounts - if p1 > target / 2 - break - - p2 = target - p1 - - if keyset_amounts[] not contains p2 - continue - - append p1 to proof_amounts[] - append p2 to proof_amounts[] - return proof_amounts[] - - return Error("No matching primes found") -``` +Otherwise, the wallet could decompose the amount using the algorithm in the Appendix below. ### Mint Considerations @@ -120,6 +86,45 @@ In contrast, keysets that use amounts that are powers of two often result in lar | 1000 | 8, 32, 64, 128, 256, 512 | 3, 997 | +### Appendix + +#### Client Algorithm + +``` +// Inputs: +// - keyset_amounts[]: 1 and the first primes until P +// - token_amount: amount for which to construct a token, token_amount <= mint maximum M +function find_proof_amounts(keyset_amounts[], token_amount): + // Special cases: if token_amount is 1 or a prime + if keyset_amounts[] contains token_amount + return [token_amount] + + proof_amounts = [] + + target = + if token_amount % 2 == 0 + token_amount + else + append 1 to proof_amounts[] + token_amount - 1 + + for p1 in keyset_amounts + if p1 > target / 2 + break + + p2 = target - p1 + + if keyset_amounts[] not contains p2 + continue + + append p1 to proof_amounts[] + append p2 to proof_amounts[] + return proof_amounts[] + + return Error("No matching primes found") +``` + + [^1]: This is 1 order of magnitude away from what a 64 bit unsigned integer can hold (`~1.8 * 10^20`). [^2]: https://en.wikipedia.org/wiki/Goldbach%27s_comet From 12d7fe7b6e726ea758348d7f06d417e02eae458a Mon Sep 17 00:00:00 2001 From: ok300 <106775972+ok300@users.noreply.github.com> Date: Sun, 3 Nov 2024 06:07:09 +0100 Subject: [PATCH 3/5] Simplify client pseudocode --- xx.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/xx.md b/xx.md index c3c7bbd..28d9f65 100644 --- a/xx.md +++ b/xx.md @@ -100,13 +100,11 @@ function find_proof_amounts(keyset_amounts[], token_amount): return [token_amount] proof_amounts = [] + target = token_amount - target = - if token_amount % 2 == 0 - token_amount - else - append 1 to proof_amounts[] - token_amount - 1 + if token_amount % 2 == 1 + append 1 to proof_amounts[] + target = target - 1 for p1 in keyset_amounts if p1 > target / 2 From 3853f265a7097e5a589381cf2d6688574a3cf5d0 Mon Sep 17 00:00:00 2001 From: ok300 <106775972+ok300@users.noreply.github.com> Date: Sun, 3 Nov 2024 12:28:46 +0100 Subject: [PATCH 4/5] Apply npx prettier fixes --- xx.md | 33 +++++++++++++-------------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/xx.md b/xx.md index 28d9f65..67ed413 100644 --- a/xx.md +++ b/xx.md @@ -15,7 +15,7 @@ Goldbach's conjecture states that every even natural number greater than 2 is th Based on this, a token can be decomposed in proofs as follows: | Token amount | Proof amounts | -|-------------:|-------------------------:| +| -----------: | -----------------------: | | 1 | 1 | | `p` | `p` | | `2n` | `p1 + p2` | @@ -27,7 +27,6 @@ In other words, any amount can be built from at most 3 proofs, but sometimes fro To allow wallets to use this scheme, a keyset has to include signatures for `1` and primes up to `P`. - ### Client Considerations There are multiple possible decompositions[^2] for each token amount. When building a token, the wallet only has to find one. @@ -36,26 +35,23 @@ For very low-powered devices, the app can include a table with precomputed decom Otherwise, the wallet could decompose the amount using the algorithm in the Appendix below. - ### Mint Considerations When choosing how many primes to include, the mint has 2 limiting factors: - - the maximum token amount the mint allows to be minted or melted `M = max(MAX_mint, MAX_melt)` - - the payload size for the `GET /v1/keys/:keyset_id` endpoint - +- the maximum token amount the mint allows to be minted or melted `M = max(MAX_mint, MAX_melt)` +- the payload size for the `GET /v1/keys/:keyset_id` endpoint #### Range A naive, but simple approach is for the keyset to include all primes up to `P <= M`. - #### Payload size Here are a few sample keysets that contain signatures for `1` and primes up to `P`, which allow all token amounts `1..=M` to be represented as a sum of up to 3 keyset amounts: | Number of primes | Max prime `P` | Max token amount `M` | Decomposition of `M` | Keyset size | 4G (ms) | -|-----------------:|--------------:|---------------------:|----------------------------:|------------:|--------:| +| ---------------: | ------------: | -------------------: | --------------------------: | ----------: | ------: | | `100` | `541` | `967` | `479 + 487 + 1` | 9 KB | 3.6 | | `1_000` | `7_919` | `15_523` | `7_699 + 7_823 + 1` | 86 KB | 34.4 | | `10_000` | `104_729` | `208_927` | `104_399 + 104_527 + 1` | 869 KB | 347.6 | @@ -65,7 +61,6 @@ Mints which mint or melt amounts of up to `~200_000` can use a keyset containing Since the keyset signatures are not fetched that often[^4] by the wallets, even a 3.6s download time (or 1.8s with HTTP compression) could be acceptable for mints that wish to support max amounts of `~2_500_000`. - ### Summary This NUT brings consistently small tokens (up to 3 proofs for any token amount) in exchange for a larger keyset. The keyset size can be reduced by the mint choosing a lower maximum mint or melt amount `M`. Furthermore, the keyset size can be halved if the mint and the wallets support HTTP compression. @@ -73,7 +68,7 @@ This NUT brings consistently small tokens (up to 3 proofs for any token amount) In contrast, keysets that use amounts that are powers of two often result in larger tokens (i.e. more proofs needed): | Token amount | Proof amounts (powers of two) | Proof amounts (this NUT) | -|-------------:|:------------------------------|:-------------------------| +| -----------: | :---------------------------- | :----------------------- | | 6 | 2, 4 | 3, 3 | | 7 | 1, 2, 4 | 2, 5 | | 10 | 2, 8 | 5, 5 | @@ -85,7 +80,6 @@ In contrast, keysets that use amounts that are powers of two often result in lar | 700 | 4, 8, 16, 32, 128, 512 | 17, 683 | | 1000 | 8, 32, 64, 128, 256, 512 | 3, 997 | - ### Appendix #### Client Algorithm @@ -98,35 +92,34 @@ function find_proof_amounts(keyset_amounts[], token_amount): // Special cases: if token_amount is 1 or a prime if keyset_amounts[] contains token_amount return [token_amount] - + proof_amounts = [] target = token_amount - + if token_amount % 2 == 1 append 1 to proof_amounts[] target = target - 1 - + for p1 in keyset_amounts if p1 > target / 2 break - + p2 = target - p1 - + if keyset_amounts[] not contains p2 continue - + append p1 to proof_amounts[] append p2 to proof_amounts[] return proof_amounts[] - + return Error("No matching primes found") ``` - [^1]: This is 1 order of magnitude away from what a 64 bit unsigned integer can hold (`~1.8 * 10^20`). [^2]: https://en.wikipedia.org/wiki/Goldbach%27s_comet [^3]: https://github.com/cashubtc/nuts/pull/176 -[^4]: As per NUT-02: https://cashubtc.github.io/nuts/02/#wallet-implementation-notes \ No newline at end of file +[^4]: As per NUT-02: https://cashubtc.github.io/nuts/02/#wallet-implementation-notes From 897b0c778b5b078eae74d57561371bf34198c8bd Mon Sep 17 00:00:00 2001 From: ok300 <106775972+ok300@users.noreply.github.com> Date: Sun, 3 Nov 2024 13:50:45 +0100 Subject: [PATCH 5/5] Add 2nd client algorithm (better fee savings, privacy) --- xx.md | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 65 insertions(+), 5 deletions(-) diff --git a/xx.md b/xx.md index 67ed413..5bc88b8 100644 --- a/xx.md +++ b/xx.md @@ -31,9 +31,9 @@ To allow wallets to use this scheme, a keyset has to include signatures for `1` There are multiple possible decompositions[^2] for each token amount. When building a token, the wallet only has to find one. -For very low-powered devices, the app can include a table with precomputed decompositions for `1..M`. +For very low-powered devices, the app can include a table with precomputed decompositions(s) for `1..M`. -Otherwise, the wallet could decompose the amount using the algorithm in the Appendix below. +Otherwise, the wallet could decompose the amount using an algorithm from the Appendix below. ### Mint Considerations @@ -82,9 +82,13 @@ In contrast, keysets that use amounts that are powers of two often result in lar ### Appendix -#### Client Algorithm +#### Client Algorithms + +`ALG1` is a generic way to decompose an amount. ``` +// ALG1: Decompose a token amount into at most 2 (for even amounts) or 3 (for odd ones) keyset amounts +// // Inputs: // - keyset_amounts[]: 1 and the first primes until P // - token_amount: amount for which to construct a token, token_amount <= mint maximum M @@ -116,9 +120,65 @@ function find_proof_amounts(keyset_amounts[], token_amount): return Error("No matching primes found") ``` -[^1]: This is 1 order of magnitude away from what a 64 bit unsigned integer can hold (`~1.8 * 10^20`). +`ALG2` is a variation that lets the wallet specify a list of preferred amounts. This is useful for wallets that wish to re-use their existing proofs in the decomposition. + +Uses cases may include: + +- fee savings: if Alice can re-use existing proof amounts, she could avoid a swap and thereby save fees. This is different from simply swapping the difference, because the difference may result in 2 or 3 proofs, whereby using `ALG2` could result in fewer. +- privacy: if Alice wishes to send `X = px1 + px2` and she already has `1, p1`, then using `ALG2` with `preferred_proof_amounts[] = [1, p1]` she may find a more favorable decomposition `X = 1 + p1 + p2`. She may then mint the missing `p2` and send Carol `X = 1 + px1 + px2`, who then swaps them. The mint sees "someone minted `px1`" and "someone swapped `1, px1, px2`". This is a weaker correlation than if the same 2 primes are minted by Alice and swapped by Carol. + +``` +// ALG2: Decompose a token amount into keyset amounts. Uses, if possible, amounts from preferred_proof_amounts. +// +// Inputs: +// - keyset_amounts[]: 1 and the first primes until P +// - token_amount: amount for which to construct a token, token_amount <= mint maximum M +// - preferred_proof_amounts[]: keyset amounts which should be included in the result, if possible +function find_proof_amounts(keyset_amounts[], token_amount, preferred_proof_amounts[]): + // Special cases: if token_amount is 1 or a prime + if keyset_amounts[] contains token_amount + return [token_amount] + + proof_amounts = [] + target = token_amount + + if token_amount % 2 == 1 + append 1 to proof_amounts[] + target = target - 1 + + for preferred in preferred_proof_amounts + if keyset_amounts[] not contains preferred + continue + + p1 = preferred + p2 = target - p1 + + if keyset_amounts[] not contains p2 + continue + + append p1 to proof_amounts[] + append p2 to proof_amounts[] + return proof_amounts[] + + for p1 in keyset_amounts + if p1 > target / 2 + break + + p2 = target - p1 + + if keyset_amounts[] not contains p2 + continue + + append p1 to proof_amounts[] + append p2 to proof_amounts[] + return proof_amounts[] + + return Error("No matching primes found") +``` + +[^1]: This is 1 order of magnitude away from what a 64-bit unsigned integer can hold (`~1.8 * 10^20`). -[^2]: https://en.wikipedia.org/wiki/Goldbach%27s_comet +[^2]: For the number of ways to decompose `2n` in a sum of 2 primes, see Goldbach's Comet: https://en.wikipedia.org/wiki/Goldbach%27s_comet [^3]: https://github.com/cashubtc/nuts/pull/176